packages feed

wai-middleware-auth-0.2.3.0: test/Spec/Network/Wai/Middleware/Auth/OIDC.hs

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Spec.Network.Wai.Middleware.Auth.OIDC (tests) where

import           Control.Monad                          (void)
import           Control.Monad.IO.Class                 (liftIO)
import qualified Crypto.JOSE                            as JOSE
import           Data.Function                          ((&))
import qualified Data.Text                              as T
import qualified Data.Text.Encoding                     as TE
import           GHC.Exts                               (fromList, fromString)
import qualified Network.HTTP.Types.Status              as Status
import qualified Network.Wai                            as Wai
import           Network.Wai.Auth.Test                  (ChangeProvider,
                                                         FakeProviderConf(..),
                                                         fakeProvider,
                                                         const200, get)
import qualified Network.Wai.Handler.Warp               as Warp
import qualified Network.Wai.Middleware.Auth            as Auth
import           Network.Wai.Middleware.Auth.OIDC 
import           Network.Wai.Middleware.Auth.Provider   (Provider(..))
import           Network.Wai.Test                       (Session, assertHeader,
                                                         assertStatus,
                                                         runSession,
                                                         setClientCookie)
import           Test.Tasty                             (TestTree, testGroup)
import           Test.Tasty.HUnit                       (testCase)
import qualified Web.Cookie                             as Cookie
import qualified Web.ClientSession

tests :: TestTree
tests = testGroup "Network.Wai.Auth.OIDC"
  [ testCase "when a request without a session is made then redirect to re-authorize" $
      runSessionWithProvider const200 $ \host _ -> do
        redirect1 <- get "/hi"
        assertStatus 303 redirect1
        assertHeader "Location" "/prefix" redirect1
        redirect2 <- get "/prefix"
        assertStatus 303 redirect2
        assertHeader "location" "/prefix/oidc" redirect2
        redirect3 <- get "/prefix/oidc"
        assertStatus 303 redirect3
        assertHeader
          "location"
          (TE.encodeUtf8 host <> "/authorize?scope=openid%2Cscope1&client_id=client-id&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2Fprefix%2Foidc%2Fcomplete")
          redirect3

  , testCase "when a request is made with a valid session then pass the request through" $
      runSessionWithProvider const200 $ \_ _ -> do
        createSession
        response <- get "/some/endpoint"
        assertStatus 200 response

  , testCase "when an ID token expired and no refresh token is available then redirect to re-authorize" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        changeProvider (\c -> c { jwtExpiresIn = -600, returnRefreshToken = False })
        createSession
        response <- get "/some/endpoint"
        assertStatus 303 response

  , testCase "when an ID token expired then use a refresh token" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        changeProvider (\c -> c { jwtExpiresIn = -600 })
        createSession
        changeProvider (\c -> c { jwtExpiresIn = 600 })
        response <- get "/some/endpoint"
        assertStatus 200 response

  , testCase "when a request is made with an invalid session redirect to re-authorize" $ 
      runSessionWithProvider const200 $ \_ _ -> do
        -- First create a known valid session, so we can see that it's the act
        -- of corrupting it that makes the test fail.
        createSession
        setClientCookie
          Cookie.defaultSetCookie
            { Cookie.setCookieName = "auth-cookie"
            , Cookie.setCookieValue = "garbage"
            }
        response <- get "/some/endpoint"
        assertStatus 303 response

  , testCase "when a request is made to the complete endpoint then create a session" $
      runSessionWithProvider const200 $ \_ _ -> do
        response <- get "/prefix/oidc/complete?code=1234"
        assertStatus 303 response
        assertHeader "location" "/" response

  , testCase "when a request with a valid session is made then the app can access the access token" $
      let app req respond = 
            case getAccessToken req of
              Nothing -> respond $ Wai.responseLBS Status.badRequest400 [] ""
              Just _ -> respond $ Wai.responseLBS Status.ok200 [] ""
      in runSessionWithProvider app $ \_ _ -> do
          createSession
          response <- get "/some/endpoint"
          assertStatus 200 response

  , testCase "when a request with a valid session is made then the app can access the id token" $
      let app req respond = 
            case getIdToken req of
              Nothing -> respond $ Wai.responseLBS Status.badRequest400 [] ""
              Just _ -> respond $ Wai.responseLBS Status.ok200 [] ""
      in runSessionWithProvider app $ \_ _ -> do
          createSession
          response <- get "/some/endpoint"
          assertStatus 200 response

  , testCase "when an ID token has an invalid audience then redirect to re-authorize" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        changeProvider (\c -> c { jwtAudience = fromString "wrong-audience" })
        createSession
        response <- get "/some/endpoint"
        assertStatus 303 response

  , testCase "when an ID token has an invalid issuer then redirect to re-authorize" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        changeProvider (\c -> c { jwtIssuer = "wrong-issuer" })
        createSession
        response <- get "/some/endpoint"
        assertStatus 303 response

  , testCase "when a session does not contain an ID token then redirect to re-authorize" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        changeProvider (\c -> c { returnIdToken = False })
        createSession
        response <- get "/some/endpoint"
        assertStatus 303 response

  , testCase "when an ID token has an invalid signature then redirect to re-authorize" $
      runSessionWithProvider const200 $ \_ changeProvider -> do
        newJWK <- liftIO $ JOSE.genJWK (JOSE.RSAGenParam 256)
        changeProvider (\c -> c { jwtJWK = newJWK })
        createSession
        response <- get "/some/endpoint"
        assertStatus 303 response
  ]

createSession :: Session ()
createSession = void $ get "/prefix/oidc/complete?code=1234"

runSessionWithProvider :: Wai.Application -> (T.Text -> ChangeProvider -> Session a) -> IO a
runSessionWithProvider app session = do
  (provider, changeProvider) <- fakeProvider
  Warp.testWithApplication (pure provider) $ \port -> do
    let host = "http://localhost:" <> T.pack (show port)
    middleware <- Auth.mkAuthMiddleware =<< authSettings host
    let app' = middleware app
    runSession (session host changeProvider) app'

authSettings :: T.Text -> IO Auth.AuthSettings
authSettings host = do
  oidc' <- discover host
  let oidc =
        oidc'
          { oidcClientId = "client-id"
          , oidcClientSecret = "client-secret"
          , oidcScopes = ["openid", "scope1"]
          }
  pure $ Auth.defaultAuthSettings
    & Auth.setAuthProviders (fromList [("oidc", Provider oidc)])
    & Auth.setAuthPrefix "prefix"
    & Auth.setAuthCookieName "auth-cookie"
    & Auth.setAuthKey (snd <$> Web.ClientSession.randomKey)