on develop help sign in
Tweaking various supported credentials layout
commitbe47a9840c8b83af599b33888ec5dd9de984cda5 authorBrendan Hay <brendan.g.hay@gmail.com> dateWed 28 Oct 2015, 18:16 (11 years ago) parent5d17e0e
8 files changed+286−295
gogol/src/Network/Google.hs+5 gogol/src/Network/Google/Auth.hs+13−116 gogol/src/Network/Google/Auth/ApplicationDefaultCredentials.hs+115−75 gogol/src/Network/Google/Auth/Device.hs−45 gogol/src/Network/Google/Auth/InstalledApplication.hs+16−6 gogol/src/Network/Google/Auth/ServiceAccount.hs+41−51 gogol/src/Network/Google/Env.hs+2−2 gogol/src/Network/Google/Internal/Auth.hs+94
gogol/src/Network/Google.hs+5
@@ -77,6 +77,10 @@ module Network.Google
7777 -- ** Constructing a Logger
7878 , newLogger
7979
80+ -- * Constructing a HTTP Manager
81+ , newManager
82+ , tlsManagerSettings
83+
8084 -- * Re-exported Types
8185 , Proxy (..)
8286 , OctetStream
@@ -110,6 +114,7 @@ import Network.Google.Internal.HTTP
110114import Network.Google.Internal.Logger
111115import Network.Google.Prelude
112116import Network.Google.Types
117+import Network.HTTP.Conduit (newManager, tlsManagerSettings)
113118
114119newtype Google a = Google { unGoogle :: ReaderT Env (ResourceT IO) a }
115120 deriving
gogol/src/Network/Google/Auth.hs+13−116
@@ -22,22 +22,22 @@ module Network.Google.Auth
2222 -- * Credentials
2323 Credentials (..)
2424
25− -- ** Discovery
26− , discover
27− , fromFile
25+ -- ** Application Default Credentials
26+ , getApplicationDefault
27+ , fromWellKnownPath
2828 , fromFilePath
29− , fromJSONCredentials
29+
30+ -- ** Installed Application Credentials
31+ , formURL
3032
3133 -- ** Authorizing Requests
3234 , authorize
3335
34− -- ** Constructing Callback URLs
35− , formURL
36−
3736 -- ** Thread-safe Storage
38− , Auth (..)
3937 , Store
4038 , initStore
39+
40+ , Auth (..)
4141 , exchange
4242 , refresh
4343
@@ -113,114 +113,6 @@ import System.Environment (lookupEnv)
113113import System.FilePath ((</>))
114114import System.Info (os)
115115
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−
224116-- | An 'OAuthToken' that can potentially be expired, with the original
225117-- credentials that can be used to perform a refresh.
226118data Auth = Auth
@@ -230,6 +122,11 @@ data Auth = Auth
230122
231123-- | Check if the given token is still valid, ie. younger than the projected
232124-- 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.
233130validate :: MonadIO m => Auth -> m Bool
234131validate a = (< _tokenExpiry (_token a)) <$> liftIO getCurrentTime
235132
gogol/src/Network/Google/Auth/ApplicationDefaultCredentials.hs+115−75
@@ -14,116 +14,156 @@ module Network.Google.Auth.ApplicationDefaultCredentials where
1414import Control.Applicative
1515import Control.Concurrent
1616import Control.Exception.Lens
17−import Control.Lens hiding (coerce, (.=))
17+import Control.Lens hiding (coerce, (.=))
1818import Control.Monad
1919import Control.Monad.Catch
2020import Control.Monad.IO.Class
21−import Crypto.Hash.Algorithms (SHA256 (..))
22−import Crypto.PubKey.RSA.PKCS15 (signSafer)
23−import Crypto.PubKey.RSA.Types (PrivateKey)
21+import Crypto.Hash.Algorithms (SHA256 (..))
22+import Crypto.PubKey.RSA.PKCS15 (signSafer)
23+import Crypto.PubKey.RSA.Types (PrivateKey)
2424import Data.Aeson
2525import Data.Aeson.Types
2626import Data.ByteArray
2727import Data.ByteArray.Encoding
28−import Data.ByteString (ByteString)
29−import Data.ByteString.Builder (Builder)
30−import qualified Data.ByteString.Builder as Build
31−import qualified Data.ByteString.Char8 as BS8
32−import qualified Data.ByteString.Lazy as LBS
28+import Data.ByteString (ByteString)
29+import Data.ByteString.Builder (Builder)
30+import qualified Data.ByteString.Builder as Build
31+import qualified Data.ByteString.Char8 as BS8
32+import qualified Data.ByteString.Lazy as LBS
3333import Data.Coerce
34−import Data.Default.Class (def)
35−import Data.List (intersperse)
34+import Data.Default.Class (def)
35+import Data.List (intersperse)
3636import Data.String
37−import qualified Data.Text as Text
38−import qualified Data.Text.Encoding as Text
39−import qualified Data.Text.Lazy as LText
40−import qualified Data.Text.Lazy.Builder as TBuild
41−import qualified Data.Text.Lazy.Encoding as LText
37+import qualified Data.Text as Text
38+import qualified Data.Text.Encoding as Text
39+import qualified Data.Text.Lazy as LText
40+import qualified Data.Text.Lazy.Builder as TBuild
41+import qualified Data.Text.Lazy.Encoding as LText
4242import Data.Time
4343import Data.Time.Clock.POSIX
4444import Data.Typeable
4545import Data.X509
4646import Data.X509.Memory
47+import Network.Google.Auth.ServiceAccount
4748import Network.Google.Compute.Metadata
4849import Network.Google.Internal.Auth
4950import Network.Google.Internal.Logger
50−import Network.Google.Prelude hiding (buildText)
51−import Network.HTTP.Conduit hiding (Request)
52−import qualified Network.HTTP.Conduit as Client
51+import Network.Google.Prelude hiding (buildText)
52+import Network.HTTP.Conduit hiding (Request)
53+import qualified Network.HTTP.Conduit as Client
5354import Network.HTTP.Types
54−import System.Directory (doesFileExist,
55− getHomeDirectory)
56−import System.Environment (lookupEnv)
57−import System.FilePath ((</>))
58−import System.Info (os)
55+import System.Directory (doesFileExist,
56+ getHomeDirectory)
57+import System.Environment (lookupEnv)
58+import System.FilePath ((</>))
59+import System.Info (os)
5960
6061-- | The environment variable name which is used to specify the directory
6162-- containing the @application_default_credentials.json@ generated by @gcloud init@.
6263cloudSDKConfigDir :: String
6364cloudSDKConfigDir = "CLOUDSDK_CONFIG"
6465
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+
6577-- | The environment variable pointing the file with local
6678-- Application Default Credentials.
6779defaultCredentialsFile :: String
6880defaultCredentialsFile = "GOOGLE_APPLICATION_CREDENTIALS"
6981
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−
11282-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
11383-- default application credentials filepath.
11484defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
11585defaultCredentialsPath = liftIO (lookupEnv defaultCredentialsFile)
11686
117−-- | Return the filepath to the Cloud SDK well known file location such as
118−-- @~\/.config\/gcloud\/application_default_credentials.json@.
119−cloudSDKConfigPath :: MonadIO m => m FilePath
120−cloudSDKConfigPath = do
121− m <- liftIO (lookupEnv cloudSDKConfigDir)
122− case m of
123− Just d -> pure $! d </> "application_default_credentials.json"
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+--
92+-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
93+-- The @~/.config@ component of the path can be overriden by the environment
94+-- variable @CLOUDSDK_CONFIG@ if it exists.
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
127+ case f of
128+ Just x -> fromFilePath ss x
124129 Nothing -> do
125− d <- getConfigDirectory
126− pure $! d </> "gcloud/application_default_credentials.json"
130+ x <- cloudSDKConfigPath
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
127167
128168getConfigDirectory :: MonadIO m => m FilePath
129169getConfigDirectory = do
gogol/src/Network/Google/Auth/Device.hs−45
@@ -9,48 +9,3 @@
99-- Portability : non-portable (GHC extensions)
1010
1111module 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)
5050import qualified Network.HTTP.Conduit as Client
5151import Network.HTTP.Types
5252
53−-- | Given a client identifier, client secret, and a list of scopes to authorize,
53+-- | @urn:ietf:wg:oauth:2.0:oob@.
54+redirectURI :: Text
55+redirectURI = "urn:ietf:wg:oauth:2.0:oob"
56+
57+-- | Given an 'OAuthClient' and a list of scopes to authorize,
5458-- 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>.
5663formURL :: OAuthClient -> [OAuthScope] -> Text
5764formURL c ss = accountsURL
5865 <> "?response_type=code"
@@ -60,10 +67,10 @@ formURL c ss = accountsURL
6067 <> "&redirect_uri=" <> redirectURI
6168 <> "&scope=" <> Text.decodeUtf8 (queryEncodeScopes ss)
6269
63−-- | @urn:ietf:wg:oauth:2.0:oob@.
64−redirectURI :: Text
65−redirectURI = "urn:ietf:wg:oauth:2.0:oob"
66−
70+-- | Exchange your 'OAuthClient' details and the 'OAuthCode' you received from
71+-- using 'formURL' with a valid 'OAuthToken'.
72+--
73+-- /See:/ <https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse Exchanging the code>.
6774exchangeCode :: (MonadIO m, MonadCatch m)
6875 => OAuthClient
6976 -> OAuthCode
@@ -80,6 +87,9 @@ exchangeCode c n = refreshRequest $
8087 <> "&redirect_uri=" <> redirectURI
8188 }
8289
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>.
8393refreshToken :: (MonadIO m, MonadCatch m)
8494 => OAuthClient
8595 -> OAuthToken
gogol/src/Network/Google/Auth/ServiceAccount.hs+41−51
@@ -56,48 +56,48 @@ import System.Environment (lookupEnv)
5656import System.FilePath ((</>))
5757import System.Info (os)
5858
59−-- | 1 hour in seconds.
60−maxTokenLifetimeSeconds :: Int
61−maxTokenLifetimeSeconds = 3600
59+-- | The maximum lifetime of a @service_account@ token which is 1 hour, in seconds.
60+maxTokenLifetime :: Seconds
61+maxTokenLifetime = 3600
6262
63−{-| Service Account credentials which are typically generated/download
64−from the Google Developer console of the following form:
65−
66−@
67−{
68− \"type\": \"service_account\",
69− \"private_key_id\": \"303ad77e5efdf2ce952DFa\",
70− \"private_key\": \"-----BEGIN PRIVATE KEY-----\n...\n\",
71− \"client_email\": \"email@serviceaccount.com\",
72− \"client_id\": \"035-2-310.useraccount.com\"
73−}
74−@
63+-- | Obtain an 'OAuthToken' from the local instace metadata
64+-- using the specific 'ServiceId'.
65+--
66+-- For example: @http:\/\/169.254.169.254\/instance\/service-accounts\/default\/token@
67+-- will be retrieved if the given 'ServiceId' is @\"default\"@.
68+metadataToken :: (MonadIO m, MonadCatch m)
69+ => ServiceId
70+ -> Logger
71+ -> Manager
72+ -> m OAuthToken
73+metadataToken s = refreshRequest $
74+ metadataRequest
75+ { Client.path = "instance/service-accounts/"
76+ <> Text.encodeUtf8 (toText s)
77+ <> "/token"
78+ }
7579
76−The private key is used to sign a JSON Web Token (JWT) of the
77−grant_type @urn:ietf:params:oauth:grant-type:jwt-bearer@, which is sent to
78−'accountsURL' to obtain a valid 'OAuthToken'. This process requires explicitly
79−specifying which 'Scope's the resulting 'OAuthToken' is authorized to access.
80−-}
81−data ServiceAccount = ServiceAccount
82− { _serviceId :: !ClientId
83− , _serviceEmail :: !Text
84− , _serviceKeyId :: !Text
85− , _servicePrivateKey :: !PrivateKey
86− } deriving (Eq, Show)
80+-- | Use the 'AuthorizedUser' to obtain a new 'OAuthToken'. If the supplied
81+-- 'RefreshToken' is 'Nothing', the original 'RefreshToken' from he user will
82+-- be used.
83+authorizedUserToken :: (MonadIO m, MonadCatch m)
84+ => AuthorizedUser
85+ -> Maybe RefreshToken
86+ -> Logger
87+ -> Manager
88+ -> m OAuthToken
89+authorizedUserToken u r = refreshRequest $
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+ }
8797
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
10098
99+-- | Obtain an 'OAuthToken' from 'accountsURL' by signing and
100+-- sending a JSON Web Token (JWT) using the supplied 'ServiceAccount'.
101101serviceAccountToken :: (MonadIO m, MonadCatch m)
102102 => ServiceAccount
103103 -> [OAuthScope]
@@ -114,18 +114,8 @@ serviceAccountToken s ss l m = do
114114 }
115115 refreshRequest rq l m
116116
117−metadataToken :: (MonadIO m, MonadCatch m)
118− => ServiceId
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−
117+-- | Encode the supplied 'ServiceAccount's key id, email, and scopes using the
118+-- private key in the JSON Web Token (JWT) format.
129119encodeBearerJWT :: (MonadIO m, MonadThrow m)
130120 => ServiceAccount
131121 -> [OAuthScope]
@@ -159,6 +149,6 @@ encodeBearerJWT s ss = liftIO $ do
159149 [ "aud" .= accountsURL
160150 , "scope" .= concatScopes ss
161151 , "iat" .= n
162− , "exp" .= (n + maxTokenLifetimeSeconds)
152+ , "exp" .= (n + seconds maxTokenLifetime)
163153 , "iss" .= _serviceEmail s
164154 ]
gogol/src/Network/Google/Env.hs+2−2
@@ -112,11 +112,11 @@ timeout s = local (override (serviceTimeout ?~ s))
112112--
113113-- Lenses from 'HasEnv' can be used to further configure the resulting 'Env'.
114114--
115−-- /See:/ 'newEnvWith', 'discover'.
115+-- /See:/ 'newEnvWith', 'getApplicationDefault'.
116116newEnv :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Env
117117newEnv ss = do
118118 m <- liftIO (newManager tlsManagerSettings)
119− c <- discover ss m
119+ c <- getApplicationDefault ss m
120120 newEnvWith c (\_ _ -> pure ()) m
121121
122122-- | 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)
6161import System.FilePath ((</>))
6262import System.Info (os)
6363
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+
64158-- | A client identifier and accompanying secret used to obtain/refresh a token.
65159data OAuthClient = OAuthClient
66160 { _clientId :: !ClientId