{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
module Network.Google.Auth.ServiceAccount where
import Control.Monad.Catch
import Control.Monad.IO.Class
import Crypto.Hash.Algorithms (SHA256 (..))
import Crypto.PubKey.RSA.PKCS15 (signSafer)
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time.Clock.POSIX
import Network.Google.Auth.Scope (AllowScopes (..),
concatScopes)
import Network.Google.Compute.Metadata
import Network.Google.Internal.Auth
import Network.Google.Internal.Logger
import Network.Google.Prelude hiding (buildText)
import Network.HTTP.Conduit hiding (Request)
import qualified Network.HTTP.Conduit as Client
maxTokenLifetime :: Seconds
maxTokenLifetime = 3600
metadataToken :: (MonadIO m, MonadCatch m)
=> ServiceId
-> Logger
-> Manager
-> m (OAuthToken s)
metadataToken s = refreshRequest $
metadataRequest
{ Client.path = "/computeMetadata/v1/instance/service-accounts/"
<> Text.encodeUtf8 (toQueryParam s)
<> "/token"
}
authorizedUserToken :: (MonadIO m, MonadCatch m)
=> AuthorizedUser
-> Maybe RefreshToken
-> Logger
-> Manager
-> m (OAuthToken s)
authorizedUserToken u r = refreshRequest $
tokenRequest
{ Client.requestBody = textBody $
"grant_type=refresh_token"
<> "&client_id=" <> toQueryParam (_userId u)
<> "&client_secret=" <> toQueryParam (_userSecret u)
<> "&refresh_token=" <> toQueryParam (fromMaybe (_userRefresh u) r)
}
serviceAccountToken :: (MonadIO m, MonadCatch m, AllowScopes s)
=> ServiceAccount
-> proxy s
-> Logger
-> Manager
-> m (OAuthToken s)
serviceAccountToken s p l m = do
b <- encodeBearerJWT s p
let rq = tokenRequest
{ Client.requestBody = RequestBodyBS $
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer"
<> "&assertion="
<> b
}
refreshRequest rq l m
encodeBearerJWT :: (MonadIO m, MonadThrow m, AllowScopes s)
=> ServiceAccount
-> proxy s
-> m ByteString
encodeBearerJWT s p = liftIO $ do
i <- input . truncate <$> getPOSIXTime
r <- signSafer (Just SHA256) (_servicePrivateKey s) i
either failure (pure . concat' i) r
where
concat' i x = i <> "." <> signature (base64 x)
failure e = throwM $
TokenRefreshError (toEnum 400) (Text.pack (show e)) Nothing
signature bs =
case BS8.unsnoc bs of
Nothing -> mempty
Just (bs', x)
| x == '=' -> bs'
| otherwise -> bs
input n = header <> "." <> payload
where
header = base64Encode
[ "alg" .= ("RS256" :: Text)
, "typ" .= ("JWT" :: Text)
, "kid" .= _serviceKeyId s
]
payload = base64Encode $
[ "aud" .= tokenURL
, "scope" .= concatScopes (allowScopes p)
, "iat" .= n
, "exp" .= (n + seconds maxTokenLifetime)
, "iss" .= _serviceEmail s
] <> maybe [] (\sub -> ["sub" .= sub]) (_serviceAccountUser s)