{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE LambdaCase #-} {-| Module: Web.OIDC.Client.Tokens Maintainer: krdlab@gmail.com Stability: experimental -} module Web.OIDC.Client.Tokens ( Tokens(..) , IdTokenClaims(..) , validateIdToken ) where import Control.Applicative ((<|>)) import Control.Exception (throwIO) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson (FromJSON (parseJSON), FromJSON, Value (Object), eitherDecode, withObject, (.:), (.:?)) import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy.Char8 as BL import Data.Either (partitionEithers) import Data.Text (Text, pack) import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic) import Jose.Jwt (IntDate, Jwt, JwtContent (Jwe, Jws, Unsecured)) import qualified Jose.Jwt as Jwt import Prelude hiding (exp) import qualified Web.OIDC.Client.Discovery.Provider as P import Web.OIDC.Client.Settings (OIDC (..)) import Web.OIDC.Client.Types (OpenIdException (..)) data Tokens a = Tokens { accessToken :: Text , tokenType :: Text , idToken :: IdTokenClaims a , expiresIn :: Maybe Integer , refreshToken :: Maybe Text } deriving (Show, Eq) -- | Claims required for an , -- plus recommended claims (nonce) and other custom claims. data IdTokenClaims a = IdTokenClaims { iss :: !Text , sub :: !Text , aud :: ![Text] , exp :: !IntDate , iat :: !IntDate , nonce :: !(Maybe ByteString) , otherClaims :: !a } deriving (Show, Eq, Generic) instance FromJSON a => FromJSON (IdTokenClaims a) where parseJSON = withObject "IdTokenClaims" $ \o -> IdTokenClaims <$> o .: "iss" <*> o .: "sub" <*> (o .: "aud" <|> ((:[]) <$> (o .: "aud"))) <*> o .: "exp" <*> o .: "iat" <*> (fmap encodeUtf8 <$> o .:? "nonce") <*> parseJSON (Object o) validateIdToken :: (MonadIO m, FromJSON a) => OIDC -> Jwt -> m (IdTokenClaims a) validateIdToken oidc jwt' = do let jwks = P.jwkSet . oidcProvider $ oidc token = Jwt.unJwt jwt' algs = P.idTokenSigningAlgValuesSupported . P.configuration $ oidcProvider oidc decoded <- selectDecodedResult <$> traverse (tryDecode jwks token) algs case decoded of Right (Unsecured payload) -> liftIO . throwIO $ UnsecuredJwt payload Right (Jws (_header, payload)) -> parsePayload payload Right (Jwe (_header, payload)) -> parsePayload payload Left err -> liftIO . throwIO $ JwtExceptoin err where tryDecode jwks token = \case P.JwsAlgJson alg -> liftIO $ Jwt.decode jwks (Just $ Jwt.JwsEncoding alg) token P.Unsupported alg -> return $ Left $ Jwt.BadAlgorithm ("Unsupported algorithm: " <> alg) selectDecodedResult xs = case partitionEithers xs of (_, k : _) -> Right k (e : _, _) -> Left e ([], []) -> Left $ Jwt.KeyError "No Keys available for decoding" parsePayload payload = case eitherDecode $ BL.fromStrict payload of Right x -> return x Left err -> liftIO . throwIO . JsonException $ pack err