hoauth2-providers-0.2: src/Network/OAuth2/Provider/Okta.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
module Network.OAuth2.Provider.Okta where
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.Aeson
import Data.Aeson qualified as Aeson
import Data.Bifunctor
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text.Lazy (Text)
import Data.Time
import GHC.Generics
import Jose.Jwa
import Jose.Jwk
import Jose.Jws
import Jose.Jwt
import Network.OAuth.OAuth2
import Network.OAuth2.Experiment
import Network.OAuth2.Provider.Utils
import Network.OIDC.WellKnown
import URI.ByteString.QQ
data Okta = Okta deriving (Eq, Show)
type instance IdpUserInfo Okta = OktaUser
defaultOktaApp :: Idp Okta -> IdpApplication 'AuthorizationCode Okta
defaultOktaApp i =
AuthorizationCodeIdpApplication
{ idpAppClientId = ""
, idpAppClientSecret = ""
, idpAppScope = Set.fromList ["openid", "profile", "email"]
, idpAppAuthorizeState = "CHANGE_ME"
, idpAppAuthorizeExtraParams = Map.empty
, idpAppRedirectUri = [uri|http://localhost|]
, idpAppName = "default-okta-App"
, idpAppTokenRequestAuthenticationMethod = ClientSecretBasic
, idp = i
}
defaultOktaIdp :: Idp Okta
defaultOktaIdp =
Idp
{ idpFetchUserInfo = authGetJSON @(IdpUserInfo Okta)
, idpUserInfoEndpoint = [uri|https://foo.okta.com/oauth2/v1/userinfo|]
, idpAuthorizeEndpoint =
[uri|https://foo.okta.com/oauth2/v1/authorize|]
, idpTokenEndpoint =
[uri|https://foo.okta.com/oauth2/v1/token|]
}
mkOktaIdp ::
MonadIO m =>
-- | Full domain with no http protocol. e.g. @foo.okta.com@
Text ->
ExceptT Text m (Idp Okta)
mkOktaIdp domain = do
OpenIDConfigurationUris {..} <- fetchWellKnownUris domain
pure
( defaultOktaIdp
{ idpUserInfoEndpoint = userinfoUri
, idpAuthorizeEndpoint = authorizationUri
, idpTokenEndpoint = tokenUri
}
)
mkOktaClientCredentialAppJwt ::
Jwk ->
ClientId ->
Idp Okta ->
IO (Either String Jwt)
mkOktaClientCredentialAppJwt jwk cid idp = do
now <- getCurrentTime
let cidStr = unClientId cid
let payload =
bsToStrict $
Aeson.encode $
Aeson.object
[ "iss" .= cidStr
, "sub" .= cidStr
, "aud" .= idpTokenEndpoint idp
, "exp" .= tToSeconds (addUTCTime (secondsToNominalDiffTime 300) now) -- 5 minutes expiration time
, "iat" .= tToSeconds now
]
first show <$> jwkEncode RS256 jwk (Claims payload)
where
tToSeconds = formatTime defaultTimeLocale "%s"
-- https://developer.okta.com/docs/reference/api/oidc/#request-parameters
-- Okta Org AS doesn't support consent
-- Okta Custom AS does support consent via config (what scope shall prompt consent)
data OktaUser = OktaUser
{ name :: Text
, preferredUsername :: Text
}
deriving (Show, Generic)
instance FromJSON OktaUser where
parseJSON =
genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}