{-# LANGUAGE QuasiQuotes #-} -- 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) -- | [Okta OIDC & OAuth2 API](https://developer.okta.com/docs/reference/api/oidc/) module Network.OAuth2.Provider.Okta where import Control.Monad.IO.Class (MonadIO (..)) import Control.Monad.Trans.Except (ExceptT (..)) import Data.Aeson import Data.Aeson qualified as Aeson import Data.Bifunctor import Data.ByteString.Contrib import Data.ByteString.Lazy.Char8 qualified as BSL import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text.Lazy as TL import Data.Time import GHC.Generics import Jose.Jwa import Jose.Jwk import Jose.Jws import Jose.Jwt import Network.HTTP.Conduit (Manager) import Network.OAuth.OAuth2 import Network.OAuth2.Experiment import Network.OAuth2.Provider import Network.OIDC.WellKnown import URI.ByteString.QQ sampleOktaAuthorizationCodeApp :: AuthorizationCodeApplication sampleOktaAuthorizationCodeApp = AuthorizationCodeApplication { acClientId = "" , acClientSecret = "" , acScope = Set.fromList ["openid", "profile", "email"] , acAuthorizeState = "CHANGE_ME" , acAuthorizeRequestExtraParams = Map.empty , acRedirectUri = [uri|http://localhost|] , acName = "sample-okta-authorization-code-app" , acTokenRequestAuthenticationMethod = ClientSecretBasic } fetchUserInfo :: (MonadIO m, HasUserInfoRequest a, FromJSON b) => IdpApplication i a -> Manager -> AccessToken -> ExceptT BSL.ByteString m b fetchUserInfo = conduitUserInfoRequest mkOktaIdp :: MonadIO m => -- | Full domain with no http protocol. e.g. @foo.okta.com@ Text -> ExceptT Text m (Idp Okta) mkOktaIdp domain = do OpenIDConfiguration {..} <- fetchWellKnown domain pure $ Idp { idpUserInfoEndpoint = userinfoEndpoint , idpAuthorizeEndpoint = authorizationEndpoint , idpTokenEndpoint = tokenEndpoint , idpDeviceAuthorizationEndpoint = Just deviceAuthorizationEndpoint } mkOktaClientCredentialAppJwt :: Jwk -> ClientId -> Idp i -> IO (Either Text Jwt) mkOktaClientCredentialAppJwt jwk cid idp = do now <- getCurrentTime let cidStr = unClientId cid let payload = bsToStrict $ Aeson.encode $ Aeson.object [ "aud" .= idpTokenEndpoint idp , "exp" .= tToSeconds (addUTCTime (secondsToNominalDiffTime 300) now) -- 5 minutes expiration time , "iat" .= tToSeconds now , "iss" .= cidStr , "sub" .= cidStr ] first (TL.pack . show) <$> jwkEncode RS256 jwk (Claims payload) where tToSeconds = formatTime defaultTimeLocale "%s" data OktaUser = OktaUser { name :: Text , preferredUsername :: Text } deriving (Show, Generic) instance FromJSON OktaUser where parseJSON = genericParseJSON defaultOptions {fieldLabelModifier = camelTo2 '_'}