{-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module Network.OAuth2.Experiment.Types where import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.Trans.Except (ExceptT (..), throwE) import Data.Aeson (FromJSON) import Data.Bifunctor import Data.ByteString qualified as BS import Data.ByteString.Lazy.Char8 qualified as BSL import Data.Default (Default (def)) import Data.Kind import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map import Data.Set (Set) import Data.Set qualified as Set import Data.String import Data.Text.Encoding qualified as T import Data.Text.Lazy (Text) import Data.Text.Lazy qualified as TL import Network.HTTP.Conduit import Network.OAuth.OAuth2 hiding (RefreshToken) import Network.OAuth.OAuth2 qualified as OAuth2 import Network.OAuth2.Experiment.Pkce import Network.OAuth2.Experiment.Utils import URI.ByteString hiding (UserInfo) {- NOTE 1. shall I lift the constrain of all 'a :: GrantTypeFlow' so that user has max customization/flexibility? -} ------------------------------------------------------------------------------- -- * Grant Type ------------------------------------------------------------------------------- data GrantTypeFlow = -- | https://www.rfc-editor.org/rfc/rfc6749#section-4.1 AuthorizationCode | -- | https://www.rfc-editor.org/rfc/rfc6749#section-4.3 ResourceOwnerPassword | -- | https://www.rfc-editor.org/rfc/rfc6749#section-4.4 ClientCredentials | -- | https://www.rfc-editor.org/rfc/rfc7523.html#section-2.1 JwtBearer ------------------------------------------------------------------------------- -- * Response Type value ------------------------------------------------------------------------------- class ToResponseTypeValue (a :: GrantTypeFlow) where toResponseTypeValue :: IsString b => b instance ToResponseTypeValue 'AuthorizationCode where -- https://www.rfc-editor.org/rfc/rfc6749#section-3.1.1 -- Only support "authorization code" flow toResponseTypeValue :: IsString b => b toResponseTypeValue = "code" toResponseTypeParam :: forall a b req. (ToResponseTypeValue a, IsString b) => req a -> Map b b toResponseTypeParam _ = Map.singleton "response_type" (toResponseTypeValue @a) ------------------------------------------------------------------------------- -- * Grant Type value ------------------------------------------------------------------------------- newtype UrnOAuthParam a = UrnOAuthParam a -- | Grant type query parameter has association with 'GrantTypeFlow' but not completely strict. -- -- e.g. Both 'AuthorizationCode' and 'ResourceOwnerPassword' flow could support refresh token flow. data GrantTypeValue = GTAuthorizationCode | GTPassword | GTClientCredentials | GTRefreshToken | GTJwtBearer deriving (Eq, Show) ------------------------------------------------------------------------------- -- * Scope ------------------------------------------------------------------------------- -- TODO: following data type is not ideal as Idp would have lots of 'Custom Text' -- -- @ -- data Scope = OPENID | PROFILE | EMAIL | OFFLINE_ACCESS | Custom Text -- @ -- -- Would be nice to define Enum for standard Scope, plus allow user to define their own define (per Idp) and plugin somehow. newtype Scope = Scope {unScope :: Text} deriving (Show, Eq, Ord) instance IsString Scope where fromString :: String -> Scope fromString = Scope . TL.pack ------------------------------------------------------------------------------- -- * Credentials ------------------------------------------------------------------------------- newtype ClientId = ClientId {unClientId :: Text} deriving (Show, Eq, IsString) -- | Can be either "Client Secret" or JWT base on client authentication method newtype ClientSecret = ClientSecret {unClientSecret :: Text} deriving (Eq, IsString) -- | In order to reuse some methods from legacy "Network.OAuth.OAuth2". -- Will be removed when Experiment module becomes default. toOAuth2Key :: ClientId -> ClientSecret -> OAuth2 toOAuth2Key cid csecret = def { oauth2ClientId = TL.toStrict $ unClientId cid , oauth2ClientSecret = TL.toStrict $ unClientSecret csecret } newtype RedirectUri = RedirectUri {unRedirectUri :: URI} deriving (Eq) newtype AuthorizeState = AuthorizeState {unAuthorizeState :: Text} deriving (Eq) instance IsString AuthorizeState where fromString :: String -> AuthorizeState fromString = AuthorizeState . TL.pack newtype Username = Username {unUsername :: Text} deriving (Eq) instance IsString Username where fromString :: String -> Username fromString = Username . TL.pack newtype Password = Password {unPassword :: Text} deriving (Eq) instance IsString Password where fromString :: String -> Password fromString = Password . TL.pack ------------------------------------------------------------------------------- -- * Query parameters ------------------------------------------------------------------------------- class ToQueryParam a where toQueryParam :: a -> Map Text Text instance ToQueryParam a => ToQueryParam (Maybe a) where toQueryParam :: ToQueryParam a => Maybe a -> Map Text Text toQueryParam Nothing = Map.empty toQueryParam (Just a) = toQueryParam a instance ToQueryParam GrantTypeValue where toQueryParam :: GrantTypeValue -> Map Text Text toQueryParam x = Map.singleton "grant_type" (val x) where val :: GrantTypeValue -> Text val GTAuthorizationCode = "authorization_code" val GTPassword = "password" val GTClientCredentials = "client_credentials" val GTRefreshToken = "refresh_token" val GTJwtBearer = "urn:ietf:params:oauth:grant-type:jwt-bearer" instance ToQueryParam ClientId where toQueryParam :: ClientId -> Map Text Text toQueryParam (ClientId i) = Map.singleton "client_id" i instance ToQueryParam ClientSecret where toQueryParam :: ClientSecret -> Map Text Text toQueryParam (ClientSecret x) = Map.singleton "client_secret" x instance ToQueryParam Username where toQueryParam :: Username -> Map Text Text toQueryParam (Username x) = Map.singleton "username" x instance ToQueryParam Password where toQueryParam :: Password -> Map Text Text toQueryParam (Password x) = Map.singleton "password" x instance ToQueryParam AuthorizeState where toQueryParam :: AuthorizeState -> Map Text Text toQueryParam (AuthorizeState x) = Map.singleton "state" x instance ToQueryParam RedirectUri where toQueryParam (RedirectUri uri) = Map.singleton "redirect_uri" (bs8ToLazyText $ serializeURIRef' uri) instance ToQueryParam (Set Scope) where toQueryParam :: Set Scope -> Map Text Text toQueryParam = toScopeParam . Set.map unScope where toScopeParam :: (IsString a) => Set Text -> Map a Text toScopeParam scope = Map.singleton "scope" (TL.intercalate " " $ Set.toList scope) instance ToQueryParam CodeVerifier where toQueryParam :: CodeVerifier -> Map Text Text toQueryParam (CodeVerifier x) = Map.singleton "code_verifier" (TL.fromStrict x) instance ToQueryParam CodeChallenge where toQueryParam :: CodeChallenge -> Map Text Text toQueryParam (CodeChallenge x) = Map.singleton "code_challenge" (TL.fromStrict x) instance ToQueryParam CodeChallengeMethod where toQueryParam :: CodeChallengeMethod -> Map Text Text toQueryParam x = Map.singleton "code_challenge_method" (TL.pack $ show x) instance ToQueryParam ExchangeToken where toQueryParam :: ExchangeToken -> Map Text Text toQueryParam (ExchangeToken x) = Map.singleton "code" (TL.fromStrict x) instance ToQueryParam OAuth2.RefreshToken where toQueryParam :: OAuth2.RefreshToken -> Map Text Text toQueryParam (OAuth2.RefreshToken x) = Map.singleton "refresh_token" (TL.fromStrict x) ------------------------------------------------------------------------------- -- * Authorization and Token Requests types ------------------------------------------------------------------------------- class HasAuthorizeRequest (a :: GrantTypeFlow) where data AuthorizationRequest a type MkAuthorizationRequestResponse a mkAuthorizeRequestParameter :: IdpApplication a i -> AuthorizationRequest a mkAuthorizeRequest :: IdpApplication a i -> MkAuthorizationRequestResponse a class HasTokenRequest (a :: GrantTypeFlow) where -- | Each GrantTypeFlow has slightly different request parameter to /token endpoint. data TokenRequest a -- | Only 'AuthorizationCode flow (but not resource owner password nor client credentials) will use 'ExchangeToken' in the token request -- create type family to be explicit on it. -- with 'type instance WithExchangeToken a b = b' implies no exchange token -- v.s. 'type instance WithExchangeToken a b = ExchangeToken -> b' implies needing an exchange token type WithExchangeToken a b mkTokenRequest :: IdpApplication a i -> WithExchangeToken a (TokenRequest a) conduitTokenRequest :: (MonadIO m) => IdpApplication a i -> Manager -> WithExchangeToken a (ExceptT TokenRequestError m OAuth2Token) class HasPkceAuthorizeRequest (a :: GrantTypeFlow) where mkPkceAuthorizeRequest :: MonadIO m => IdpApplication a i -> m (TL.Text, CodeVerifier) class HasPkceTokenRequest (b :: GrantTypeFlow) where conduitPkceTokenRequest :: (MonadIO m) => IdpApplication b i -> Manager -> (ExchangeToken, CodeVerifier) -> ExceptT TokenRequestError m OAuth2Token class HasRefreshTokenRequest (a :: GrantTypeFlow) where -- | https://www.rfc-editor.org/rfc/rfc6749#page-47 data RefreshTokenRequest a mkRefreshTokenRequest :: IdpApplication a i -> OAuth2.RefreshToken -> RefreshTokenRequest a conduitRefreshTokenRequest :: (MonadIO m) => IdpApplication a i -> Manager -> OAuth2.RefreshToken -> ExceptT TokenRequestError m OAuth2Token ------------------------------------------------------------------------------- -- * User Info types ------------------------------------------------------------------------------- type family IdpUserInfo a class HasUserInfoRequest (a :: GrantTypeFlow) where conduitUserInfoRequest :: FromJSON (IdpUserInfo i) => IdpApplication a i -> Manager -> AccessToken -> ExceptT BSL.ByteString IO (IdpUserInfo i) ------------------------------------------------------------------------------- -- * Idp App ------------------------------------------------------------------------------- -- | Shall IdpApplication has a field of 'Idp a'?? data Idp a = Idp { idpUserInfoEndpoint :: URI , -- NOTE: maybe worth data type to distinguish authorize and token endpoint -- as I made mistake at passing to Authorize and Token Request idpAuthorizeEndpoint :: URI , idpTokenEndpoint :: URI , idpFetchUserInfo :: forall m. (FromJSON (IdpUserInfo a), MonadIO m) => Manager -> AccessToken -> URI -> ExceptT BSL.ByteString m (IdpUserInfo a) } ------------------------------------------------------------------------------- -- * Idp App Config ------------------------------------------------------------------------------- data family IdpApplication (a :: GrantTypeFlow) (i :: Type) ------------------------------------------------------------------------------- -- * Authorization Code flow ------------------------------------------------------------------------------- -- | An Application that supports "Authorization code" flow data instance IdpApplication 'AuthorizationCode i = AuthorizationCodeIdpApplication { idpAppName :: Text , idpAppClientId :: ClientId , idpAppClientSecret :: ClientSecret , idpAppScope :: Set Scope , idpAppRedirectUri :: URI , idpAppAuthorizeState :: AuthorizeState , idpAppAuthorizeExtraParams :: Map Text Text -- ^ Though technically one key can have multiple value in query, but who actually does it?! , idpAppTokenRequestAuthenticationMethod :: ClientAuthenticationMethod , idp :: Idp i } -- NOTE: maybe add function for parase authorization response -- though seems overkill. https://github.com/freizl/hoauth2/issues/149 -- parseAuthorizationResponse :: String -> AuthorizationResponse -- parseAuthorizationResponse :: ( String, String ) -> AuthorizationResponse instance HasAuthorizeRequest 'AuthorizationCode where -- \| https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1 data AuthorizationRequest 'AuthorizationCode = AuthorizationCodeAuthorizationRequest { scope :: Set Scope , state :: AuthorizeState , clientId :: ClientId , redirectUri :: Maybe RedirectUri } type MkAuthorizationRequestResponse 'AuthorizationCode = Text mkAuthorizeRequestParameter :: IdpApplication 'AuthorizationCode i -> AuthorizationRequest 'AuthorizationCode mkAuthorizeRequestParameter AuthorizationCodeIdpApplication {..} = AuthorizationCodeAuthorizationRequest { scope = if null idpAppScope then Set.empty else idpAppScope , state = idpAppAuthorizeState , clientId = idpAppClientId , redirectUri = Just (RedirectUri idpAppRedirectUri) } mkAuthorizeRequest :: IdpApplication 'AuthorizationCode i -> Text mkAuthorizeRequest idpAppConfig@AuthorizationCodeIdpApplication {..} = let req = mkAuthorizeRequestParameter idpAppConfig allParams = map (bimap tlToBS tlToBS) $ Map.toList $ Map.unions [idpAppAuthorizeExtraParams, toQueryParam req] in TL.fromStrict $ T.decodeUtf8 $ serializeURIRef' $ appendQueryParams allParams $ idpAuthorizeEndpoint idp instance HasTokenRequest 'AuthorizationCode where -- \| https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3 data TokenRequest 'AuthorizationCode = AuthorizationCodeTokenRequest { code :: ExchangeToken , clientId :: ClientId , grantType :: GrantTypeValue , redirectUri :: RedirectUri } type WithExchangeToken 'AuthorizationCode a = ExchangeToken -> a mkTokenRequest :: IdpApplication 'AuthorizationCode i -> ExchangeToken -> TokenRequest 'AuthorizationCode mkTokenRequest AuthorizationCodeIdpApplication {..} authCode = AuthorizationCodeTokenRequest { code = authCode , clientId = idpAppClientId , grantType = GTAuthorizationCode , redirectUri = RedirectUri idpAppRedirectUri } conduitTokenRequest :: forall m i. (MonadIO m) => IdpApplication 'AuthorizationCode i -> Manager -> ExchangeToken -> ExceptT TokenRequestError m OAuth2Token conduitTokenRequest idpAppConfig@AuthorizationCodeIdpApplication {..} mgr exchangeToken = let req = mkTokenRequest idpAppConfig exchangeToken key = toOAuth2Key idpAppClientId idpAppClientSecret body = mapsToParams [ toQueryParam req , toQueryParam ( if idpAppTokenRequestAuthenticationMethod == ClientSecretPost then Just idpAppClientSecret else Nothing ) ] in doJSONPostRequest mgr key (idpTokenEndpoint idp) body instance HasPkceAuthorizeRequest 'AuthorizationCode where mkPkceAuthorizeRequest :: MonadIO m => IdpApplication 'AuthorizationCode i -> m (Text, CodeVerifier) mkPkceAuthorizeRequest idpAppConfig@AuthorizationCodeIdpApplication {..} = do PkceRequestParam {..} <- mkPkceParam let req = mkAuthorizeRequestParameter idpAppConfig let allParams = mapsToParams [ idpAppAuthorizeExtraParams , toQueryParam req , toQueryParam codeChallenge , toQueryParam codeChallengeMethod ] let url = TL.fromStrict $ T.decodeUtf8 $ serializeURIRef' $ appendQueryParams allParams $ idpAuthorizeEndpoint idp pure (url, codeVerifier) instance HasPkceTokenRequest 'AuthorizationCode where conduitPkceTokenRequest :: MonadIO m => IdpApplication 'AuthorizationCode i -> Manager -> (ExchangeToken, CodeVerifier) -> ExceptT TokenRequestError m OAuth2Token conduitPkceTokenRequest idpAppConfig@AuthorizationCodeIdpApplication {..} mgr (exchangeToken, codeVerifier) = let req = mkTokenRequest idpAppConfig exchangeToken key = toOAuth2Key idpAppClientId idpAppClientSecret body = mapsToParams [ toQueryParam req , toQueryParam codeVerifier , toQueryParam (if idpAppTokenRequestAuthenticationMethod == ClientSecretPost then Just idpAppClientSecret else Nothing) ] in doJSONPostRequest mgr key (idpTokenEndpoint idp) body instance HasRefreshTokenRequest 'AuthorizationCode where data RefreshTokenRequest 'AuthorizationCode = AuthorizationCodeTokenRefreshRequest { refreshToken :: OAuth2.RefreshToken , grantType :: GrantTypeValue , scope :: Set Scope } mkRefreshTokenRequest :: IdpApplication 'AuthorizationCode i -> OAuth2.RefreshToken -> RefreshTokenRequest 'AuthorizationCode mkRefreshTokenRequest AuthorizationCodeIdpApplication {..} rt = AuthorizationCodeTokenRefreshRequest { scope = idpAppScope , grantType = GTRefreshToken , refreshToken = rt } conduitRefreshTokenRequest :: (MonadIO m) => IdpApplication 'AuthorizationCode i -> Manager -> OAuth2.RefreshToken -> ExceptT TokenRequestError m OAuth2Token conduitRefreshTokenRequest idpAppConfig@AuthorizationCodeIdpApplication {..} mgr rt = let req = mkRefreshTokenRequest idpAppConfig rt key = toOAuth2Key idpAppClientId idpAppClientSecret body = mapsToParams [ toQueryParam req , toQueryParam (if idpAppTokenRequestAuthenticationMethod == ClientSecretPost then Just idpAppClientSecret else Nothing) ] in doJSONPostRequest mgr key (idpTokenEndpoint idp) body instance HasUserInfoRequest 'AuthorizationCode where conduitUserInfoRequest :: FromJSON (IdpUserInfo i) => IdpApplication 'AuthorizationCode i -> Manager -> AccessToken -> ExceptT BSL.ByteString IO (IdpUserInfo i) conduitUserInfoRequest AuthorizationCodeIdpApplication {..} mgr at = do idpFetchUserInfo idp mgr at (idpUserInfoEndpoint idp) instance ToQueryParam (AuthorizationRequest 'AuthorizationCode) where toQueryParam :: AuthorizationRequest 'AuthorizationCode -> Map Text Text toQueryParam req@AuthorizationCodeAuthorizationRequest {..} = Map.unions [ toResponseTypeParam req , toQueryParam scope , toQueryParam clientId , toQueryParam state , toQueryParam redirectUri ] instance ToQueryParam (TokenRequest 'AuthorizationCode) where toQueryParam :: TokenRequest 'AuthorizationCode -> Map Text Text toQueryParam AuthorizationCodeTokenRequest {..} = Map.unions [ toQueryParam grantType , toQueryParam code , toQueryParam redirectUri ] instance ToQueryParam (RefreshTokenRequest 'AuthorizationCode) where toQueryParam :: RefreshTokenRequest 'AuthorizationCode -> Map Text Text toQueryParam AuthorizationCodeTokenRefreshRequest {..} = Map.unions [ toQueryParam grantType , toQueryParam scope , toQueryParam refreshToken ] ------------------------------------------------------------------------------- -- * JWTBearer ------------------------------------------------------------------------------- -- | An Application that supports "Authorization code" flow data instance IdpApplication 'JwtBearer i = JwtBearerIdpApplication { idpAppName :: Text , idpAppJwt :: BS.ByteString , idp :: Idp i } instance HasTokenRequest 'JwtBearer where data TokenRequest 'JwtBearer = JwtBearerTokenRequest { grantType :: GrantTypeValue -- \| 'GTJwtBearer' , assertion :: BS.ByteString -- \| The the signed JWT token } type WithExchangeToken 'JwtBearer a = a mkTokenRequest :: IdpApplication 'JwtBearer i -> TokenRequest 'JwtBearer mkTokenRequest JwtBearerIdpApplication {..} = JwtBearerTokenRequest { grantType = GTJwtBearer , assertion = idpAppJwt } conduitTokenRequest :: forall m i. (MonadIO m) => IdpApplication 'JwtBearer i -> Manager -> ExceptT TokenRequestError m OAuth2Token conduitTokenRequest idpAppConfig@JwtBearerIdpApplication {..} mgr = do resp <- ExceptT . liftIO $ do let tokenReq = mkTokenRequest idpAppConfig let body = mapsToParams [toQueryParam tokenReq] req <- uriToRequest (idpTokenEndpoint idp) handleOAuth2TokenResponse <$> httpLbs (urlEncodedBody body (addDefaultRequestHeaders req)) mgr case parseResponseFlexible resp of Right obj -> return obj Left e -> throwE e instance ToQueryParam (TokenRequest 'JwtBearer) where toQueryParam :: TokenRequest 'JwtBearer -> Map Text Text toQueryParam JwtBearerTokenRequest {..} = Map.unions [ toQueryParam grantType , Map.fromList [("assertion", bs8ToLazyText assertion)] ] instance HasUserInfoRequest 'JwtBearer where conduitUserInfoRequest JwtBearerIdpApplication {..} mgr at = do idpFetchUserInfo idp mgr at (idpUserInfoEndpoint idp) ------------------------------------------------------------------------------- -- * Password flow ------------------------------------------------------------------------------- -- https://www.rfc-editor.org/rfc/rfc6749#section-4.3.1 -- 4.3.1. Authorization Request and Response (Password grant type) -- The method through which the client obtains the resource owner -- credentials is beyond the scope of this specification. The client -- MUST discard the credentials once an access token has been obtained. -- -- Hence no AuhorizationRequest instance data instance IdpApplication 'ResourceOwnerPassword i = ResourceOwnerPasswordIDPApplication { idpAppClientId :: ClientId , idpAppClientSecret :: ClientSecret , idpAppName :: Text , idpAppScope :: Set Scope , idpAppUserName :: Username , idpAppPassword :: Password , idpAppTokenRequestExtraParams :: Map Text Text -- ^ Any parameter that required by your Idp and not mentioned in the OAuth2 spec , idp :: Idp i } instance HasUserInfoRequest 'ResourceOwnerPassword where conduitUserInfoRequest ResourceOwnerPasswordIDPApplication {..} mgr at = do idpFetchUserInfo idp mgr at (idpUserInfoEndpoint idp) instance HasTokenRequest 'ResourceOwnerPassword where -- \| https://www.rfc-editor.org/rfc/rfc6749#section-4.3.2 data TokenRequest 'ResourceOwnerPassword = PasswordTokenRequest { scope :: Set Scope , username :: Username , password :: Password , grantType :: GrantTypeValue } type WithExchangeToken 'ResourceOwnerPassword a = a mkTokenRequest :: IdpApplication 'ResourceOwnerPassword i -> TokenRequest 'ResourceOwnerPassword mkTokenRequest ResourceOwnerPasswordIDPApplication {..} = PasswordTokenRequest { username = idpAppUserName , password = idpAppPassword , grantType = GTPassword , scope = idpAppScope } conduitTokenRequest :: (MonadIO m) => IdpApplication 'ResourceOwnerPassword i -> Manager -> ExceptT TokenRequestError m OAuth2Token conduitTokenRequest idpAppConfig@ResourceOwnerPasswordIDPApplication {..} mgr = let req = mkTokenRequest idpAppConfig key = toOAuth2Key idpAppClientId idpAppClientSecret body = mapsToParams [idpAppTokenRequestExtraParams, toQueryParam req] in doJSONPostRequest mgr key (idpTokenEndpoint idp) body -- | TODO: TBD instance HasRefreshTokenRequest 'ResourceOwnerPassword where data RefreshTokenRequest 'ResourceOwnerPassword = PasswordRefreshTokenRequest mkRefreshTokenRequest :: IdpApplication 'ResourceOwnerPassword i -> OAuth2.RefreshToken -> RefreshTokenRequest 'ResourceOwnerPassword mkRefreshTokenRequest = undefined conduitRefreshTokenRequest :: MonadIO m => IdpApplication 'ResourceOwnerPassword i -> Manager -> OAuth2.RefreshToken -> ExceptT TokenRequestError m OAuth2Token conduitRefreshTokenRequest = undefined instance ToQueryParam (TokenRequest 'ResourceOwnerPassword) where toQueryParam :: TokenRequest 'ResourceOwnerPassword -> Map Text Text toQueryParam PasswordTokenRequest {..} = Map.unions [ toQueryParam grantType , toQueryParam scope , toQueryParam username , toQueryParam password ] ------------------------------------------------------------------------------- -- * Client Credentials flow ------------------------------------------------------------------------------- -- https://www.rfc-editor.org/rfc/rfc6749#section-4.4.1 -- 4.4.1. Authorization Request and Response (Client Credentials grant type) -- Since the client authentication is used as the authorization grant, -- no additional authorization request is needed. -- -- Hence no AuhorizationRequest instance data instance IdpApplication 'ClientCredentials i = ClientCredentialsIDPApplication { idpAppClientId :: ClientId , idpAppClientSecret :: ClientSecret , idpAppTokenRequestAuthenticationMethod :: ClientAuthenticationMethod -- ^ FIXME: rename to ClientCredential , idpAppName :: Text , idpAppScope :: Set Scope , idpAppTokenRequestExtraParams :: Map Text Text -- ^ Any parameter that required by your Idp and not mentioned in the OAuth2 spec , idp :: Idp i } instance HasTokenRequest 'ClientCredentials where -- \| https://www.rfc-editor.org/rfc/rfc6749#section-4.4.2 data TokenRequest 'ClientCredentials = ClientCredentialsTokenRequest { scope :: Set Scope , grantType :: GrantTypeValue , clientAssertionType :: Text , clientAssertion :: BS.ByteString , clientAuthenticationMethod :: ClientAuthenticationMethod } type WithExchangeToken 'ClientCredentials a = a mkTokenRequest :: IdpApplication 'ClientCredentials i -> TokenRequest 'ClientCredentials mkTokenRequest ClientCredentialsIDPApplication {..} = ClientCredentialsTokenRequest { scope = idpAppScope , grantType = GTClientCredentials , clientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" , clientAssertion = tlToBS $ unClientSecret idpAppClientSecret , clientAuthenticationMethod = idpAppTokenRequestAuthenticationMethod } conduitTokenRequest :: (MonadIO m) => IdpApplication 'ClientCredentials i -> Manager -> ExceptT TokenRequestError m OAuth2Token conduitTokenRequest idpAppConfig@ClientCredentialsIDPApplication {..} mgr = do let tokenReq = mkTokenRequest idpAppConfig key = toOAuth2Key idpAppClientId idpAppClientSecret body = mapsToParams [ idpAppTokenRequestExtraParams , toQueryParam tokenReq ] if clientAuthenticationMethod tokenReq == ClientAssertionJwt then do resp <- ExceptT . liftIO $ do req <- uriToRequest (idpTokenEndpoint idp) let req' = urlEncodedBody body (addDefaultRequestHeaders req) handleOAuth2TokenResponse <$> httpLbs req' mgr case parseResponseFlexible resp of Right obj -> return obj Left e -> throwE e else doJSONPostRequest mgr key (idpTokenEndpoint idp) body instance ToQueryParam (TokenRequest 'ClientCredentials) where toQueryParam :: TokenRequest 'ClientCredentials -> Map Text Text toQueryParam ClientCredentialsTokenRequest {..} = Map.unions $ [ toQueryParam grantType , toQueryParam scope ] ++ [ Map.fromList ( if clientAuthenticationMethod == ClientAssertionJwt then [ ("client_assertion_type", clientAssertionType) , ("client_assertion", bs8ToLazyText clientAssertion) ] else [] ) ]