{-# LANGUAGE OverloadedStrings #-}

-- | Create a signed JWT needed to make the access token request
-- to gain access to Google APIs for server to server applications.
--
-- For all details : https://developers.google.com/identity/protocols/OAuth2ServiceAccount
--

module Network.Google.OAuth2.JWT where

import           Codec.Crypto.RSA.Pure
import qualified Data.ByteString            as B
import           Data.ByteString.Base64.URL (encode)
import           Data.ByteString.Lazy       (fromStrict, toStrict)
import           Data.Maybe                 (fromMaybe, fromJust)
import           Data.Monoid                ((<>))
import qualified Data.Text                  as T
import           Data.Text.Encoding
import           Data.UnixTime              (getUnixTime, utSeconds)
import           Foreign.C.Types
import           OpenSSL.EVP.PKey           (toKeyPair)
import           OpenSSL.PEM                (PemPasswordSupply (PwNone),
                                             readPrivateKey)
import           OpenSSL.RSA

type Scope = T.Text

type Email = T.Text

-- |Get the private key obtained from
-- the Google API Console from a PEM file.
fromPEMFile :: FilePath -> IO PrivateKey
fromPEMFile f = readFile f >>= fromPEMString

-- |Get the private key obtained from
-- the Google API Console from a PEM 'String'.
fromPEMString :: String -> IO PrivateKey
fromPEMString s =
    fromJust . toKeyPair <$> readPrivateKey s PwNone
        >>= \k -> return PrivateKey
            { private_pub =
                  PublicKey { public_size = rsaSize k
                            , public_n    = rsaN k
                            , public_e    = rsaE k
                            }
            , private_d    = rsaD k
            , private_p    = rsaP k
            , private_q    = rsaQ k
            , private_dP   = 0
            , private_dQ   = 0
            , private_qinv = 0
            }

-- | Create the signed JWT ready for transmission
-- in the access token request as assertion value.
--
-- >grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=
--
getSignedJWT :: Email
       -- ^ The email address of the service account.
       -> Maybe Email
       -- ^ The email address of the user for which the
       -- application is requesting delegated access.
       -> [Scope]
       -- ^The list of the permissions that the application requests.
       -> Maybe Int
       -- ^ Expiration time (maximun and default value is an hour, 3600).
       -> PrivateKey
       -- ^ The private key obtained from the Google API Console.
       -> IO (Either String B.ByteString)
       -- ^ Either an error message or a signed JWT.
getSignedJWT iss msub scopes mexp privateKey = do
    let expt = fromIntegral $ fromMaybe 3600 mexp
    cs <- jwtClaimsSet
              (maybe T.empty (\s -> "\"sub\":\"" <> s <> "\",") msub) expt
    let i = jwtHeader <> "." <> cs
    return $
        if expt > 0 && expt <= 3600 then
            case rsassa_pkcs1_v1_5_sign hashSHA256 privateKey (fromStrict i) of
                Right s -> Right $ i <> "." <> encode (toStrict s)
                Left _  -> Left "RSAError"
                                    else Left "Bad expiration time"
  where
    jwtHeader = toJWT "{\"alg\":\"RS256\",\"typ\":\"JWT\"}"
    jwtClaimsSet s e = do
        (exp',iat') <-
            getUnixTime >>= \t ->
                return ( toText $ utSeconds t + CTime e
                       , toText $ utSeconds t
                       )
        return $ toJWT $
               "{\"iss\":\"" <> iss <> "\"," <> s <> "\"scope\":\""
            <> T.intercalate " " scopes <> "\",\"aud\":\"https://ww\
               \w.googleapis.com/oauth2/v4/token\",\"exp\":" <> exp'
            <> ",\"iat\":" <> iat' <> "}"
    toText = T.pack . show
    toJWT = encode . encodeUtf8