wai-middleware-auth-0.2.3.0: test/Network/Wai/Auth/Test.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Network.Wai.Auth.Test
(ChangeProvider
, FakeProviderConf(..)
, fakeProvider
, const200
, get
) where
import Control.Monad.IO.Class (liftIO)
import Data.ByteString (ByteString)
import qualified Data.IORef as IORef
import qualified Crypto.JOSE as JOSE
import qualified Crypto.JWT as JWT
import qualified Control.Monad.Except
import qualified Data.Aeson as Aeson
import Data.Function ((&))
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import qualified Data.Time.Clock as Clock
import GHC.Exts (fromString)
import qualified Network.HTTP.Types.Status as Status
import qualified Network.OAuth.OAuth2 as OA2
import qualified Network.Wai as Wai
import Network.Wai.Auth.Internal (Metadata(..))
import Network.Wai.Test (Session, SResponse,
defaultRequest,
request, setPath)
import qualified Lens.Micro as Lens
import qualified URI.ByteString as U
get :: ByteString -> Session SResponse
get = request . setPath defaultRequest
const200 :: Wai.Application
const200 _ respond = respond $ Wai.responseLBS Status.ok200 [] ""
data FakeProviderConf
= FakeProviderConf
{ jwtExpiresIn :: Clock.NominalDiffTime,
jwtAudience :: JWT.StringOrURI,
jwtIssuer :: T.Text,
jwtJWK :: JOSE.JWK,
jwtSub :: String,
accessTokenExpiresIn :: Int,
returnIdToken :: Bool,
returnRefreshToken :: Bool
}
defaultConfig :: IO FakeProviderConf
defaultConfig = do
jwk <- JOSE.genJWK (JOSE.RSAGenParam 256)
pure
FakeProviderConf
{ jwtExpiresIn = 600,
jwtAudience = "client-id",
jwtIssuer = "test-oidc-provider",
jwtJWK = jwk,
jwtSub = "1234",
accessTokenExpiresIn = 600,
returnIdToken = True,
returnRefreshToken = True
}
type ChangeProvider = (FakeProviderConf -> FakeProviderConf) -> Session ()
fakeProvider :: IO (Wai.Application, ChangeProvider)
fakeProvider = do
config <- defaultConfig
configRef <- IORef.newIORef config
let changeProvider = IORef.modifyIORef configRef
pure (fakeProvider' configRef, liftIO . changeProvider)
fakeProvider' :: IORef.IORef FakeProviderConf -> Wai.Application
fakeProvider' configRef req respond = do
config <- IORef.readIORef configRef
case Wai.pathInfo req of
[".well-known", "openid-configuration"] ->
case TE.decodeUtf8 <$> Wai.requestHeaderHost req of
Nothing ->
Wai.responseLBS Status.badRequest400 [] ""
& respond
Just host ->
Metadata
{ issuer = jwtIssuer config,
authorizationEndpoint = parseURI ("http://" <> host <> "/authorize"),
tokenEndpoint = parseURI ("http://" <> host <> "/token"),
userinfoEndpoint = Nothing,
revocationEndpoint = Nothing,
jwksUri = "http://" <> host <> "/jwks",
responseTypesSupported = ["code"],
subjectTypesSupported = ["public"],
idTokenSigningAlgValuesSupported = ["RS256"],
scopesSupported = Just ["openid"],
tokenEndpointAuthMethodsSupported = Just ["client_secret_basic"],
claimsSupported = Just ["iss", "sub", "aud", "exp", "iat"]
}
& Aeson.encode
& Wai.responseLBS Status.ok200 [("Content-Type", "application/json")]
& respond
["jwks"] ->
JOSE.JWKSet [jwtJWK config]
& Aeson.encode
& Wai.responseLBS Status.ok200 [("Content-Type", "application/json")]
& respond
["token"] -> do
now <- Clock.getCurrentTime
let claims =
JWT.emptyClaimsSet
& Lens.set JWT.claimIss (Just (fromString (T.unpack (jwtIssuer config))))
& Lens.set JWT.claimAud (Just (JWT.Audience [jwtAudience config]))
& Lens.set JWT.claimIat (Just (JWT.NumericDate now))
& Lens.set JWT.claimExp (Just (JWT.NumericDate (Clock.addUTCTime (jwtExpiresIn config) now)))
& Lens.set JWT.claimSub (Just (fromString (jwtSub config)))
idToken <- doJwtSign (jwtJWK config) claims
OA2.OAuth2Token
{ OA2.accessToken = OA2.AccessToken "access-granted",
OA2.refreshToken =
if returnRefreshToken config
then Just (OA2.RefreshToken "refresh-token")
else Nothing,
OA2.expiresIn = Just (accessTokenExpiresIn config),
OA2.tokenType = Nothing,
OA2.idToken =
if returnIdToken config
then Just (OA2.IdToken idToken)
else Nothing
}
& Aeson.encode
& Wai.responseLBS Status.ok200 [("Content-Type", "application/json")]
& respond
_ ->
Wai.responseLBS Status.notFound404 [] ""
& respond
doJwtSign :: JOSE.JWK -> JWT.ClaimsSet -> IO T.Text
doJwtSign jwk claims = do
result <- Control.Monad.Except.runExceptT $ do
alg <- JOSE.bestJWSAlg jwk
JWT.signClaims jwk (JOSE.newJWSHeader ((), alg)) claims
case result of
Left (err :: JOSE.Error) -> fail (show err)
Right bytestring ->
JOSE.encodeCompact bytestring
& TLE.decodeUtf8
& TL.toStrict
& pure
parseURI :: T.Text -> U.URIRef U.Absolute
parseURI uri =
TE.encodeUtf8 uri
& U.parseURI U.laxURIParserOptions
& either (error . show) id