on develop help sign in
Tweaking various supported credentials layout
be47a98Brendan Hay
gogol/src/Network/Google.hs+5
@@ -77,6 +77,10 @@ module Network.Google
77 -- ** Constructing a Logger
77 -- ** Constructing a Logger
78 , newLogger
78 , newLogger
79
79
80+ -- * Constructing a HTTP Manager
81+ , newManager
82+ , tlsManagerSettings
83+
80 -- * Re-exported Types
84 -- * Re-exported Types
81 , Proxy (..)
85 , Proxy (..)
82 , OctetStream
86 , OctetStream
@@ -110,6 +114,7 @@ import Network.Google.Internal.HTTP
110import Network.Google.Internal.Logger
114import Network.Google.Internal.Logger
111import Network.Google.Prelude
115import Network.Google.Prelude
112import Network.Google.Types
116import Network.Google.Types
117+import Network.HTTP.Conduit (newManager, tlsManagerSettings)
113
118
114newtype Google a = Google { unGoogle :: ReaderT Env (ResourceT IO) a }
119newtype Google a = Google { unGoogle :: ReaderT Env (ResourceT IO) a }
115 deriving
120 deriving
gogol/src/Network/Google/Auth.hs+13−116
@@ -22,22 +22,22 @@ module Network.Google.Auth
22 -- * Credentials
22 -- * Credentials
23 Credentials (..)
23 Credentials (..)
24
24
25− -- ** Discovery
25+ -- ** Application Default Credentials
26− , discover
26+ , getApplicationDefault
27− , fromFile
27+ , fromWellKnownPath
28 , fromFilePath
28 , fromFilePath
29− , fromJSONCredentials
29+
30+ -- ** Installed Application Credentials
31+ , formURL
30
32
31 -- ** Authorizing Requests
33 -- ** Authorizing Requests
32 , authorize
34 , authorize
33
35
34− -- ** Constructing Callback URLs
35− , formURL
36−
37 -- ** Thread-safe Storage
36 -- ** Thread-safe Storage
38− , Auth (..)
39 , Store
37 , Store
40 , initStore
38 , initStore
39+
40+ , Auth (..)
41 , exchange
41 , exchange
42 , refresh
42 , refresh
43
43
@@ -113,114 +113,6 @@ import System.Environment (lookupEnv)
113import System.FilePath ((</>))
113import System.FilePath ((</>))
114import System.Info (os)
114import System.Info (os)
115
115
116−-- | The supported credential mechanisms.
117−data Credentials
118− = FromMetadata !ServiceId
119− -- ^ Obtain and refresh access tokens from the underlying GCE host metadata
120− -- at @http:\/\/169.254.169.254@.
121−
122− | FromClient !OAuthClient !OAuthCode
123− -- ^ Obtain and refresh access tokens using the specified client secret
124− -- and authorization code obtained from.
125− --
126− -- See the <https://developers.google.com/accounts/docs/OAuth2InstalledApp OAuth2 Installed Application>
127− -- documentation for more information.
128−
129− | FromAccount !ServiceAccount ![OAuthScope]
130− -- ^ Use the specified @service_account@ and scopes to sign and request
131− -- an access token. The 'ServiceAccount' will also be used for subsequent
132− -- token refreshes.
133− --
134− -- A 'ServiceAccount' is typically generated through the
135− -- Google Developer Console.
136−
137− | FromUser !AuthorizedUser
138− -- ^ Use the specified @authorized_user@ to obtain and refresh access tokens.
139− --
140− -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
141− -- of the Google CloudSDK Tools.
142−
143−-- | Attempt credentials discovery via the following steps:
144−--
145−-- * Read the default credentials from a file specified by
146−-- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
147−--
148−-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
149−-- The @~/.config@ component of the path can be overriden by the environment
150−-- variable @CLOUDSDK_CONFIG@ if it exists.
151−--
152−-- * Retrieve the default service account application credentials if
153−-- running on GCE. The environment variable @NO_GCE_CHECK@ can be used to
154−-- skip this check if set to a truthy value such as @1@ or @true@.
155−--
156−-- The specified 'Scope's are used to authorize any @service_account@ that is
157−-- found with the appropriate scopes, otherwise they are not used. See the
158−-- top-level module of each individual @gogol-*@ library for a list of available
159−-- scopes, such as @Network.Google.Compute.computeScope@.
160−discover :: (MonadIO m, MonadCatch m)
161− => [OAuthScope]
162− -> Manager
163− -> m Credentials
164−discover ss m =
165− catching _MissingFileError (fromFile ss) $ \f -> do
166− p <- isGCE m
167− unless p $
168− throwingM _MissingFileError f
169− pure $! FromMetadata "default"
170−
171−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
172−-- file to obtain the credentials neccessary to perform a token refresh.
173−--
174−-- The specified 'Scope's are used to authorize any @service_account@ that is
175−-- found with the appropriate scopes, otherwise they are not used. See the
176−-- top-level module of each individual @gogol-*@ library for a list of available
177−-- scopes, such as @Network.Google.Compute.computeScope@.
178−--
179−-- /See:/ 'cloudSDKConfigPath', 'defaultCredentialsPath'.
180−fromFile :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Credentials
181−fromFile ss = do
182− f <- defaultCredentialsPath
183− case f of
184− Just x -> fromFilePath ss x
185− Nothing -> do
186− x <- cloudSDKConfigPath
187− fromFilePath ss x
188−
189−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
190−-- file to obtain the credentials neccessary to perform a token refresh from
191−-- the specified file.
192−--
193−-- The specified 'Scope's are used to authorize any @service_account@ that is
194−-- found with the appropriate scopes, otherwise they are not used. See the
195−-- top-level module of each individual @gogol-*@ library for a list of available
196−-- scopes, such as @Network.Google.Compute.computeScope@.
197−fromFilePath :: (MonadIO m, MonadCatch m)
198− => [OAuthScope]
199− -> FilePath
200− -> m Credentials
201−fromFilePath ss f = do
202− p <- liftIO (doesFileExist f)
203− unless p $
204− throwM (MissingFileError f)
205− bs <- liftIO (LBS.readFile f)
206− either (throwM . InvalidFileError f . Text.pack) pure
207− (fromJSONCredentials ss bs)
208−
209−-- | Attempt to parse either a @service_account@ or @authorized_user@ formatted
210−-- JSON value to obtain credentials.
211−fromJSONCredentials :: [OAuthScope]
212− -> LBS.ByteString
213− -> Either String Credentials
214−fromJSONCredentials ss bs = do
215− v <- eitherDecode' bs
216− let x = (`FromAccount` ss) <$> parseEither parseJSON v
217− y = FromUser <$> parseEither parseJSON v
218− case (x, y) of
219− (Left xe, Left ye) -> Left $
220− "Failed parsing service_account: " ++ xe ++
221− ", Failed parsing authorized_user: " ++ ye
222− _ -> x <|> y
223−
224-- | An 'OAuthToken' that can potentially be expired, with the original
116-- | An 'OAuthToken' that can potentially be expired, with the original
225-- credentials that can be used to perform a refresh.
117-- credentials that can be used to perform a refresh.
226data Auth = Auth
118data Auth = Auth
@@ -230,6 +122,11 @@ data Auth = Auth
230
122
231-- | Check if the given token is still valid, ie. younger than the projected
123-- | Check if the given token is still valid, ie. younger than the projected
232-- expiry time.
124-- expiry time.
125+--
126+-- This deliberately makes no external calls due to the absolute construction of
127+-- the '_tokenExpiry' field, unlike the
128+-- <https://developers.google.com/accounts/docs/OAuth2Login#validatingtoken documented>
129+-- validation method.
233validate :: MonadIO m => Auth -> m Bool
130validate :: MonadIO m => Auth -> m Bool
234validate a = (< _tokenExpiry (_token a)) <$> liftIO getCurrentTime
131validate a = (< _tokenExpiry (_token a)) <$> liftIO getCurrentTime
235
132
gogol/src/Network/Google/Auth/ApplicationDefaultCredentials.hs+115−75
@@ -14,116 +14,156 @@ module Network.Google.Auth.ApplicationDefaultCredentials where
14import Control.Applicative
14import Control.Applicative
15import Control.Concurrent
15import Control.Concurrent
16import Control.Exception.Lens
16import Control.Exception.Lens
17−import Control.Lens hiding (coerce, (.=))
17+import Control.Lens hiding (coerce, (.=))
18import Control.Monad
18import Control.Monad
19import Control.Monad.Catch
19import Control.Monad.Catch
20import Control.Monad.IO.Class
20import Control.Monad.IO.Class
21−import Crypto.Hash.Algorithms (SHA256 (..))
21+import Crypto.Hash.Algorithms (SHA256 (..))
22−import Crypto.PubKey.RSA.PKCS15 (signSafer)
22+import Crypto.PubKey.RSA.PKCS15 (signSafer)
23−import Crypto.PubKey.RSA.Types (PrivateKey)
23+import Crypto.PubKey.RSA.Types (PrivateKey)
24import Data.Aeson
24import Data.Aeson
25import Data.Aeson.Types
25import Data.Aeson.Types
26import Data.ByteArray
26import Data.ByteArray
27import Data.ByteArray.Encoding
27import Data.ByteArray.Encoding
28−import Data.ByteString (ByteString)
28+import Data.ByteString (ByteString)
29−import Data.ByteString.Builder (Builder)
29+import Data.ByteString.Builder (Builder)
30−import qualified Data.ByteString.Builder as Build
30+import qualified Data.ByteString.Builder as Build
31−import qualified Data.ByteString.Char8 as BS8
31+import qualified Data.ByteString.Char8 as BS8
32−import qualified Data.ByteString.Lazy as LBS
32+import qualified Data.ByteString.Lazy as LBS
33import Data.Coerce
33import Data.Coerce
34−import Data.Default.Class (def)
34+import Data.Default.Class (def)
35−import Data.List (intersperse)
35+import Data.List (intersperse)
36import Data.String
36import Data.String
37−import qualified Data.Text as Text
37+import qualified Data.Text as Text
38−import qualified Data.Text.Encoding as Text
38+import qualified Data.Text.Encoding as Text
39−import qualified Data.Text.Lazy as LText
39+import qualified Data.Text.Lazy as LText
40−import qualified Data.Text.Lazy.Builder as TBuild
40+import qualified Data.Text.Lazy.Builder as TBuild
41−import qualified Data.Text.Lazy.Encoding as LText
41+import qualified Data.Text.Lazy.Encoding as LText
42import Data.Time
42import Data.Time
43import Data.Time.Clock.POSIX
43import Data.Time.Clock.POSIX
44import Data.Typeable
44import Data.Typeable
45import Data.X509
45import Data.X509
46import Data.X509.Memory
46import Data.X509.Memory
47+import Network.Google.Auth.ServiceAccount
47import Network.Google.Compute.Metadata
48import Network.Google.Compute.Metadata
48import Network.Google.Internal.Auth
49import Network.Google.Internal.Auth
49import Network.Google.Internal.Logger
50import Network.Google.Internal.Logger
50−import Network.Google.Prelude hiding (buildText)
51+import Network.Google.Prelude hiding (buildText)
51−import Network.HTTP.Conduit hiding (Request)
52+import Network.HTTP.Conduit hiding (Request)
52−import qualified Network.HTTP.Conduit as Client
53+import qualified Network.HTTP.Conduit as Client
53import Network.HTTP.Types
54import Network.HTTP.Types
54−import System.Directory (doesFileExist,
55+import System.Directory (doesFileExist,
55− getHomeDirectory)
56+ getHomeDirectory)
56−import System.Environment (lookupEnv)
57+import System.Environment (lookupEnv)
57−import System.FilePath ((</>))
58+import System.FilePath ((</>))
58−import System.Info (os)
59+import System.Info (os)
59
60
60-- | The environment variable name which is used to specify the directory
61-- | The environment variable name which is used to specify the directory
61-- containing the @application_default_credentials.json@ generated by @gcloud init@.
62-- containing the @application_default_credentials.json@ generated by @gcloud init@.
62cloudSDKConfigDir :: String
63cloudSDKConfigDir :: String
63cloudSDKConfigDir = "CLOUDSDK_CONFIG"
64cloudSDKConfigDir = "CLOUDSDK_CONFIG"
64
65
66+-- | Return the filepath to the Cloud SDK well known file location such as
67+-- @~\/.config\/gcloud\/application_default_credentials.json@.
68+cloudSDKConfigPath :: MonadIO m => m FilePath
69+cloudSDKConfigPath = do
70+ m <- liftIO (lookupEnv cloudSDKConfigDir)
71+ case m of
72+ Just d -> pure $! d </> "application_default_credentials.json"
73+ Nothing -> do
74+ d <- getConfigDirectory
75+ pure $! d </> "gcloud/application_default_credentials.json"
76+
65-- | The environment variable pointing the file with local
77-- | The environment variable pointing the file with local
66-- Application Default Credentials.
78-- Application Default Credentials.
67defaultCredentialsFile :: String
79defaultCredentialsFile :: String
68defaultCredentialsFile = "GOOGLE_APPLICATION_CREDENTIALS"
80defaultCredentialsFile = "GOOGLE_APPLICATION_CREDENTIALS"
69
81
70−{-| Authorized User credentials which are typically generated by the Cloud SDK
71−Tools such as @gcloud init@, of the following form:
72−
73−@
74−{
75− \"type\": \"authorized_user\",
76− \"client_id\": \"32555940559.apps.googleusercontent.com\",
77− \"client_secret\": \"Zms2qjJy2998hD4CTg2ejr2\",
78− \"refresh_token\": \"1/B3gM1x35v.VtqffS1n5w-rSJ\"
79−}
80−@
81−
82−The secret and refresh token are used to obtain a valid 'OAuthToken' from
83−'accountsURL' using grant_type @refresh_token@.
84−-}
85−data AuthorizedUser = AuthorizedUser
86− { _userId :: !ClientId
87− , _userRefresh :: !RefreshToken
88− , _userSecret :: !Secret
89− } deriving (Eq, Show)
90−
91−instance FromJSON AuthorizedUser where
92− parseJSON = withObject "authorized_user" $ \o -> AuthorizedUser
93− <$> o .: "client_id"
94− <*> o .: "refresh_token"
95− <*> o .: "client_secret"
96−
97−authorizedUserToken :: (MonadIO m, MonadCatch m)
98− => AuthorizedUser
99− -> Maybe RefreshToken
100− -> Logger
101− -> Manager
102− -> m OAuthToken
103−authorizedUserToken u r = refreshRequest $
104− accountsRequest
105− { Client.requestBody = textBody $
106− "grant_type=refresh_token"
107− <> "&client_id=" <> toText (_userId u)
108− <> "&client_secret=" <> toText (_userSecret u)
109− <> "&refresh_token=" <> toText (fromMaybe (_userRefresh u) r)
110− }
111−
112-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
82-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
113-- default application credentials filepath.
83-- default application credentials filepath.
114defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
84defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
115defaultCredentialsPath = liftIO (lookupEnv defaultCredentialsFile)
85defaultCredentialsPath = liftIO (lookupEnv defaultCredentialsFile)
116
86
117−-- | Return the filepath to the Cloud SDK well known file location such as
87+-- | Attempt credentials discovery via the following steps:
88+--
89+-- * Read the default credentials from a file specified by
90+-- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
91+--
118−-- @~\/.config\/gcloud\/application_default_credentials.json@.
92+-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
119−cloudSDKConfigPath :: MonadIO m => m FilePath
93+-- The @~/.config@ component of the path can be overriden by the environment
120−cloudSDKConfigPath = do
94+-- variable @CLOUDSDK_CONFIG@ if it exists.
121− m <- liftIO (lookupEnv cloudSDKConfigDir)
95+--
96+-- * Retrieve the default service account application credentials if
97+-- running on GCE. The environment variable @NO_GCE_CHECK@ can be used to
98+-- skip this check if set to a truthy value such as @1@ or @true@.
99+--
100+-- The specified 'Scope's are used to authorize any @service_account@ that is
101+-- found with the appropriate scopes, otherwise they are not used. See the
102+-- top-level module of each individual @gogol-*@ library for a list of available
103+-- scopes, such as @Network.Google.Compute.computeScope@.
104+getApplicationDefault :: (MonadIO m, MonadCatch m)
105+ => [OAuthScope]
106+ -> Manager
107+ -> m Credentials
108+getApplicationDefault ss m =
109+ catching _MissingFileError (fromWellKnownPath ss) $ \f -> do
110+ p <- isGCE m
111+ unless p $
112+ throwingM _MissingFileError f
113+ pure $! FromMetadata "default"
114+
115+-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
116+-- file to obtain the credentials neccessary to perform a token refresh.
117+--
118+-- The specified 'Scope's are used to authorize any @service_account@ that is
119+-- found with the appropriate scopes, otherwise they are not used. See the
120+-- top-level module of each individual @gogol-*@ library for a list of available
121+-- scopes, such as @Network.Google.Compute.computeScope@.
122+--
123+-- /See:/ 'cloudSDKConfigPath', 'defaultCredentialsPath'.
124+fromWellKnownPath :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Credentials
125+fromWellKnownPath ss = do
126+ f <- defaultCredentialsPath
122− case m of
127+ case f of
123− Just d -> pure $! d </> "application_default_credentials.json"
128+ Just x -> fromFilePath ss x
124 Nothing -> do
129 Nothing -> do
125− d <- getConfigDirectory
130+ x <- cloudSDKConfigPath
126− pure $! d </> "gcloud/application_default_credentials.json"
131+ fromFilePath ss x
132+
133+-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
134+-- file to obtain the credentials neccessary to perform a token refresh from
135+-- the specified file.
136+--
137+-- The specified 'Scope's are used to authorize any @service_account@ that is
138+-- found with the appropriate scopes, otherwise they are not used. See the
139+-- top-level module of each individual @gogol-*@ library for a list of available
140+-- scopes, such as @Network.Google.Compute.computeScope@.
141+fromFilePath :: (MonadIO m, MonadCatch m)
142+ => [OAuthScope]
143+ -> FilePath
144+ -> m Credentials
145+fromFilePath ss f = do
146+ p <- liftIO (doesFileExist f)
147+ unless p $
148+ throwM (MissingFileError f)
149+ bs <- liftIO (LBS.readFile f)
150+ either (throwM . InvalidFileError f . Text.pack) pure
151+ (fromJSONCredentials ss bs)
152+
153+-- | Attempt to parse either a @service_account@ or @authorized_user@ formatted
154+-- JSON value to obtain credentials.
155+fromJSONCredentials :: [OAuthScope]
156+ -> LBS.ByteString
157+ -> Either String Credentials
158+fromJSONCredentials ss bs = do
159+ v <- eitherDecode' bs
160+ let x = (`FromAccount` ss) <$> parseEither parseJSON v
161+ y = FromUser <$> parseEither parseJSON v
162+ case (x, y) of
163+ (Left xe, Left ye) -> Left $
164+ "Failed parsing service_account: " ++ xe ++
165+ ", Failed parsing authorized_user: " ++ ye
166+ _ -> x <|> y
127
167
128getConfigDirectory :: MonadIO m => m FilePath
168getConfigDirectory :: MonadIO m => m FilePath
129getConfigDirectory = do
169getConfigDirectory = do
gogol/src/Network/Google/Auth/Device.hs−45
@@ -9,48 +9,3 @@
9-- Portability : non-portable (GHC extensions)
9-- Portability : non-portable (GHC extensions)
10
10
11module Network.Google.Auth.Device where
11module Network.Google.Auth.Device where
12−
13−import Control.Applicative
14−import Control.Concurrent
15−import Control.Exception.Lens
16−import Control.Lens hiding (coerce, (.=))
17−import Control.Monad
18−import Control.Monad.Catch
19−import Control.Monad.IO.Class
20−import Crypto.Hash.Algorithms (SHA256 (..))
21−import Crypto.PubKey.RSA.PKCS15 (signSafer)
22−import Crypto.PubKey.RSA.Types (PrivateKey)
23−import Data.Aeson
24−import Data.Aeson.Types
25−import Data.ByteArray
26−import Data.ByteArray.Encoding
27−import Data.ByteString (ByteString)
28−import Data.ByteString.Builder (Builder)
29−import qualified Data.ByteString.Builder as Build
30−import qualified Data.ByteString.Char8 as BS8
31−import qualified Data.ByteString.Lazy as LBS
32−import Data.Coerce
33−import Data.Default.Class (def)
34−import Data.List (intersperse)
35−import Data.String
36−import qualified Data.Text as Text
37−import qualified Data.Text.Encoding as Text
38−import qualified Data.Text.Lazy as LText
39−import qualified Data.Text.Lazy.Builder as TBuild
40−import qualified Data.Text.Lazy.Encoding as LText
41−import Data.Time
42−import Data.Time.Clock.POSIX
43−import Data.Typeable
44−import Data.X509
45−import Data.X509.Memory
46−import Network.Google.Compute.Metadata
47−import Network.Google.Internal.Logger
48−import Network.Google.Prelude hiding (buildText)
49−import Network.HTTP.Conduit hiding (Request)
50−import qualified Network.HTTP.Conduit as Client
51−import Network.HTTP.Types
52−import System.Directory (doesFileExist,
53− getHomeDirectory)
54−import System.Environment (lookupEnv)
55−import System.FilePath ((</>))
56−import System.Info (os)
gogol/src/Network/Google/Auth/InstalledApplication.hs+16−6
@@ -50,9 +50,16 @@ import Network.HTTP.Conduit hiding (Request)
50import qualified Network.HTTP.Conduit as Client
50import qualified Network.HTTP.Conduit as Client
51import Network.HTTP.Types
51import Network.HTTP.Types
52
52
53+-- | @urn:ietf:wg:oauth:2.0:oob@.
54+redirectURI :: Text
55+redirectURI = "urn:ietf:wg:oauth:2.0:oob"
56+
53−-- | Given a client identifier, client secret, and a list of scopes to authorize,
57+-- | Given an 'OAuthClient' and a list of scopes to authorize,
54-- construct a URL that can be used to obtain the 'OAuthCode' required to
58-- construct a URL that can be used to obtain the 'OAuthCode' required to
55−-- instantiate 'FromClient'-style credentials.
59+-- instantiate 'FromClient'-style credentials. The received code can be used
60+-- with 'exchangeCode'.
61+--
62+-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl Forming the URL>.
56formURL :: OAuthClient -> [OAuthScope] -> Text
63formURL :: OAuthClient -> [OAuthScope] -> Text
57formURL c ss = accountsURL
64formURL c ss = accountsURL
58 <> "?response_type=code"
65 <> "?response_type=code"
@@ -60,10 +67,10 @@ formURL c ss = accountsURL
60 <> "&redirect_uri=" <> redirectURI
67 <> "&redirect_uri=" <> redirectURI
61 <> "&scope=" <> Text.decodeUtf8 (queryEncodeScopes ss)
68 <> "&scope=" <> Text.decodeUtf8 (queryEncodeScopes ss)
62
69
63−-- | @urn:ietf:wg:oauth:2.0:oob@.
70+-- | Exchange your 'OAuthClient' details and the 'OAuthCode' you received from
64−redirectURI :: Text
71+-- using 'formURL' with a valid 'OAuthToken'.
65−redirectURI = "urn:ietf:wg:oauth:2.0:oob"
72+--
66−
73+-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse Exchanging the code>.
67exchangeCode :: (MonadIO m, MonadCatch m)
74exchangeCode :: (MonadIO m, MonadCatch m)
68 => OAuthClient
75 => OAuthClient
69 -> OAuthCode
76 -> OAuthCode
@@ -80,6 +87,9 @@ exchangeCode c n = refreshRequest $
80 <> "&redirect_uri=" <> redirectURI
87 <> "&redirect_uri=" <> redirectURI
81 }
88 }
82
89
90+-- | Perform a refresh to obtain a valid 'OAuthToken' with a new expiry time.
91+--
92+-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh Refreshing tokens>.
83refreshToken :: (MonadIO m, MonadCatch m)
93refreshToken :: (MonadIO m, MonadCatch m)
84 => OAuthClient
94 => OAuthClient
85 -> OAuthToken
95 -> OAuthToken
gogol/src/Network/Google/Auth/ServiceAccount.hs+41−51
@@ -56,48 +56,48 @@ import System.Environment (lookupEnv)
56import System.FilePath ((</>))
56import System.FilePath ((</>))
57import System.Info (os)
57import System.Info (os)
58
58
59−-- | 1 hour in seconds.
59+-- | The maximum lifetime of a @service_account@ token which is 1 hour, in seconds.
60−maxTokenLifetimeSeconds :: Int
60+maxTokenLifetime :: Seconds
61−maxTokenLifetimeSeconds = 3600
61+maxTokenLifetime = 3600
62
62
63−{-| Service Account credentials which are typically generated/download
63+-- | Obtain an 'OAuthToken' from the local instace metadata
64−from the Google Developer console of the following form:
64+-- using the specific 'ServiceId'.
65−
65+--
66−@
66+-- For example: @http:\/\/169.254.169.254\/instance\/service-accounts\/default\/token@
67−{
67+-- will be retrieved if the given 'ServiceId' is @\"default\"@.
68− \"type\": \"service_account\",
68+metadataToken :: (MonadIO m, MonadCatch m)
69− \"private_key_id\": \"303ad77e5efdf2ce952DFa\",
69+ => ServiceId
70− \"private_key\": \"-----BEGIN PRIVATE KEY-----\n...\n\",
70+ -> Logger
71− \"client_email\": \"email@serviceaccount.com\",
71+ -> Manager
72− \"client_id\": \"035-2-310.useraccount.com\"
72+ -> m OAuthToken
73+metadataToken s = refreshRequest $
74+ metadataRequest
75+ { Client.path = "instance/service-accounts/"
76+ <> Text.encodeUtf8 (toText s)
77+ <> "/token"
73−}
78+ }
74−@
75
79
76−The private key is used to sign a JSON Web Token (JWT) of the
80+-- | Use the 'AuthorizedUser' to obtain a new 'OAuthToken'. If the supplied
77−grant_type @urn:ietf:params:oauth:grant-type:jwt-bearer@, which is sent to
81+-- 'RefreshToken' is 'Nothing', the original 'RefreshToken' from he user will
78−'accountsURL' to obtain a valid 'OAuthToken'. This process requires explicitly
82+-- be used.
79−specifying which 'Scope's the resulting 'OAuthToken' is authorized to access.
83+authorizedUserToken :: (MonadIO m, MonadCatch m)
80−-}
84+ => AuthorizedUser
81−data ServiceAccount = ServiceAccount
85+ -> Maybe RefreshToken
82− { _serviceId :: !ClientId
86+ -> Logger
83− , _serviceEmail :: !Text
87+ -> Manager
84− , _serviceKeyId :: !Text
88+ -> m OAuthToken
85− , _servicePrivateKey :: !PrivateKey
89+authorizedUserToken u r = refreshRequest $
86− } deriving (Eq, Show)
90+ accountsRequest
91+ { Client.requestBody = textBody $
92+ "grant_type=refresh_token"
93+ <> "&client_id=" <> toText (_userId u)
94+ <> "&client_secret=" <> toText (_userSecret u)
95+ <> "&refresh_token=" <> toText (fromMaybe (_userRefresh u) r)
96+ }
87
97
88−instance FromJSON ServiceAccount where
89− parseJSON = withObject "service_account" $ \o -> do
90− bs <- Text.encodeUtf8 <$> o .: "private_key"
91− k <- case listToMaybe (readKeyFileFromMemory bs) of
92− Just (PrivKeyRSA k) -> pure k
93− _ ->
94− fail "Unable to parse key contents from \"private_key\""
95− ServiceAccount
96− <$> o .: "client_id"
97− <*> o .: "client_email"
98− <*> o .: "private_key_id"
99− <*> pure k
100
98
99+-- | Obtain an 'OAuthToken' from 'accountsURL' by signing and
100+-- sending a JSON Web Token (JWT) using the supplied 'ServiceAccount'.
101serviceAccountToken :: (MonadIO m, MonadCatch m)
101serviceAccountToken :: (MonadIO m, MonadCatch m)
102 => ServiceAccount
102 => ServiceAccount
103 -> [OAuthScope]
103 -> [OAuthScope]
@@ -114,18 +114,8 @@ serviceAccountToken s ss l m = do
114 }
114 }
115 refreshRequest rq l m
115 refreshRequest rq l m
116
116
117−metadataToken :: (MonadIO m, MonadCatch m)
117+-- | Encode the supplied 'ServiceAccount's key id, email, and scopes using the
118− => ServiceId
118+-- private key in the JSON Web Token (JWT) format.
119− -> Logger
120− -> Manager
121− -> m OAuthToken
122−metadataToken s = refreshRequest $
123− metadataRequest
124− { Client.path = "instance/service-accounts/"
125− <> Text.encodeUtf8 (toText s)
126− <> "/token"
127− }
128−
129encodeBearerJWT :: (MonadIO m, MonadThrow m)
119encodeBearerJWT :: (MonadIO m, MonadThrow m)
130 => ServiceAccount
120 => ServiceAccount
131 -> [OAuthScope]
121 -> [OAuthScope]
@@ -159,6 +149,6 @@ encodeBearerJWT s ss = liftIO $ do
159 [ "aud" .= accountsURL
149 [ "aud" .= accountsURL
160 , "scope" .= concatScopes ss
150 , "scope" .= concatScopes ss
161 , "iat" .= n
151 , "iat" .= n
162− , "exp" .= (n + maxTokenLifetimeSeconds)
152+ , "exp" .= (n + seconds maxTokenLifetime)
163 , "iss" .= _serviceEmail s
153 , "iss" .= _serviceEmail s
164 ]
154 ]
gogol/src/Network/Google/Env.hs+2−2
@@ -112,11 +112,11 @@ timeout s = local (override (serviceTimeout ?~ s))
112--
112--
113-- Lenses from 'HasEnv' can be used to further configure the resulting 'Env'.
113-- Lenses from 'HasEnv' can be used to further configure the resulting 'Env'.
114--
114--
115−-- /See:/ 'newEnvWith', 'discover'.
115+-- /See:/ 'newEnvWith', 'getApplicationDefault'.
116newEnv :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Env
116newEnv :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Env
117newEnv ss = do
117newEnv ss = do
118 m <- liftIO (newManager tlsManagerSettings)
118 m <- liftIO (newManager tlsManagerSettings)
119− c <- discover ss m
119+ c <- getApplicationDefault ss m
120 newEnvWith c (\_ _ -> pure ()) m
120 newEnvWith c (\_ _ -> pure ()) m
121
121
122-- | Creates a new environment without logging, using the supplied 'Manager' and
122-- | Creates a new environment without logging, using the supplied 'Manager' and
gogol/src/Network/Google/Internal/Auth.hs+94
@@ -61,6 +61,100 @@ import System.Environment (lookupEnv)
61import System.FilePath ((</>))
61import System.FilePath ((</>))
62import System.Info (os)
62import System.Info (os)
63
63
64+-- | The supported credential mechanisms.
65+data Credentials
66+ = FromMetadata !ServiceId
67+ -- ^ Obtain and refresh access tokens from the underlying GCE host metadata
68+ -- at @http:\/\/169.254.169.254@.
69+
70+ | FromClient !OAuthClient !OAuthCode
71+ -- ^ Obtain and refresh access tokens using the specified client secret
72+ -- and authorization code obtained from.
73+ --
74+ -- See the <https://developers.google.com/accounts/docs/OAuth2InstalledApp OAuth2 Installed Application>
75+ -- documentation for more information.
76+
77+ | FromAccount !ServiceAccount ![OAuthScope]
78+ -- ^ Use the specified @service_account@ and scopes to sign and request
79+ -- an access token. The 'ServiceAccount' will also be used for subsequent
80+ -- token refreshes.
81+ --
82+ -- A 'ServiceAccount' is typically generated through the
83+ -- Google Developer Console.
84+
85+ | FromUser !AuthorizedUser
86+ -- ^ Use the specified @authorized_user@ to obtain and refresh access tokens.
87+ --
88+ -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
89+ -- of the Google CloudSDK Tools.
90+
91+{-| Service Account credentials which are typically generated/download
92+from the Google Developer console of the following form:
93+
94+@
95+{
96+ \"type\": \"service_account\",
97+ \"private_key_id\": \"303ad77e5efdf2ce952DFa\",
98+ \"private_key\": \"-----BEGIN PRIVATE KEY-----\n...\n\",
99+ \"client_email\": \"email@serviceaccount.com\",
100+ \"client_id\": \"035-2-310.useraccount.com\"
101+}
102+@
103+
104+The private key is used to sign a JSON Web Token (JWT) of the
105+grant_type @urn:ietf:params:oauth:grant-type:jwt-bearer@, which is sent to
106+'accountsURL' to obtain a valid 'OAuthToken'. This process requires explicitly
107+specifying which 'Scope's the resulting 'OAuthToken' is authorized to access.
108+
109+/See:/ <https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority Delegating authority to your service account>.
110+-}
111+data ServiceAccount = ServiceAccount
112+ { _serviceId :: !ClientId
113+ , _serviceEmail :: !Text
114+ , _serviceKeyId :: !Text
115+ , _servicePrivateKey :: !PrivateKey
116+ } deriving (Eq, Show)
117+
118+instance FromJSON ServiceAccount where
119+ parseJSON = withObject "service_account" $ \o -> do
120+ bs <- Text.encodeUtf8 <$> o .: "private_key"
121+ k <- case listToMaybe (readKeyFileFromMemory bs) of
122+ Just (PrivKeyRSA k) -> pure k
123+ _ ->
124+ fail "Unable to parse key contents from \"private_key\""
125+ ServiceAccount
126+ <$> o .: "client_id"
127+ <*> o .: "client_email"
128+ <*> o .: "private_key_id"
129+ <*> pure k
130+
131+{-| Authorized User credentials which are typically generated by the Cloud SDK
132+Tools such as @gcloud init@, of the following form:
133+
134+@
135+{
136+ \"type\": \"authorized_user\",
137+ \"client_id\": \"32555940559.apps.googleusercontent.com\",
138+ \"client_secret\": \"Zms2qjJy2998hD4CTg2ejr2\",
139+ \"refresh_token\": \"1/B3gM1x35v.VtqffS1n5w-rSJ\"
140+}
141+@
142+
143+The secret and refresh token are used to obtain a valid 'OAuthToken' from
144+'accountsURL' using grant_type @refresh_token@.
145+-}
146+data AuthorizedUser = AuthorizedUser
147+ { _userId :: !ClientId
148+ , _userRefresh :: !RefreshToken
149+ , _userSecret :: !Secret
150+ } deriving (Eq, Show)
151+
152+instance FromJSON AuthorizedUser where
153+ parseJSON = withObject "authorized_user" $ \o -> AuthorizedUser
154+ <$> o .: "client_id"
155+ <*> o .: "refresh_token"
156+ <*> o .: "client_secret"
157+
64-- | A client identifier and accompanying secret used to obtain/refresh a token.
158-- | A client identifier and accompanying secret used to obtain/refresh a token.
65data OAuthClient = OAuthClient
159data OAuthClient = OAuthClient
66 { _clientId :: !ClientId
160 { _clientId :: !ClientId