@@ -1,9 +1,7 @@
1−{-# LANGUAGE BangPatterns #-}
2{-# LANGUAGE DeriveDataTypeable #-}
1{-# LANGUAGE DeriveDataTypeable #-}
3{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE DeriveGeneric #-}
4{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE FlexibleContexts #-}
5{-# LANGUAGE FlexibleInstances #-}
4{-# LANGUAGE FlexibleInstances #-}
7{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8{-# LANGUAGE LambdaCase #-}
6{-# LANGUAGE LambdaCase #-}
9{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE OverloadedStrings #-}
@@ -30,39 +28,28 @@ module Network.Google.Auth
30 , fromFilePath
28 , fromFilePath
31 , fromJSONCredentials
29 , fromJSONCredentials
33− -- ** Thread-safe Storage
38 -- ** Authorizing Requests
31 -- ** Authorizing Requests
39 , authorize
32 , authorize
41 -- ** Constructing Callback URLs
34 -- ** Constructing Callback URLs
44− , accountsURL
37+ -- ** Thread-safe Storage
46 -- ** Default Constants
44 -- ** Default Constants
47− -- *** Google Compute Engine
48 , checkGCEVar
45 , checkGCEVar
51 , cloudSDKConfigDir
46 , cloudSDKConfigDir
54− -- *** Application Default Credentials
55 , defaultCredentialsFile
47 , defaultCredentialsFile
56− , defaultCredentialsPath
58 -- ** Handling Errors
49 -- ** Handling Errors
59 , AsAuthError (..)
50 , AsAuthError (..)
60 , AuthError (..)
51 , AuthError (..)
62− -- * Credential Types
63− , ServiceAccount (..)
64− , AuthorizedUser (..)
66 -- * OAuth Types
53 -- * OAuth Types
67 , OAuthClient (..)
54 , OAuthClient (..)
68 , OAuthToken (..)
55 , OAuthToken (..)
@@ -75,278 +62,56 @@ module Network.Google.Auth
75 , Secret (..)
62 , Secret (..)
76 , ServiceId (..)
63 , ServiceId (..)
77 , ClientId (..)
64 , ClientId (..)
79− -- * Exchange and Refresh Internals
92import Control.Applicative
67import Control.Applicative
93import Control.Concurrent
68import Control.Concurrent
94import Control.Exception.Lens
69import Control.Exception.Lens
95−import Control.Lens hiding (coerce, (.=))
70+import Control.Lens hiding
96import Control.Monad
73import Control.Monad
97import Control.Monad.Catch
74import Control.Monad.Catch
98import Control.Monad.IO.Class
75import Control.Monad.IO.Class
99−import Crypto.Hash.Algorithms (SHA256 (..))
76+import Crypto.Hash.Algorithms (SHA256 (..))
100−import Crypto.PubKey.RSA.PKCS15 (signSafer)
77+import Crypto.PubKey.RSA.PKCS15 (signSafer)
101−import Crypto.PubKey.RSA.Types (PrivateKey)
78+import Crypto.PubKey.RSA.Types (PrivateKey)
102import Data.Aeson
79import Data.Aeson
103import Data.Aeson.Types
80import Data.Aeson.Types
104import Data.ByteArray
81import Data.ByteArray
105import Data.ByteArray.Encoding
82import Data.ByteArray.Encoding
106−import Data.ByteString (ByteString)
83+import Data.ByteString (ByteString)
107−import Data.ByteString.Builder (Builder)
84+import Data.ByteString.Builder (Builder)
108−import qualified Data.ByteString.Builder as Build
85+import qualified Data.ByteString.Builder as Build
109−import qualified Data.ByteString.Char8 as BS8
86+import qualified Data.ByteString.Char8 as BS8
110−import qualified Data.ByteString.Lazy as LBS
87+import qualified Data.ByteString.Lazy as LBS
111import Data.Coerce
88import Data.Coerce
112−import Data.Default.Class (def)
89+import Data.Default.Class (def)
113−import Data.List (intersperse)
90+import Data.List (intersperse)
114import Data.String
91import Data.String
115−import qualified Data.Text as Text
92+import qualified Data.Text as Text
116−import qualified Data.Text.Encoding as Text
93+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
94import Data.Time
121import Data.Time.Clock.POSIX
95import Data.Time.Clock.POSIX
122import Data.Typeable
96import Data.Typeable
123import Data.X509
97import Data.X509
124import Data.X509.Memory
98import Data.X509.Memory
99+import Network.Google.Auth.ApplicationDefaultCredentials
100+import Network.Google.Auth.Device
101+import Network.Google.Auth.InstalledApplication
102+import Network.Google.Auth.ServiceAccount
125import Network.Google.Compute.Metadata
103import Network.Google.Compute.Metadata
104+import Network.Google.Internal.Auth
126import Network.Google.Internal.Logger
105import Network.Google.Internal.Logger
127−import Network.Google.Prelude hiding (buildText)
106+import Network.Google.Prelude
128−import Network.HTTP.Conduit hiding (Request)
107+import Network.HTTP.Conduit hiding
129−import qualified Network.HTTP.Conduit as Client
109+import qualified Network.HTTP.Conduit as Client
130import Network.HTTP.Types
110import Network.HTTP.Types
131−import System.Directory (doesFileExist,
111+import System.Directory (doesFileExist, getHomeDirectory)
133−import System.Environment (lookupEnv)
112+import System.Environment (lookupEnv)
134−import System.FilePath ((</>))
113+import System.FilePath ((</>))
135−import System.Info (os)
114+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:
324− \"token_type\": \"Bearer\",
325− \"access_token\": \"eyJhbGci...\",
326− \"refresh_token\": \"1/B3gq9K...\",
327− \"expires_in\": 3600,
332−The '_tokenAccess' field will be inserted verbatim into the
333−@Authorization: Bearer ...@ header for all HTTP requests.
335−data OAuthToken = OAuthToken
336− { _tokenAccess :: !AccessToken
337− , _tokenRefresh :: !(Maybe RefreshToken)
338− , _tokenExpiry :: !UTCTime
339− } deriving (Eq, Show)
341−instance FromJSON (UTCTime -> OAuthToken) where
342− parseJSON = withObject "bearer" $ \o -> do
343− t <- o .: "access_token"
344− r <- o .:? "refresh_token"
345− e <- o .: "expires_in" <&> fromInteger
346− pure (OAuthToken t r . addUTCTime e)
348−newtype OAuthCode = OAuthCode { codeToText :: Text }
349− deriving (Eq, Ord, Show, Read, IsString, Generic, Typeable, ToText, FromJSON, ToJSON)
351-- | The supported credential mechanisms.
116-- | The supported credential mechanisms.
352data Credentials
117data Credentials
@@ -375,24 +140,6 @@ data Credentials
375 -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
140 -- An 'AuthorizedUser' is typically created by the @gcloud init@ command
376 -- of the Google CloudSDK Tools.
141 -- of the Google CloudSDK Tools.
378−-- | A credentials value in one of two possible states, pre-exchange and
379−-- containing a valid, but possibly expired access token.
381− = Exchange !Credentials
382− -- ^ Signifies that the initial token refresh has not occured, and the
383− -- 'Credentials' need to be signed and exchanged for a valid 'OAuthToken'.
385− | Refresh !Credentials !OAuthToken
386− -- ^ An 'OAuthToken' that can potentially be expired, with the original
387− -- credentials that can be used to perform a refresh.
389−type Store = MVar Auth
391−-- | Construct storage containing the credentials which have not yet been
392−-- exchanged or refreshed.
393−emptyStore :: MonadIO m => Credentials -> m Store
394−emptyStore !c = liftIO . newMVar $ Exchange c
396-- | Attempt credentials discovery via the following steps:
143-- | Attempt credentials discovery via the following steps:
398-- * Read the default credentials from a file specified by
145-- * Read the default credentials from a file specified by
@@ -459,6 +206,8 @@ fromFilePath ss f = do
459 either (throwM . InvalidFileError f . Text.pack) pure
206 either (throwM . InvalidFileError f . Text.pack) pure
460 (fromJSONCredentials ss bs)
207 (fromJSONCredentials ss bs)
209+-- | Attempt to parse either a @service_account@ or @authorized_user@ formatted
210+-- JSON value to obtain credentials.
462fromJSONCredentials :: [OAuthScope]
211fromJSONCredentials :: [OAuthScope]
463 -> LBS.ByteString
212 -> LBS.ByteString
464 -> Either String Credentials
213 -> Either String Credentials
@@ -472,21 +221,31 @@ fromJSONCredentials ss bs = do
472 ", Failed parsing authorized_user: " ++ ye
221 ", Failed parsing authorized_user: " ++ ye
473 _ -> x <|> y
222 _ -> x <|> y
475−authorize :: (MonadIO m, MonadCatch m)
224+-- | An 'OAuthToken' that can potentially be expired, with the original
476− => Client.Request
225+-- credentials that can be used to perform a refresh.
227+ { _credentials :: !Credentials
228+ , _token :: !OAuthToken
231+-- | Check if the given token is still valid, ie. younger than the projected
233+validate :: MonadIO m => Auth -> m Bool
234+validate a = (< _tokenExpiry (_token a)) <$> liftIO getCurrentTime
477− -> Store
236+type Store = MVar Auth
238+-- | Construct storage containing the credentials which have not yet been
239+-- exchanged or refreshed.
240+initStore :: (MonadIO m, MonadCatch m)
478 -> Logger
242 -> Logger
479 -> Manager
243 -> Manager
480− -> m Client.Request
244+ -> m Store
481−authorize rq s l m = bearer <$> getToken s l m
245+initStore c l m = exchange c l m >>= liftIO . newMVar
484− { Client.requestHeaders =
486− , "Bearer " <> Text.encodeUtf8 (accessToText (_tokenAccess t))
487− ) : Client.requestHeaders rq
247+-- | Concurrently read the current token, and if expired, then
248+-- safely perform a single serial refresh.
490getToken :: (MonadIO m, MonadCatch m)
249getToken :: (MonadIO m, MonadCatch m)
492 -> Logger
251 -> Logger
@@ -494,255 +253,59 @@ getToken :: (MonadIO m, MonadCatch m)
494 -> m OAuthToken
253 -> m OAuthToken
495getToken s l m = do
254getToken s l m = do
496 x <- liftIO (readMVar s)
255 x <- liftIO (readMVar s)
497− mx <- validateToken x
256+ mx <- validate x
498− case mx of
257+ if mx
499− Just t -> pure t
258+ then pure (_token x)
500− Nothing -> liftIO . modifyMVar s $ \y -> do
259+ else liftIO . modifyMVar s $ \y -> do
501− my <- validateToken y
260+ my <- validate y
502− case my of
261+ if my
503− Just t -> pure (y, t)
262+ then pure (y, _token y)
504− Nothing ->
263+ else do
505− case y of
264+ z <- refresh y l m
506− Exchange c -> do
265+ pure (z, _token z)
507− t <- exchange c l m
266+
508− pure (Refresh c t, t)
267+-- | Perform the initial credentials exchange to obtain a valid 'OAuthToken'
509− Refresh c t -> do
268+-- suitable for authorizing requests.
510− t' <- refresh c t l m
511− 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
521exchange :: (MonadIO m, MonadCatch m)
269exchange :: (MonadIO m, MonadCatch m)
522 => Credentials
270 => Credentials
523 -> Logger
271 -> Logger
524 -> Manager
272 -> Manager
525− -> m OAuthToken
273+ -> m Auth
526−exchange c l m =
274+exchange c l = fmap (Auth c) . action l
527− case c of
276+ action = case c of
528− FromMetadata s -> refreshMetadata s l m
277+ FromMetadata s -> metadataToken s
529− FromAccount a ss -> refreshAssertion a ss l m
278+ FromAccount a ss -> serviceAccountToken a ss
530− FromClient c' n -> exchangeCode c' n l m
279+ FromClient c n -> exchangeCode c n
531− FromUser AuthorizedUser{..} ->
280+ FromUser u -> authorizedUserToken u Nothing
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
282+-- | Refresh an existing 'OAuthToken' using
550refresh :: (MonadIO m, MonadCatch m)
283refresh :: (MonadIO m, MonadCatch m)
551− => Credentials
284+ => Auth
553 -> Logger
285 -> Logger
554 -> Manager
286 -> Manager
555− -> m OAuthToken
287+ -> m Auth
556−refresh c OAuthToken{..} l m =
288+refresh (Auth c t) l = fmap (Auth c) . action l
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
290+ action = case c of
626− Nothing -> mempty
291+ FromMetadata s -> metadataToken s
627− Just (bs', x)
292+ FromAccount a ss -> serviceAccountToken a ss
628− | x == '=' -> bs'
293+ FromClient x _ -> refreshToken x t
629− | otherwise -> bs
294+ FromUser u -> authorizedUserToken u (_tokenRefresh t)
631− input n = header <> "." <> payload
296+-- | Apply the (by way of possible token refresh) a bearer token to the
632− where
297+-- authentication header of a request.
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)
298+authorize :: (MonadIO m, MonadCatch m)
665− => Client.Request
299+ => Client.Request
666− -> Logger
301+ -> Logger
667− -> Manager
302+ -> Manager
668− -> m OAuthToken
303+ -> m Client.Request
669−refreshRequest rq l m = do
304+authorize rq s l m = bearer <$> getToken s l m
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
684− success s bs = do
306+ bearer t = rq
685− f <- parseErr s bs
307+ { Client.requestHeaders =
686− ts <- liftIO getCurrentTime
308+ ( hAuthorization
687− pure (f ts)
309+ , "Bearer " <> Text.encodeUtf8 (toText (_tokenAccess t))
688−
310+ ) : Client.requestHeaders rq
689− failure s bs = do
311+ }
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"