gogol-0.3.0: src/Network/Google/Auth.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module : Network.Google.Auth
-- Copyright : (c) 2015-2016 Brendan Hay
-- License : Mozilla Public License, v. 2.0.
-- Maintainer : Brendan Hay <brendan.g.hay@gmail.com>
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
--
-- Explicitly specify your Google credentials, or retrieve them
-- from the underlying OS.
module Network.Google.Auth
(
-- * Credentials
Credentials (..)
-- ** Application Default Credentials
, getApplicationDefault
, fromWellKnownPath
, fromFilePath
, saveAuthorizedUserToWellKnownPath
, saveAuthorizedUser
-- ** Installed Application Credentials
, installedApplication
, formURL
-- ** Authorizing Requests
, authorize
-- ** Thread-safe Storage
, Store
, initStore
, retrieveAuthFromStore
, Auth (..)
, authToAuthorizedUser
, exchange
, refresh
-- ** Default Constants
, checkGCEVar
, cloudSDKConfigDir
, defaultCredentialsFile
-- ** Handling Errors
, AsAuthError (..)
, AuthError (..)
-- * OAuth Types
, OAuthClient (..)
, OAuthToken (..)
, OAuthCode (..)
, OAuthScope (..)
-- * Re-exported Types
, AccessToken (..)
, RefreshToken (..)
, Secret (..)
, ServiceId (..)
, ClientId (..)
-- * Re-exported Modules
, module Network.Google.Auth.Scope
) where
import Control.Concurrent
import Control.Monad.Catch (MonadCatch)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Time (getCurrentTime)
import GHC.TypeLits (Symbol)
import Network.Google.Auth.ApplicationDefault
import Network.Google.Auth.InstalledApplication
import Network.Google.Auth.Scope
import Network.Google.Auth.ServiceAccount
import Network.Google.Compute.Metadata (checkGCEVar)
import Network.Google.Internal.Auth
import Network.Google.Internal.Logger (Logger)
import Network.Google.Prelude
import Network.HTTP.Conduit (Manager)
import qualified Network.HTTP.Conduit as Client
import Network.HTTP.Types (hAuthorization)
-- | 'authToAuthorizedUser' converts 'Auth' into an 'AuthorizedUser'
-- by returning 'Right' if there is a 'FromClient'-constructed
-- Credentials and a refreshed token; otherwise, returning
-- 'Left' with error message.
authToAuthorizedUser :: AllowScopes s => Auth s -> Either Text AuthorizedUser
authToAuthorizedUser a = AuthorizedUser
<$> (_clientId <$> getClient)
<*> maybe (Left "no refresh token") Right (_tokenRefresh (_token a))
<*> (_clientSecret <$> getClient)
where getClient = case _credentials a of
FromClient c _ -> Right c
_ -> Left "not FromClient"
-- | An 'OAuthToken' that can potentially be expired, with the original
-- credentials that can be used to perform a refresh.
data Auth (s :: [Symbol]) = Auth
{ _credentials :: !(Credentials s)
, _token :: !(OAuthToken s)
}
-- | Check if the given token is still valid, ie. younger than the projected
-- expiry time.
--
-- This deliberately makes no external calls due to the absolute construction of
-- the '_tokenExpiry' field, unlike the
-- <https://developers.google.com/accounts/docs/OAuth2Login#validatingtoken documented>
-- validation method.
validate :: MonadIO m => Auth s -> m Bool
validate a = (< _tokenExpiry (_token a)) <$> liftIO getCurrentTime
-- | Data store which ensures thread-safe access of credentials.
newtype Store (s :: [Symbol]) = Store (MVar (Auth s))
-- | Construct storage containing the credentials which have not yet been
-- exchanged or refreshed.
initStore :: (MonadIO m, MonadCatch m, AllowScopes s)
=> Credentials s
-> Logger
-> Manager
-> m (Store s)
initStore c l m = exchange c l m >>= fmap Store . liftIO . newMVar
-- | Retrieve auth from storage
retrieveAuthFromStore :: (MonadIO m, MonadCatch m, AllowScopes s)
=> Store s
-> m (Auth s)
retrieveAuthFromStore (Store s) = liftIO (readMVar s)
-- | Concurrently read the current token, and if expired, then
-- safely perform a single serial refresh.
getToken :: (MonadIO m, MonadCatch m, AllowScopes s)
=> Store s
-> Logger
-> Manager
-> m (OAuthToken s)
getToken (Store s) l m = do
x <- liftIO (readMVar s)
mx <- validate x
if mx
then pure (_token x)
else liftIO . modifyMVar s $ \y -> do
my <- validate y
if my
then pure (y, _token y)
else do
z <- refresh y l m
pure (z, _token z)
-- | Perform the initial credentials exchange to obtain a valid 'OAuthToken'
-- suitable for authorizing requests.
exchange :: forall m s. (MonadIO m, MonadCatch m, AllowScopes s)
=> Credentials s
-> Logger
-> Manager
-> m (Auth s)
exchange c l = fmap (Auth c) . action l
where
action = case c of
FromMetadata s -> metadataToken s
FromAccount a -> serviceAccountToken a (Proxy :: Proxy s)
FromClient x n -> exchangeCode x n
FromUser u -> authorizedUserToken u Nothing
-- | Refresh an existing 'OAuthToken'.
refresh :: forall m s. (MonadIO m, MonadCatch m, AllowScopes s)
=> Auth s
-> Logger
-> Manager
-> m (Auth s)
refresh (Auth c t) l = fmap (Auth c) . action l
where
action = case c of
FromMetadata s -> metadataToken s
FromAccount a -> serviceAccountToken a (Proxy :: Proxy s)
FromClient x _ -> refreshToken x t
FromUser u -> authorizedUserToken u (_tokenRefresh t)
-- | Apply the (by way of possible token refresh) a bearer token to the
-- authentication header of a request.
authorize :: (MonadIO m, MonadCatch m, AllowScopes s)
=> Client.Request
-> Store s
-> Logger
-> Manager
-> m Client.Request
authorize rq s l m = bearer <$> getToken s l m
where
bearer t = rq
{ Client.requestHeaders =
( hAuthorization
, "Bearer " <> toHeader (_tokenAccess t)
) : Client.requestHeaders rq
}