@@ -19,334 +19,148 @@
20-- Explicitly specify your Google credentials, or retrieve them
20-- Explicitly specify your Google credentials, or retrieve them
21-- from the underlying OS.
21-- from the underlying OS.
22−module Network.Google.Auth
22+module Network.Google.Auth where
24− -- * Credentials
24+ -- -- * Credentials
25− Credentials (..)
25+ -- Credentials (..)
27− -- ** Discovery
27+ -- -- ** Discovery
28− , discover
28+ -- , discover
29− , fromFile
29+ -- , fromFile
30− , fromFilePath
30+ -- , fromFilePath
31− , fromJSONCredentials
31+ -- , fromJSONCredentials
33− -- ** Thread-safe Storage
33+ -- -- ** Thread-safe Storage
34− , Auth (..)
34+ -- , Auth (..)
35− , Store
35+ -- , Store
36− , emptyStore
36+ -- , emptyStore
38− -- ** Authorizing Requests
38+ -- -- ** Authorizing Requests
39− , authorize
39+ -- , authorize
41− -- ** Constructing Callback URLs
41+ -- -- ** Constructing Callback URLs
42− , formURL
42+ -- , formURL
43− , redirectURI
43+ -- , redirectURI
44− , accountsURL
44+ -- , accountsURL
46− -- ** Default Constants
46+ -- -- ** Default Constants
47− -- *** Google Compute Engine
47+ -- -- *** Google Compute Engine
48− , checkGCEVar
48+ -- , checkGCEVar
50− -- *** Cloud SDK
50+ -- -- *** Cloud SDK
51− , cloudSDKConfigDir
51+ -- , cloudSDKConfigDir
52− , cloudSDKConfigPath
52+ -- , cloudSDKConfigPath
54− -- *** Application Default Credentials
54+ -- -- *** Application Default Credentials
55− , defaultCredentialsFile
55+ -- , defaultCredentialsFile
56− , defaultCredentialsPath
56+ -- , defaultCredentialsPath
58− -- ** Handling Errors
58+ -- -- ** Handling Errors
59− , AsAuthError (..)
59+ -- , AsAuthError (..)
60− , AuthError (..)
60+ -- , AuthError (..)
62− -- * Credential Types
62+ -- -- * Credential Types
63− , ServiceAccount (..)
63+ -- , ServiceAccount (..)
64− , AuthorizedUser (..)
64+ -- , AuthorizedUser (..)
66− -- * OAuth Types
66+ -- -- * OAuth Types
67− , OAuthClient (..)
67+ -- , OAuthClient (..)
68− , OAuthToken (..)
68+ -- , OAuthToken (..)
69− , OAuthCode (..)
69+ -- , OAuthCode (..)
70− , OAuthScope (..)
70+ -- , OAuthScope (..)
72− -- * Re-exported Types
72+ -- -- * Re-exported Types
73− , AccessToken (..)
73+ -- , AccessToken (..)
74− , RefreshToken (..)
74+ -- , RefreshToken (..)
75− , Secret (..)
75+ -- , Secret (..)
76− , ServiceId (..)
76+ -- , ServiceId (..)
77− , ClientId (..)
77+ -- , ClientId (..)
79− -- * Exchange and Refresh Internals
79+ -- -- * Exchange and Refresh Internals
80− , getToken
80+ -- , getToken
81− , validateToken
81+ -- , validateToken
83− , exchange
83+ -- , exchange
84− , exchangeCode
84+ -- , exchangeCode
86− , refresh
86+ -- , refresh
87− , refreshMetadata
87+ -- , refreshMetadata
88− , refreshToken
88+ -- , refreshToken
89− , refreshAssertion
89+ -- , refreshAssertion
90− ) where
90+ -- ) where
92import Control.Applicative
92import Control.Applicative
93import Control.Concurrent
93import Control.Concurrent
94import Control.Exception.Lens
94import Control.Exception.Lens
95−import Control.Lens hiding (coerce, (.=))
95+import Control.Lens hiding
96import Control.Monad
98import Control.Monad
97import Control.Monad.Catch
99import Control.Monad.Catch
98import Control.Monad.IO.Class
100import Control.Monad.IO.Class
99−import Crypto.Hash.Algorithms (SHA256 (..))
101+import Crypto.Hash.Algorithms (SHA256 (..))
100−import Crypto.PubKey.RSA.PKCS15 (signSafer)
102+import Crypto.PubKey.RSA.PKCS15 (signSafer)
101−import Crypto.PubKey.RSA.Types (PrivateKey)
103+import Crypto.PubKey.RSA.Types (PrivateKey)
102import Data.Aeson
104import Data.Aeson
103import Data.Aeson.Types
105import Data.Aeson.Types
104import Data.ByteArray
106import Data.ByteArray
105import Data.ByteArray.Encoding
107import Data.ByteArray.Encoding
106−import Data.ByteString (ByteString)
108+import Data.ByteString (ByteString)
107−import Data.ByteString.Builder (Builder)
109+import Data.ByteString.Builder (Builder)
108−import qualified Data.ByteString.Builder as Build
110+import qualified Data.ByteString.Builder as Build
109−import qualified Data.ByteString.Char8 as BS8
111+import qualified Data.ByteString.Char8 as BS8
110−import qualified Data.ByteString.Lazy as LBS
112+import qualified Data.ByteString.Lazy as LBS
111import Data.Coerce
113import Data.Coerce
112−import Data.Default.Class (def)
114+import Data.Default.Class (def)
113−import Data.List (intersperse)
115+import Data.List (intersperse)
114import Data.String
116import Data.String
115−import qualified Data.Text as Text
117+import qualified Data.Text as Text
116−import qualified Data.Text.Encoding as Text
118+import qualified Data.Text.Encoding as Text
117−import qualified Data.Text.Lazy as LText
118−import qualified Data.Text.Lazy.Builder as TBuild
119−import qualified Data.Text.Lazy.Encoding as LText
120import Data.Time
119import Data.Time
121import Data.Time.Clock.POSIX
120import Data.Time.Clock.POSIX
122import Data.Typeable
121import Data.Typeable
123import Data.X509
122import Data.X509
124import Data.X509.Memory
123import Data.X509.Memory
124+import Network.Google.Auth.ApplicationDefaultCredentials
125+import Network.Google.Auth.Device
126+import Network.Google.Auth.InstalledApplication
127+import Network.Google.Auth.ServiceAccount
125import Network.Google.Compute.Metadata
128import Network.Google.Compute.Metadata
129+import Network.Google.Internal.Auth
126import Network.Google.Internal.Logger
130import Network.Google.Internal.Logger
127−import Network.Google.Prelude hiding (buildText)
131+import Network.Google.Prelude
128−import Network.HTTP.Conduit hiding (Request)
132+import Network.HTTP.Conduit hiding
129−import qualified Network.HTTP.Conduit as Client
134+import qualified Network.HTTP.Conduit as Client
130import Network.HTTP.Types
135import Network.HTTP.Types
131−import System.Directory (doesFileExist,
133−import System.Environment (lookupEnv)
134−import System.FilePath ((</>))
135−import System.Info (os)
137−-- | 1 hour in seconds.
138−maxTokenLifetimeSeconds :: Int
139−maxTokenLifetimeSeconds = 3600
141−-- | The environment variable name which is used to specify the directory
142−-- containing the @application_default_credentials.json@ generated by @gcloud init@.
143−cloudSDKConfigDir :: String
144−cloudSDKConfigDir = "CLOUDSDK_CONFIG"
146−-- | The environment variable pointing the file with local
147−-- Application Default Credentials.
148−defaultCredentialsFile :: String
149−defaultCredentialsFile = "GOOGLE_APPLICATION_CREDENTIALS"
151−-- | An error thrown when attempting to read AuthN/AuthZ information.
153− = RetrievalError HttpException
154− | MissingFileError FilePath
155− | InvalidFileError FilePath Text
156− | TokenRefreshError Status Text (Maybe Text)
157− deriving (Show, Typeable)
159−instance Exception AuthError
161−class AsAuthError a where
162− -- | A general authentication error.
163− _AuthError :: Prism' a AuthError
164− {-# MINIMAL _AuthError #-}
166− -- | An error occured while communicating over HTTP with either then
167− -- local metadata or remote accounts.google.com endpoints.
168− _RetrievalError :: Prism' a HttpException
170− -- | The specified default credentials file could not be found.
171− _MissingFileError :: Prism' a FilePath
173− -- | An error occured parsing the default credentials file.
174− _InvalidFileError :: Prism' a (FilePath, Text)
176− -- | An error occured when attempting to refresh a token.
177− _TokenRefreshError :: Prism' a (Status, Text, Maybe Text)
179− _RetrievalError = _AuthError . _RetrievalError
180− _MissingFileError = _AuthError . _MissingFileError
181− _InvalidFileError = _AuthError . _InvalidFileError
182− _TokenRefreshError = _AuthError . _TokenRefreshError
184−instance AsAuthError SomeException where
185− _AuthError = exception
187−instance AsAuthError AuthError where
190− _RetrievalError = prism RetrievalError $ \case
191− RetrievalError e -> Right e
194− _MissingFileError = prism MissingFileError $ \case
195− MissingFileError f -> Right f
198− _InvalidFileError = prism
199− (uncurry InvalidFileError)
201− InvalidFileError f e -> Right (f, e)
204− _TokenRefreshError = prism
205− (\(s, e, d) -> TokenRefreshError s e d)
207− TokenRefreshError s e d -> Right (s, e, d)
210−-- | Lookup the @GOOGLE_APPLICATION_CREDENTIALS@ environment variable for the
211−-- default application credentials filepath.
212−defaultCredentialsPath :: MonadIO m => m (Maybe FilePath)
213−defaultCredentialsPath = liftIO (lookupEnv defaultCredentialsFile)
215−-- | Return the filepath to the Cloud SDK well known file location such as
216−-- @~\/.config\/gcloud\/application_default_credentials.json@.
217−cloudSDKConfigPath :: MonadIO m => m FilePath
218−cloudSDKConfigPath = do
219− m <- liftIO (lookupEnv cloudSDKConfigDir)
221− Just d -> pure $! d </> "application_default_credentials.json"
223− d <- getConfigDirectory
224− pure $! d </> "gcloud/application_default_credentials.json"
226−getConfigDirectory :: MonadIO m => m FilePath
227−getConfigDirectory = do
228− h <- liftIO getHomeDirectory
231− else pure $! h </> ".config"
233−-- | Given a client identifier, client secret, and a list of scopes to authorize,
234−-- construct a URL that can be used to obtain the 'OAuthCode' required to
235−-- instantiate 'FromClient'-style credentials.
236−formURL :: OAuthClient -> [OAuthScope] -> Text
237−formURL OAuthClient{..} ss =
238− LText.toStrict . LText.decodeUtf8 . Build.toLazyByteString $
239− buildText accountsURL
240− <> "?response_type=code"
241− <> "&client_id=" <> buildText _clientId
242− <> "&redirect_uri=" <> buildText redirectURI
243− <> "&scope=" <> queryEncodeScopes ss
245−-- | @urn:ietf:wg:oauth:2.0:oob@.
247−redirectURI = "urn:ietf:wg:oauth:2.0:oob"
249−{-| Service Account credentials which are typically generated/download
250−from the Google Developer console of the following form:
254− \"type\": \"service_account\",
255− \"private_key_id\": \"303ad77e5efdf2ce952DFa\",
256− \"private_key\": \"-----BEGIN PRIVATE KEY-----\n...\n\",
257− \"client_email\": \"email@serviceaccount.com\",
258− \"client_id\": \"035-2-310.useraccount.com\"
262−The private key is used to sign a JSON Web Token (JWT) of the
263−grant_type @urn:ietf:params:oauth:grant-type:jwt-bearer@, which is sent to
264−'accountsURL' to obtain a valid 'OAuthToken'. This process requires explicitly
265−specifying which 'Scope's the resulting 'OAuthToken' is authorized to access.
267−data ServiceAccount = ServiceAccount
268− { _serviceId :: !ClientId
269− , _serviceEmail :: !Text
270− , _serviceKeyId :: !Text
271− , _servicePrivateKey :: !PrivateKey
272− } deriving (Eq, Show)
274−instance FromJSON ServiceAccount where
275− parseJSON = withObject "service_account" $ \o -> do
276− bs <- Text.encodeUtf8 <$> o .: "private_key"
277− k <- case listToMaybe (readKeyFileFromMemory bs) of
278− Just (PrivKeyRSA k) -> pure k
280− fail "Unable to parse key contents from \"private_key\""
282− <$> o .: "client_id"
283− <*> o .: "client_email"
284− <*> o .: "private_key_id"
287−{-| Authorized User credentials which are typically generated by the Cloud SDK
288−Tools such as @gcloud init@, of the following form:
292− \"type\": \"authorized_user\",
293− \"client_id\": \"32555940559.apps.googleusercontent.com\",
294− \"client_secret\": \"Zms2qjJy2998hD4CTg2ejr2\",
295− \"refresh_token\": \"1/B3gM1x35v.VtqffS1n5w-rSJ\"
299−The secret and refresh token are used to obtain a valid 'OAuthToken' from
300−'accountsURL' using grant_type @refresh_token@.
302−data AuthorizedUser = AuthorizedUser
303− { _userId :: !ClientId
304− , _userRefresh :: !RefreshToken
305− , _userSecret :: !Secret
306− } deriving (Eq, Show)
308−instance FromJSON AuthorizedUser where
309− parseJSON = withObject "authorized_user" $ \o -> AuthorizedUser
310− <$> o .: "client_id"
311− <*> o .: "refresh_token"
312− <*> o .: "client_secret"
314−-- | A client identifier and accompanying secret used to obtain/refresh a token.
315−data OAuthClient = OAuthClient
316− { _clientId :: !ClientId
317− , _clientSecret :: !Secret
318− } deriving (Eq, Show)
320−{-| An OAuth bearer type token of the following form:
322−@
137+-- | Attempt credentials discovery via the following steps:
324− \"token_type\": \"Bearer\",
139+-- * Read the default credentials from a file specified by
325− \"access_token\": \"eyJhbGci...\",
140+-- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
326− \"refresh_token\": \"1/B3gq9K...\",
141+--
327− \"expires_in\": 3600,
142+-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
328− ...
143+-- The @~/.config@ component of the path can be overriden by the environment
329−}
144+-- variable @CLOUDSDK_CONFIG@ if it exists.
331−
146+-- * Retrieve the default service account application credentials if
332−The '_tokenAccess' field will be inserted verbatim into the
147+-- running on GCE. The environment variable @NO_GCE_CHECK@ can be used to
333−@Authorization: Bearer ...@ header for all HTTP requests.
148+-- skip this check if set to a truthy value such as @1@ or @true@.
335−data OAuthToken = OAuthToken
150+-- The specified 'Scope's are used to authorize any @service_account@ that is
336− { _tokenAccess :: !AccessToken
151+-- found with the appropriate scopes, otherwise they are not used. See the
337− , _tokenRefresh :: !(Maybe RefreshToken)
152+-- top-level module of each individual @gogol-*@ library for a list of available
338− , _tokenExpiry :: !UTCTime
153+-- scopes, such as @Network.Google.Compute.computeScope@.
339− } deriving (Eq, Show)
154+-- discover :: (MonadIO m, MonadCatch m)
340−
155+-- => [OAuthScope]
341−instance FromJSON (UTCTime -> OAuthToken) where
156+-- -> Manager
342− parseJSON = withObject "bearer" $ \o -> do
157+-- -> m Credentials
343− t <- o .: "access_token"
158+-- discover ss m =
344− r <- o .:? "refresh_token"
159+-- catching _MissingFileError (fromFile ss) $ \f -> do
345− e <- o .: "expires_in" <&> fromInteger
160+-- p <- isGCE m
346− pure (OAuthToken t r . addUTCTime e)
161+-- unless p $
347−
162+-- throwingM _MissingFileError f
348−newtype OAuthCode = OAuthCode { codeToText :: Text }
163+-- pure $! FromMetadata "default"
349− deriving (Eq, Ord, Show, Read, IsString, Generic, Typeable, ToText, FromJSON, ToJSON)
351-- | The supported credential mechanisms.
165-- | The supported credential mechanisms.
352data Credentials
166data Credentials
@@ -375,6 +189,31 @@ data Credentials
375 -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
189 -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
376 -- of the Google CloudSDK Tools.
190 -- of the Google CloudSDK Tools.
192+exchange :: (MonadIO m, MonadCatch m)
199+ FromMetadata s -> metadataToken s l m
200+ FromAccount a ss -> serviceAccountToken a ss l m
201+ FromClient c' n -> exchangeCode c' n l m
202+ FromUser u -> authorizedUserToken u Nothing l m
204+refresh :: (MonadIO m, MonadCatch m)
212+ FromMetadata s -> metadataToken s l m
213+ FromAccount a ss -> serviceAccountToken a ss l m
214+ FromClient c _ -> refreshToken c t l m
215+ FromUser u -> authorizedUserToken u (_tokenRefresh t) l m
378-- | A credentials value in one of two possible states, pre-exchange and
217-- | A credentials value in one of two possible states, pre-exchange and
379-- containing a valid, but possibly expired access token.
218-- containing a valid, but possibly expired access token.
@@ -386,6 +225,14 @@ data Auth
386 -- ^ An 'OAuthToken' that can potentially be expired, with the original
225 -- ^ An 'OAuthToken' that can potentially be expired, with the original
387 -- credentials that can be used to perform a refresh.
226 -- credentials that can be used to perform a refresh.
228+validateToken :: MonadIO m => Auth -> m (Maybe OAuthToken)
229+validateToken Exchange {} = pure Nothing
230+validateToken (Refresh _ t) = do
231+ ts <- liftIO getCurrentTime
232+ if ts < _tokenExpiry t
389type Store = MVar Auth
236type Store = MVar Auth
391-- | Construct storage containing the credentials which have not yet been
238-- | Construct storage containing the credentials which have not yet been
@@ -393,85 +240,6 @@ type Store = MVar Auth
393emptyStore :: MonadIO m => Credentials -> m Store
240emptyStore :: MonadIO m => Credentials -> m Store
394emptyStore !c = liftIO . newMVar $ Exchange c
241emptyStore !c = liftIO . newMVar $ Exchange c
396−-- | Attempt credentials discovery via the following steps:
398−-- * Read the default credentials from a file specified by
399−-- the environment variable @GOOGLE_APPLICATION_CREDENTIALS@ if it exists.
401−-- * Read the platform equivalent of @~\/.config\/gcloud\/application_default_credentials.json@ if it exists.
402−-- The @~/.config@ component of the path can be overriden by the environment
403−-- variable @CLOUDSDK_CONFIG@ if it exists.
405−-- * Retrieve the default service account application credentials if
406−-- running on GCE. The environment variable @NO_GCE_CHECK@ can be used to
407−-- skip this check if set to a truthy value such as @1@ or @true@.
409−-- The specified 'Scope's are used to authorize any @service_account@ that is
410−-- found with the appropriate scopes, otherwise they are not used. See the
411−-- top-level module of each individual @gogol-*@ library for a list of available
412−-- scopes, such as @Network.Google.Compute.computeScope@.
413−discover :: (MonadIO m, MonadCatch m)
418− catching _MissingFileError (fromFile ss) $ \f -> do
421− throwingM _MissingFileError f
422− pure $! FromMetadata "default"
424−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
425−-- file to obtain the credentials neccessary to perform a token refresh.
427−-- The specified 'Scope's are used to authorize any @service_account@ that is
428−-- found with the appropriate scopes, otherwise they are not used. See the
429−-- top-level module of each individual @gogol-*@ library for a list of available
430−-- scopes, such as @Network.Google.Compute.computeScope@.
432−-- /See:/ 'cloudSDKConfigPath', 'defaultCredentialsPath'.
433−fromFile :: (MonadIO m, MonadCatch m) => [OAuthScope] -> m Credentials
435− f <- defaultCredentialsPath
437− Just x -> fromFilePath ss x
439− x <- cloudSDKConfigPath
442−-- | Attempt to load either a @service_account@ or @authorized_user@ formatted
443−-- file to obtain the credentials neccessary to perform a token refresh from
444−-- the specified file.
446−-- The specified 'Scope's are used to authorize any @service_account@ that is
447−-- found with the appropriate scopes, otherwise they are not used. See the
448−-- top-level module of each individual @gogol-*@ library for a list of available
449−-- scopes, such as @Network.Google.Compute.computeScope@.
450−fromFilePath :: (MonadIO m, MonadCatch m)
454−fromFilePath ss f = do
455− p <- liftIO (doesFileExist f)
457− throwM (MissingFileError f)
458− bs <- liftIO (LBS.readFile f)
459− either (throwM . InvalidFileError f . Text.pack) pure
460− (fromJSONCredentials ss bs)
462−fromJSONCredentials :: [OAuthScope]
464− -> Either String Credentials
465−fromJSONCredentials ss bs = do
466− v <- eitherDecode' bs
467− let x = (`FromAccount` ss) <$> parseEither parseJSON v
468− y = FromUser <$> parseEither parseJSON v
470− (Left xe, Left ye) -> Left $
471− "Failed parsing service_account: " ++ xe ++
472− ", Failed parsing authorized_user: " ++ ye
475authorize :: (MonadIO m, MonadCatch m)
243authorize :: (MonadIO m, MonadCatch m)
476 => Client.Request
244 => Client.Request
@@ -483,7 +251,7 @@ authorize rq s l m = bearer <$> getToken s l m
483 bearer t = rq
251 bearer t = rq
484 { Client.requestHeaders =
252 { Client.requestHeaders =
485 ( hAuthorization
253 ( hAuthorization
486− , "Bearer " <> Text.encodeUtf8 (accessToText (_tokenAccess t))
254+ , "Bearer " <> Text.encodeUtf8 (toText (_tokenAccess t))
487 ) : Client.requestHeaders rq
255 ) : Client.requestHeaders rq
@@ -509,240 +277,3 @@ getToken s l m = do
509 Refresh c t -> do
277 Refresh c t -> do
510 t' <- refresh c t l m
278 t' <- refresh c t l m
511 pure (Refresh c t', t')
279 pure (Refresh c t', t')
513−validateToken :: MonadIO m => Auth -> m (Maybe OAuthToken)
514−validateToken Exchange {} = pure Nothing
515−validateToken (Refresh _ t) = do
516− ts <- liftIO getCurrentTime
517− if ts < _tokenExpiry t
521−exchange :: (MonadIO m, MonadCatch m)
528− FromMetadata s -> refreshMetadata s l m
529− FromAccount a ss -> refreshAssertion a ss l m
530− FromClient c' n -> exchangeCode c' n l m
531− FromUser AuthorizedUser{..} ->
532− refreshToken _userId _userSecret (Just _userRefresh) l m
534−exchangeCode :: (MonadIO m, MonadCatch m)
540−exchangeCode OAuthClient{..} n = refreshRequest $
542− { Client.requestBody = buildBody $
543− "grant_type=authorization_code"
544− <> "&client_id=" <> buildText _clientId
545− <> "&client_secret=" <> buildText _clientSecret
546− <> "&code=" <> buildText n
547− <> "&redirect_uri=" <> buildText redirectURI
550−refresh :: (MonadIO m, MonadCatch m)
556−refresh c OAuthToken{..} l m =
558− FromMetadata s -> refreshMetadata s l m
559− FromAccount a ss -> refreshAssertion a ss l m
560− FromClient OAuthClient{..} _ ->
561− refreshToken _clientId _clientSecret _tokenRefresh l m
562− FromUser AuthorizedUser{..} ->
563− refreshToken _userId _userSecret
564− (_tokenRefresh <|> Just _userRefresh) l m
566−refreshMetadata :: (MonadIO m, MonadCatch m)
571−refreshMetadata s = refreshRequest $
573− { Client.path = "instance/service-accounts/"
574− <> Text.encodeUtf8 (serviceIdToText s)
578−refreshToken :: (MonadIO m, MonadCatch m)
581− -> Maybe RefreshToken
585−refreshToken c s r = refreshRequest $
587− { Client.requestBody = buildBody $
588− "grant_type=refresh_token"
589− <> "&client_id=" <> buildText c
590− <> "&client_secret=" <> buildText s
591− <> maybe mempty ("&refresh_token=" <>) (buildText <$> r)
594−refreshAssertion :: (MonadIO m, MonadCatch m)
600−refreshAssertion s ss l m = do
601− b <- encodeJWTBearer s ss
602− let rq = accountsRequest
603− { Client.requestBody = buildBody $
604− "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
606− <> Build.byteString b
608− refreshRequest rq l m
610−encodeJWTBearer :: (MonadIO m, MonadThrow m)
614−encodeJWTBearer s ss = liftIO $ do
615− i <- input . truncate <$> getPOSIXTime
616− r <- signSafer (Just SHA256) (_servicePrivateKey s) i
617− either failure (pure . concat' i) r
619− concat' i x = i <> "." <> signature (base64 x)
621− failure e = throwM $
622− TokenRefreshError (toEnum 400) (Text.pack (show e)) Nothing
625− case BS8.unsnoc bs of
631− input n = header <> "." <> payload
633− header = base64Encode
634− [ "alg" .= ("RS256" :: Text)
635− , "typ" .= ("JWT" :: Text)
636− , "kid" .= _serviceKeyId s
639− payload = base64Encode
640− [ "aud" .= accountsURL
641− , "scope" .= concatScopes ss
643− , "exp" .= (n + maxTokenLifetimeSeconds)
644− , "iss" .= _serviceEmail s
647−-- | @https://accounts.google.com/o/oauth2/token@.
649−accountsURL = "https://accounts.google.com/o/oauth2/token"
651−accountsRequest :: Client.Request
652−accountsRequest = def
653− { Client.host = "accounts.google.com"
655− , Client.secure = True
656− , Client.checkStatus = \_ _ _ -> Nothing
657− , Client.method = "POST"
658− , Client.path = "/o/oauth2/token"
659− , Client.requestHeaders =
660− [ (hContentType, "application/x-www-form-urlencoded")
664−refreshRequest :: (MonadIO m, MonadCatch m)
669−refreshRequest rq l m = do
670− logDebug l rq -- debug:ClientRequest
672− rs <- liftIO (httpLbs rq m) `catch` (throwM . RetrievalError)
674− let bs = responseBody rs
675− s = responseStatus rs
677− logDebug l rs -- debug:ClientResponse
678− logTrace l $ "[Response Body]\n" <> bs -- trace:ResponseBody
680− if fromEnum s == 200
686− ts <- liftIO getCurrentTime
690− let e = "Failure refreshing token from " <> host <> path
691− logError l $ "[Refresh Error] " <> build e
693− Right x -> refreshErr s (_error x) (_description x)
694− Left _ -> refreshErr s e Nothing
701− "[Parse Error] Failure parsing token refresh " <> build e
702− refreshErr s e Nothing
704− refreshErr :: MonadThrow m => Status -> Text -> Maybe Text -> m a
705− refreshErr s e = throwM . TokenRefreshError s e
707− host = Text.decodeUtf8 (Client.host rq)
708− path = Text.decodeUtf8 (Client.path rq)
710−parseLBS :: FromJSON a => LBS.ByteString -> Either Text a
711−parseLBS = either (Left . Text.pack) Right . eitherDecode'
713−base64Encode :: [Pair] -> ByteString
714−base64Encode = base64 . LBS.toStrict . encode . object
716−base64 :: ByteArray a => a -> ByteString
717−base64 = convertToBase Base64URLUnpadded
719−buildBody :: Builder -> RequestBody
720−buildBody = RequestBodyLBS . Build.toLazyByteString
722−buildText :: ToText a => a -> Builder
723−buildText = build . toText
725−queryEncodeScopes :: [OAuthScope] -> Build.Builder
729− . map (urlEncodeBuilder True . Text.encodeUtf8)
732−concatScopes :: [OAuthScope] -> LText.Text
737− . map TBuild.fromText
740−data RefreshError = RefreshError
742− , _description :: !(Maybe Text)
745−instance FromJSON RefreshError where
746− parseJSON = withObject "refresh_error" $ \o -> RefreshError
748− <*> o .:? "error_description"