@@ -1,5 +1,6 @@
1{-# LANGUAGE BangPatterns #-}
1{-# LANGUAGE BangPatterns #-}
2{-# LANGUAGE DeriveDataTypeable #-}
2{-# LANGUAGE DeriveDataTypeable #-}
3+{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE FlexibleContexts #-}
4{-# LANGUAGE FlexibleContexts #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE GADTs #-}
6{-# LANGUAGE GADTs #-}
@@ -18,57 +19,61 @@
19-- Explicitly specify your Google credentials, or retrieve them
20-- Explicitly specify your Google credentials, or retrieve them
20-- from the underlying OS.
21-- from the underlying OS.
21−module Network.Google.Auth
22+module Network.Google.Auth where
23− -- * Authentication
24+ -- -- * Authentication
24− -- ** Retrieving Authentication
25+ -- -- ** Retrieving Authentication
25− getAuth
26+ -- getAuth
26− , Credentials (..)
27+ -- , Credentials (..)
29− -- ** Authorising Requests
30+ -- -- ** Authorising Requests
30− , authorise
31+ -- , authorize
32− -- ** Default Constants
33+ -- -- ** Default Constants
33− -- *** Google Compute Engine
34+ -- -- *** Google Compute Engine
34− , checkGCEVar
35+ -- , checkGCEVar
36− -- *** Cloud SDK
37+ -- -- *** Cloud SDK
37− , cloudSDKConfigDir
38+ -- , cloudSDKConfigDir
38− , cloudSDKConfigPath
39+ -- , cloudSDKConfigPath
40− -- *** Application Default Credentials
41+ -- -- *** Application Default Credentials
41− , defaultCredentialsFile
42+ -- , defaultCredentialsFile
42− , defaultCredentialsPath
43+ -- , defaultCredentialsPath
44− -- ** Credentials
45+ -- -- ** Credential Styles
45− , fromMetadata
46+ -- , fromMetadata
46− , fromFile
47+ -- , fromFile
47− , fromFilePath
48+ -- , fromFilePath
49− -- *** Authorised User
50+ -- -- *** OAuth Token
50− , AuthorisedUser (..)
51+ -- , Bearer (..)
51− , fromAuthorisedUser
52+ -- , fromToken
54+ -- -- *** Authorized User
55+ -- , AuthorizedUser (..)
56+ -- , fromAuthorizedUser
53− -- *** Service Account
58+ -- -- *** Service Account
54− , ServiceAccount (..)
59+ -- , ServiceAccount (..)
55− , fromServiceAccount
60+ -- , fromServiceAccount
57− -- ** Handling Errors
62+ -- -- ** Handling Errors
58− , AsAuthError (..)
63+ -- , AsAuthError (..)
59− , AuthError (..)
64+ -- , AuthError (..)
61− -- ** Re-exported Types
66+ -- -- ** Re-exported Types
62− , OAuthScope (..)
67+ -- , Scope (..)
63− , OAuthToken (..)
68+ -- , OAuthToken (..)
64− , ServiceId (..)
69+ -- , ServiceId (..)
65− , ClientId (..)
70+ -- , ClientId (..)
66− ) where
71+ -- ) where
68import Control.Applicative
73import Control.Applicative
69import Control.Concurrent
74import Control.Concurrent
70import Control.Exception.Lens
75import Control.Exception.Lens
71−import Control.Lens hiding ((.=))
76+import Control.Lens hiding (coerce, (.=))
72import Control.Monad
77import Control.Monad
73import Control.Monad.Catch
78import Control.Monad.Catch
74import Control.Monad.IO.Class
79import Control.Monad.IO.Class
@@ -80,11 +85,19 @@ import Data.Aeson.Types
80import Data.ByteArray
85import Data.ByteArray
81import Data.ByteArray.Encoding
86import Data.ByteArray.Encoding
82import Data.ByteString (ByteString)
87import Data.ByteString (ByteString)
88+import Data.ByteString.Builder (Builder)
89+import qualified Data.ByteString.Builder as Build
83import qualified Data.ByteString.Char8 as BS8
90import qualified Data.ByteString.Char8 as BS8
84import qualified Data.ByteString.Lazy as LBS
91import qualified Data.ByteString.Lazy as LBS
85import Data.Default.Class (def)
93import Data.Default.Class (def)
94+import Data.List (intersperse)
86import qualified Data.Text as Text
96import qualified Data.Text as Text
87import qualified Data.Text.Encoding as Text
97import qualified Data.Text.Encoding as Text
98+import qualified Data.Text.Lazy as LText
99+import qualified Data.Text.Lazy.Builder as TBuild
100+import qualified Data.Text.Lazy.Encoding as LText
88import Data.Time
101import Data.Time
89import Data.Time.Clock.POSIX
102import Data.Time.Clock.POSIX
90import Data.Typeable
103import Data.Typeable
@@ -92,7 +105,7 @@ import Data.X509
92import Data.X509.Memory
105import Data.X509.Memory
93import Network.Google.Compute.Metadata
106import Network.Google.Compute.Metadata
94import Network.Google.Internal.Logger
107import Network.Google.Internal.Logger
95−import Network.Google.Prelude
108+import Network.Google.Prelude hiding (buildText)
96import Network.HTTP.Conduit hiding (Request)
109import Network.HTTP.Conduit hiding (Request)
97import qualified Network.HTTP.Conduit as Client
110import qualified Network.HTTP.Conduit as Client
98import Network.HTTP.Types
111import Network.HTTP.Types
@@ -177,135 +190,6 @@ instance AsAuthError AuthError where
177 TokenRefreshError s e d -> Right (s, e, d)
190 TokenRefreshError s e d -> Right (s, e, d)
178 x -> Left x)
191 x -> Left x)
180−-- | Determines how AuthN/AuthZ information is retrieved.
182− = FromToken !OAuthToken
183− -- ^ Supply an explicit access token. See 'fromToken'.
185− | FromFile !FilePath
186− -- ^ Load the Application Default Credentials from a specific file path.
187− -- The file can be formatted as either a @service_account@ or an @authorized_user@.
189− | FromAccount !ServiceId
190− -- ^ Retrieve the Application Default Credentials using the speicfic
191− -- 'ServiceId' from the local metadata endpoint.
194− -- ^ Attempt credentials discovery via the following steps:
196− -- * Read the default credentials from a file specified by
197− -- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
199− -- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
200− -- The @~/.config@ component of the path can be overriden by the environment
201− -- variable @CLOUDSDK_CONFIG@ if it exists.
203− -- * Retrieve the default service account application credentials if
207−-- | Retrieve authentication information via the specified 'Credentials' mechanism.
209−-- The specified 'OAuthScope's are used to authorise any @service_account@
210−-- with the appropriate scopes. See the top-level module of each individual
211−-- @gogol-*@ library for a list of available scopes, such as @Network.Google.Compute.computeScope@.
213−-- Throws 'AuthError' when environment variables or service account information
214−-- cannot be read, and credentials files are invalid or cannot be found.
215−getAuth :: (MonadIO m, MonadCatch m)
221−getAuth c ss l m = case c of
222− FromToken t -> pure (AuthToken t)
223− FromFile f -> fromFilePath ss f l m
224− FromAccount s -> fromMetadata s l m
226− catching _MissingFileError (fromFile ss l m) $ \f -> do
229− throwingM _MissingFileError f
230− fromMetadata "default" l m
232−-- | Refresh a token from the local GCE metadata endpoint for the specified
234−fromMetadata :: (MonadIO m, MonadCatch m)
239−fromMetadata s l = refresh s l >=> fmap AuthMeta . liftIO . newMVar
241−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
242−-- file to obtain the credentials neccessary to perform a token refresh.
243−fromFile :: (MonadIO m, MonadCatch m) => [OAuthScope] -> Logger -> Manager -> m Auth
245− f <- defaultCredentialsPath
247− Just x -> fromFilePath ss x l m
249− x <- cloudSDKConfigPath
250− fromFilePath ss x l m
252−-- | An internal type inhabited by the supported JSON formats
253−-- for Application Default Credentials.
255− = ParsedService !ServiceAccount
256− | ParsedUser !AuthorisedUser
258−-- The following instance accumulates all error messages if all parsers fail.
259−-- If there is a success, the entire parse succeeds with the single
261−instance FromJSON AccountJSON where
263− let x = ParsedService <$> parseEither parseJSON o
264− y = ParsedUser <$> parseEither parseJSON o
265− in either fail pure $
267− (Left e, Left e') -> Left $
268− "Failed parsing service_account: " ++ e ++
269− ", Failed parsing authorized_user: " ++ e'
272−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
273−-- file to obtain the credentials neccessary to perform a token refresh from
274−-- the specified file.
275−fromFilePath :: (MonadIO m, MonadCatch m)
281−fromFilePath ss f l m = do
282− logDebug l $ "Trying to read credentials from " <> build f
283− p <- liftIO (doesFileExist f)
285− throwM (MissingFileError f)
286− e <- liftIO (LBS.readFile f) >>=
287− either (throwM . InvalidFileError f) pure . parseLBS
289− ParsedUser u -> fromAuthorisedUser u l m
290− ParsedService a -> fromServiceAccount (a { _serviceScope = ss }) l m
292−fromAuthorisedUser :: (MonadIO m, MonadCatch m)
297−fromAuthorisedUser u l m =
298− refresh u l m >>= fmap AuthUser . liftIO . newMVar
300−fromServiceAccount :: (MonadIO m, MonadCatch m)
305−fromServiceAccount s l m =
306− -- Add a check to ensure scopes aren't empty.
307− refresh s l m >>= fmap AuthSign . liftIO . newMVar
309-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
193-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
310-- default application credentials filepath.
194-- default application credentials filepath.
311defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
195defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
@@ -339,160 +223,425 @@ instance FromJSON RefreshError where
339 <$> o .: "error"
223 <$> o .: "error"
340 <*> o .:? "error_description"
224 <*> o .:? "error_description"
342−data Expires a = Expires
226+{-| Service Account credentials which are typically generated/download
343− { _expiry :: !UTCTime
227+from the Google Developer console of the following form:
344− , _token :: !OAuthToken
228+
345− , _details :: !a
229+@
231+ "type": "service_account",
232+ "private_key_id": "303ad77e5efdf2ce952DFa",
233+ "private_key": "-----BEGIN PRIVATE KEY-----\n...\n",
234+ "client_email": "email@serviceaccount.com",
235+ "client_id": "035-2-310.useraccount.com"
239+The private key is used to sign a JSON Web Token (JWT) of the
240+grant_type @urn:ietf:params:oauth:grant-type:jwt-bearer@, which is sent to
241+'accountsURL' to obtain a valid 'OAuthToken'. This process requires explicitly
242+specifying which 'Scope's the resulting 'OAuthToken' is authorized to access.
244+A 'ServiceAccount' is typically generated through the
245+Google Developer Console.
247+data ServiceAccount = ServiceAccount
248+ { _serviceId :: !ClientId
249+ , _serviceEmail :: !Text
250+ , _serviceKeyId :: !Text
251+ , _servicePrivateKey :: !PrivateKey
252+ } deriving (Eq, Show)
348−isValid :: MonadIO m => Expires a -> m Bool
254+instance FromJSON ServiceAccount where
349−isValid r = (< _expiry r) <$> liftIO getCurrentTime
255+ parseJSON = withObject "service_account" $ \o -> do
256+ bs <- Text.encodeUtf8 <$> o .: "private_key"
257+ k <- case listToMaybe (readKeyFileFromMemory bs) of
258+ Just (PrivKeyRSA k) -> pure k
260+ fail "Unable to parse key contents from \"private_key\""
262+ <$> o .: "client_id"
263+ <*> o .: "client_email"
264+ <*> o .: "private_key_id"
351−data Auth
267+{-| Authorized User credentials which are typically generated by the Cloud SDK
352− = AuthToken !OAuthToken
268+tools such as @gcloud init@, of the following form:
353− | AuthSign !(MVar (Expires ServiceAccount))
354− | AuthMeta !(MVar (Expires ServiceId))
355− | AuthUser !(MVar (Expires AuthorisedUser))
357−authorise :: (MonadIO m, MonadCatch m)
270+{
358− => Logger
271+ "type": "authorized_user",
359− -> Manager
272+ "client_id": "32555940559.apps.googleusercontent.com",
360− -> Client.Request
273+ "client_secret": "Zms2qjJy2998hD4CTg2ejr2",
361− -> Auth
274+ "refresh_token": "1/B3gM1x35v.VtqffS1n5w-rSJ"
363−authorise l m rq = \case
364− AuthToken t -> pure $! authoriseBearer rq t
365− AuthSign r -> authoriseToken l m r rq
366− AuthMeta r -> authoriseToken l m r rq
367− AuthUser r -> authoriseToken l m r rq
369−authoriseBearer :: Client.Request -> OAuthToken -> Client.Request
370−authoriseBearer rq t = rq
371− { Client.requestHeaders = (hAuthorization, "Bearer " <> tokenToBS t)
372− : Client.requestHeaders rq
375−authoriseToken :: (MonadIO m, MonadCatch m, Refresh a)
277+The secret and refresh token are used to obtain a valid 'OAuthToken' from
376− => Logger
278+'accountsURL' using grant_type @refresh_token@.
381−authoriseToken l m r rq = authoriseBearer rq <$> refreshToken l m r
383−refreshToken :: (MonadIO m, MonadCatch m, Refresh a)
280+An 'AuthorizedUser' is typically generated by the @gcloud init@ command
384− => Logger
281+of the Google CloudSDK Tools.
386− -> MVar (Expires a)
283+data AuthorizedUser = AuthorizedUser
387− -> m OAuthToken
284+ { _userId :: !ClientId
388−refreshToken l m r = do
285+ , _userRefresh :: !RefreshToken
389− x <- liftIO (readMVar r)
286+ , _userSecret :: !Secret
390− xv <- isValid x
287+ } deriving (Eq, Show)
392− then pure (_token x)
289+instance FromJSON AuthorizedUser where
393− else liftIO . modifyMVar r $ \y -> do
290+ parseJSON = withObject "authorized_user" $ \o -> AuthorizedUser
394− yv <- isValid y
291+ <$> o .: "client_id"
396− then pure (y, _token y)
398− z <- refresh (_details y) l m
402−-- "access_token": "sadsdasd",
403−-- "expires_in": 3600,
404−-- "id_token": "eyJhbGciOiJSd",
405−-- "refresh_token": "1/B3gq9K",
292+ <*> o .: "refresh_token"
406−-- "token_type": "Bearer"
293+ <*> o .: "client_secret"
410− { _bearerAccess :: !OAuthToken
411− , _bearerRefresh :: !(Maybe Text)
412− , _bearerExpiry :: !UTCTime
295+-- | A client identifier and accompanying secret used to obtain/refresh a token.
296+data OAuthClient = OAuthClient
297+ { _clientId :: !ClientId
298+ , _clientSecret :: !Secret
299+ } deriving (Eq, Show)
301+{-| An OAuth bearer type token of the following form:
305+ "token_type": "Bearer",
306+ "access_token": "eyJhbGci...",
307+ "refresh_token": "1/B3gq9K...",
313+The '_tokenAccess' field will be inserted verbatim into the
314+@Authorization: Bearer ...@ header for all HTTP requests.
316+data OAuthToken = OAuthToken
317+ { _tokenAccess :: !AccessToken
318+ , _tokenRefresh :: !(Maybe RefreshToken)
319+ , _tokenExpiry :: !UTCTime
320+ } deriving (Eq, Show)
415−instance FromJSON (UTCTime -> Bearer) where
322+instance FromJSON (UTCTime -> OAuthToken) where
416 parseJSON = withObject "bearer" $ \o -> do
323 parseJSON = withObject "bearer" $ \o -> do
417 t <- o .: "access_token"
324 t <- o .: "access_token"
418 r <- o .:? "refresh_token"
325 r <- o .:? "refresh_token"
419 e <- o .: "expires_in" <&> fromInteger
326 e <- o .: "expires_in" <&> fromInteger
420− pure (Bearer t r . addUTCTime e)
327+ pure (OAuthToken t r . addUTCTime e)
422−-- {
329+newtype OAuthCode = OAuthCode { codeToText :: Text }
423−-- "private_key_id": "303ad77e5efdf2ce952DFa",
330+ deriving (Eq, Ord, Show, Read, IsString, Generic, Typeable, ToText, FromJSON, ToJSON)
424−-- "private_key": "-----BEGIN PRIVATE KEY-----\nMIIE...\n",
425−-- "client_email": "01395191@gserviceaccount.com",
426−-- "client_id": "035-2-310eusercontent.com",
427−-- "type": "service_account"
430−data ServiceAccount = ServiceAccount
332+-- | The supported credential mechanisms.
431− { _serviceId :: !ClientId
333+data Credentials
432− , _serviceEmail :: !Text
334+ = FromMetadata !ServiceId
433− , _serviceScope :: ![OAuthScope]
335+ -- ^ Obtain and refresh access tokens from the underlying GCE host metadata
434− , _serviceKeyId :: !Text
336+ -- at @http:\/\/169.254.169.254@.
435− , _serviceKey :: !PrivateKey
438−instance FromJSON ServiceAccount where
338+ | FromClient !OAuthClient !OAuthCode
439− parseJSON = withObject "service_account" $ \o -> do
339+ -- ^ Obtain and refresh access tokens using the specified client secret
440− bs <- Text.encodeUtf8 <$> o .: "private_key"
340+ -- and authorization code obtained from.
441− ServiceAccount
341+ --
442− <$> o .: "client_id"
342+ -- See the <https://developers.google.com/accounts/docs/OAuth2InstalledApp OAuth2 Installed Application>
443− <*> o .: "client_email"
343+ -- documentation for more information.
445− <*> o .: "private_key_id"
446− <*> case listToMaybe (readKeyFileFromMemory bs) of
447− Just (PrivKeyRSA k) -> pure k
449− fail "Unable to parse key contents from \"private_key\""
452−-- "client_id": "32555940559.apps.googleusercontent.com",
453−-- "client_secret": "ZmssLNjJy2998hD4CTg2ejr2",
454−-- "refresh_token": "1/B3gqKM1xzVtqffS1n5w-rSJ",
455−-- "type": "authorized_user"
458−data AuthorisedUser = AuthorisedUser
459− { _userId :: !ClientId
460− , _userSecret :: !Text
461− , _userRefresh :: !Text
464−instance FromJSON AuthorisedUser where
345+ | FromAccount !ServiceAccount ![OAuthScope]
465− parseJSON = withObject "authorized_user" $ \o -> AuthorisedUser
346+ -- ^ Use the specified @service_account@ and scopes to sign and request
466− <$> o .: "client_id"
347+ -- an access token. The 'ServiceAccount' will also be used for subsequent
467− <*> o .: "client_secret"
348+ -- token refreshes.
468− <*> o .: "refresh_token"
349+ --
350+ -- A 'ServiceAccount' is typically generated through the
351+ -- Google Developer Console.
353+ | FromUser !AuthorizedUser
354+ -- ^ Use the specified @authorized_user@ to obtain and refresh access tokens.
356+ -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
357+ -- of the Google CloudSDK Tools.
359+-- | A credentials value in one of two possible states, pre-exchange and
360+-- containing a valid, but possibly expired access token.
362+ = Exchange !Credentials
363+ -- ^ Signifies that the initial token refresh has not occured, and the
364+ -- 'Credentials' need to be signed and exchanged for a valid 'OAuthToken'.
366+ | Refresh !Credentials !OAuthToken
367+ -- ^ An 'OAuthToken' that can potentially be expired, with the original
368+ -- credentials that can be used to perform a refresh.
370+type Store = MVar Auth
372+emptyStore :: MonadIO m => Credentials -> m Store
373+emptyStore !c = liftIO . newMVar $ Exchange c
470−class Refresh a where
375+-- FIXME: support for generating the url using scopes to obtain the OAuthCode.
378+-- The parameter is include_granted_scopes and the allowed values are true and
379+-- false (the default is false). When the value is true, the effect is that if
380+-- your scope authorization request is granted, the Google authorization server
381+-- will roll this authorization together with all the previous authorizations
382+-- granted to the requesting user from the requesting app.
384+-- FIXME: Get around to replacing the threading of Logger, Manager through
385+-- every exchange/refresh call.
387+-- | Retrieve authentication information via the specified 'Credentials' mechanism.
390+-- Throws 'AuthError' when environment variables or service account information
391+-- cannot be read, and credentials files are invalid or cannot be found.
392+-- getAuth :: (MonadIO m, MonadCatch m)
398+-- getAuth c ss l m = case c of
399+-- FromToken t -> pure (AuthToken t)
400+-- FromFile f -> fromFilePath ss f l m
401+-- FromAccount s -> fromMetadata s l m
403+-- catching _MissingFileError (fromFile ss l m) $ \f -> do
406+-- throwingM _MissingFileError f
407+-- fromMetadata "default" l m
409+-- | Attempt credentials discovery via the following steps:
411+-- * Read the default credentials from a file specified by
412+-- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
414+-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
415+-- The @~/.config@ component of the path can be overriden by the environment
416+-- variable @CLOUDSDK_CONFIG@ if it exists.
418+-- * Retrieve the default service account application credentials if
421+-- The specified 'Scope's are used to authorize any @service_account@ that is
422+-- found with the appropriate scopes, otherwise they are not used. See the
423+-- top-level module of each individual @gogol-*@ library for a list of available
424+-- scopes, such as @Network.Google.Compute.computeScope@.
425+discover :: (MonadIO m, MonadCatch m)
430+ catching _MissingFileError (fromFile ss) $ \f -> do
433+ throwingM _MissingFileError f
434+ pure $! FromMetadata "default"
436+-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
437+-- file to obtain the credentials neccessary to perform a token refresh.
439+-- The specified 'Scope's are used to authorize any @service_account@ that is
440+-- found with the appropriate scopes, otherwise they are not used. See the
441+-- top-level module of each individual @gogol-*@ library for a list of available
442+-- scopes, such as @Network.Google.Compute.computeScope@.
444+-- /See:/ 'cloudSDKConfigPath', 'defaultCredentialsPath'.
471− refresh :: (MonadIO m, MonadCatch m)
445+fromFile :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Credentials
472− => a
446+fromFile ss = do
447+ f <- defaultCredentialsPath
449+ Just x -> fromFilePath ss x
451+ x <- cloudSDKConfigPath
454+-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
455+-- file to obtain the credentials neccessary to perform a token refresh from
456+-- the specified file.
458+-- The specified 'Scope's are used to authorize any @service_account@ that is
459+-- found with the appropriate scopes, otherwise they are not used. See the
460+-- top-level module of each individual @gogol-*@ library for a list of available
461+-- scopes, such as @Network.Google.Compute.computeScope@.
462+fromFilePath :: (MonadIO m, MonadCatch m)
466+fromFilePath ss f = do
467+ p <- liftIO (doesFileExist f)
469+ throwM (MissingFileError f)
470+ bs <- liftIO (LBS.readFile f)
471+ either (throwM . InvalidFileError f . Text.pack) pure
472+ (fromJSONCredentials ss bs)
474+fromJSONCredentials :: [OAuthScope]
476+ -> Either String Credentials
477+fromJSONCredentials ss bs = do
478+ v <- eitherDecode' bs
479+ let x = (`FromAccount` ss) <$> parseEither parseJSON v
480+ y = FromUser <$> parseEither parseJSON v
482+ (Left xe, Left ye) -> Left $
483+ "Failed parsing service_account: " ++ xe ++
484+ ", Failed parsing authorized_user: " ++ ye
487+authorize :: (MonadIO m, MonadCatch m)
473− -> Logger
490+ -> Logger
493+authorize rq s l m = bearer <$> getToken s l m
496+ { Client.requestHeaders =
498+ , "Bearer " <> Text.encodeUtf8 (accessToText (_tokenAccess t))
499+ ) : Client.requestHeaders rq
502+getToken :: (MonadIO m, MonadCatch m)
474− -> Manager
505+ -> Manager
475− -> m (Expires a)
506+ -> m OAuthToken
476−
507+getToken s l m = do
477−instance Refresh ServiceAccount where
508+ x <- liftIO (readMVar s)
478− refresh s l m = do
509+ mx <- validateToken x
479− b <- jwtEncode s
510+ case mx of
512+ Nothing -> liftIO . modifyMVar s $ \y -> do
513+ my <- validateToken y
515+ Just t -> pure (y, t)
520+ pure (Refresh c t, t)
522+ t' <- refresh c t l m
523+ pure (Refresh c t', t')
525+validateToken :: MonadIO m => Auth -> m (Maybe OAuthToken)
526+validateToken Exchange {} = pure Nothing
527+validateToken (Refresh _ t) = do
528+ ts <- liftIO getCurrentTime
529+ if ts < _tokenExpiry t
533+exchange :: (MonadIO m, MonadCatch m)
540+ FromMetadata s -> refreshMetadata s l m
541+ FromAccount a ss -> refreshAssertion a ss l m
542+ FromClient c' n -> exchangeCode c' n l m
543+ FromUser AuthorizedUser{..} ->
544+ refreshToken _userId _userSecret (Just _userRefresh) l m
546+formURL :: OAuthClient -> [OAuthScope] -> Text
547+formURL OAuthClient{..} ss =
548+ LText.toStrict . LText.decodeUtf8 . Build.toLazyByteString $
549+ buildText accountsURL
550+ <> "?response_type=code"
551+ <> "&client_id=" <> buildText _clientId
552+ <> "&redirect_uri=" <> buildText redirectURI
553+ <> "&scope=" <> queryEncodeScopes ss
556+redirectURI = "urn:ietf:wg:oauth:2.0:oob"
558+exchangeCode :: (MonadIO m, MonadCatch m)
564+exchangeCode OAuthClient{..} n = refreshRequest $
566+ { Client.requestBody = buildBody $
567+ "grant_type=authorization_code"
568+ <> "&client_id=" <> buildText _clientId
569+ <> "&client_secret=" <> buildText _clientSecret
570+ <> "&code=" <> buildText n
571+ <> "&redirect_uri=" <> buildText redirectURI
574+refresh :: (MonadIO m, MonadCatch m)
580+refresh c OAuthToken{..} l m =
582+ FromMetadata s -> refreshMetadata s l m
583+ FromAccount a ss -> refreshAssertion a ss l m
584+ FromClient OAuthClient{..} _ ->
585+ refreshToken _clientId _clientSecret _tokenRefresh l m
586+ FromUser AuthorizedUser{..} ->
587+ refreshToken _userId _userSecret
588+ (_tokenRefresh <|> Just _userRefresh) l m
590+refreshMetadata :: (MonadIO m, MonadCatch m)
595+refreshMetadata s = refreshRequest $
597+ { Client.path = "instance/service-accounts/"
598+ <> Text.encodeUtf8 (serviceIdToText s)
602+refreshToken :: (MonadIO m, MonadCatch m)
605+ -> Maybe RefreshToken
609+refreshToken c s r = refreshRequest $
611+ { Client.requestBody = buildBody $
612+ "grant_type=refresh_token"
613+ <> "&client_id=" <> buildText c
614+ <> "&client_secret=" <> buildText s
615+ <> maybe mempty ("&refresh_token=" <>) (buildText <$> r)
618+refreshAssertion :: (MonadIO m, MonadCatch m)
624+refreshAssertion s ss l m = do
625+ b <- encodeJWTBearer s ss
480− let rq = accountsRequest
626+ let rq = accountsRequest
481− { Client.requestBody = RequestBodyBS $
627+ { Client.requestBody = buildBody $
482− "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
628+ "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
483− <> "&assertion="
629+ <> "&assertion="
484− <> b
630+ <> Build.byteString b
486− refreshRequest s rq l m
632+ refreshRequest rq l m
488−jwtEncode :: (MonadIO m, MonadThrow m)
634+encodeJWTBearer :: (MonadIO m, MonadThrow m)
489− => ServiceAccount
635+ => ServiceAccount
490− -> m ByteString
637+ -> m ByteString
491−jwtEncode s = liftIO $ do
638+encodeJWTBearer s ss = liftIO $ do
492 i <- input . truncate <$> getPOSIXTime
639 i <- input . truncate <$> getPOSIXTime
493− r <- signSafer (Just SHA256) (_serviceKey s) i
640+ r <- signSafer (Just SHA256) (_servicePrivateKey s) i
494− either failure (\x -> pure (i <> "." <> signature (base64 x))) r
641+ either failure (pure . concat' i) r
643+ concat' i x = i <> "." <> signature (base64 x)
496 failure e = throwM $
645 failure e = throwM $
497 TokenRefreshError (toEnum 400) (Text.pack (show e)) Nothing
646 TokenRefreshError (toEnum 400) (Text.pack (show e)) Nothing
@@ -512,30 +661,30 @@ jwtEncode s = liftIO $ do
514 payload = base64Encode
663 payload = base64Encode
515− [ "aud" .= Text.pack "https://accounts.google.com/o/oauth2/token"
664+ [ "aud" .= accountsURL
516− , "scope" .= concatScopes (_serviceScope s)
665+ , "scope" .= concatScopes ss
517 , "iat" .= n
666 , "iat" .= n
518 , "exp" .= (n + maxTokenLifetimeSeconds)
667 , "exp" .= (n + maxTokenLifetimeSeconds)
519 , "iss" .= _serviceEmail s
668 , "iss" .= _serviceEmail s
522−instance Refresh AuthorisedUser where
671+queryEncodeScopes :: [OAuthScope] -> Build.Builder
523− refresh u@AuthorisedUser{..} = refreshRequest u $
672+queryEncodeScopes =
524− accountsRequest
673+ mconcat
525− { Client.requestBody = RequestBodyBS . Text.encodeUtf8 $
674+ . intersperse "+"
526− "grant_type=refresh_token"
675+ . map (urlEncodeBuilder True . Text.encodeUtf8)
527− <> "&client_id=" <> clientIdToText _userId
676+ . coerce
528− <> "&client_secret=" <> _userSecret
677+
529− <> "&refresh_token=" <> _userRefresh
678+concatScopes :: [OAuthScope] -> LText.Text
531−
680+ TBuild.toLazyText
532−instance Refresh ServiceId where
681+ . mconcat
533− refresh sid = refreshRequest sid $
682+ . intersperse " "
534− metadataRequest
683+ . map TBuild.fromText
535− { Client.path = "instance/service-accounts/"
684+ . coerce
536− <> Text.encodeUtf8 (serviceIdToText sid)
685+
537− <> "/token"
686+accountsURL :: Text
538− }
687+accountsURL = "https://accounts.google.com/o/oauth2/token"
540accountsRequest :: Client.Request
689accountsRequest :: Client.Request
541accountsRequest = def
690accountsRequest = def
@@ -545,16 +694,17 @@ accountsRequest = def
545 , Client.checkStatus = \_ _ _ -> Nothing
694 , Client.checkStatus = \_ _ _ -> Nothing
546 , Client.method = "POST"
695 , Client.method = "POST"
547 , Client.path = "/o/oauth2/token"
696 , Client.path = "/o/oauth2/token"
697+ , Client.requestHeaders =
548− , Client.requestHeaders = [(hContentType, "application/x-www-form-urlencoded")]
698+ [ (hContentType, "application/x-www-form-urlencoded")
551refreshRequest :: (MonadIO m, MonadCatch m)
702refreshRequest :: (MonadIO m, MonadCatch m)
553− -> Client.Request
703+ => Client.Request
554 -> Logger
704 -> Logger
555 -> Manager
705 -> Manager
556− -> m (Expires a)
706+ -> m OAuthToken
557−refreshRequest r rq l m = do
707+refreshRequest rq l m = do
558 logDebug l rq -- debug:ClientRequest
708 logDebug l rq -- debug:ClientRequest
560 rs <- liftIO (httpLbs rq m) `catch` (throwM . RetrievalError)
710 rs <- liftIO (httpLbs rq m) `catch` (throwM . RetrievalError)
@@ -572,8 +722,7 @@ refreshRequest r rq l m = do
572 success s bs = do
722 success s bs = do
573 f <- parseErr s bs
723 f <- parseErr s bs
574 ts <- liftIO getCurrentTime
724 ts <- liftIO getCurrentTime
575− let Bearer {..} = f ts
725+ pure (f ts)
576− pure $! Expires _bearerExpiry _bearerAccess r
578 failure s bs = do
727 failure s bs = do
579 let e = "Failure refreshing token from " <> host <> path
728 let e = "Failure refreshing token from " <> host <> path
@@ -586,7 +735,8 @@ refreshRequest r rq l m = do
586 case parseLBS bs of
735 case parseLBS bs of
587 Right !x -> pure x
736 Right !x -> pure x
588 Left e -> do
737 Left e -> do
589− logError l $ "[Parse Error] Failure parsing token refresh " <> build e
739+ "[Parse Error] Failure parsing token refresh " <> build e
590 refreshErr s e Nothing
740 refreshErr s e Nothing
592 refreshErr :: MonadThrow m => Status -> Text -> Maybe Text -> m a
742 refreshErr :: MonadThrow m => Status -> Text -> Maybe Text -> m a
@@ -603,3 +753,9 @@ base64Encode = base64 . LBS.toStrict . encode . object
604base64 :: ByteArray a => a -> ByteString
754base64 :: ByteArray a => a -> ByteString
605base64 = convertToBase Base64URLUnpadded
755base64 = convertToBase Base64URLUnpadded
757+buildBody :: Builder -> RequestBody
758+buildBody = RequestBodyLBS . Build.toLazyByteString
760+buildText :: ToText a => a -> Builder
761+buildText = build . toText