packages feed

minion-jwt-0.1.0.0: src/Web/Minion/Auth/Jwt.hs

module Web.Minion.Auth.Jwt where

import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Trans.Maybe (MaybeT (..))
import Crypto.JOSE qualified as Jose
import Crypto.JWT (JWTError)
import Crypto.JWT qualified as Jose
import Data.Aeson (FromJSON (..))
import Data.ByteString qualified as Bytes
import Data.ByteString.Lazy qualified as Bytes.Lazy
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Time qualified as Time
import Network.HTTP.Types.Header qualified as Http
import Network.Wai qualified as Wai
import Web.Minion

data JwtAuthSettings m payload a = JwtAuthSettings
  { getNow :: m Time.UTCTime
  , jwk :: m Jose.JWK
  , validationSettings :: m Jose.JWTValidationSettings
  , check :: MakeError -> Either JWTError (JwtPayload payload) -> m (AuthResult a)
  }

defaultJwtAuthSettings ::
  (MonadIO m) =>
  m Jose.JWK ->
  -- | Audience predicate
  (Jose.StringOrURI -> Bool) ->
  (MakeError -> Either JWTError (JwtPayload payload) -> m (AuthResult a)) ->
  JwtAuthSettings m payload a
defaultJwtAuthSettings jwk audCheck check =
  JwtAuthSettings
    { getNow = liftIO Time.getCurrentTime
    , jwk = jwk
    , validationSettings = pure (Jose.defaultJWTValidationSettings audCheck)
    , check = check
    }

data JwtPayload a = JwtPayload
  { claims :: Jose.ClaimsSet
  , payload :: a
  }

instance Jose.HasClaimsSet (JwtPayload a) where
  claimsSet f JwtPayload{..} = f claims <&> \c -> JwtPayload{claims = c, ..}

instance (FromJSON a) => FromJSON (JwtPayload a) where
  parseJSON v =
    JwtPayload
      <$> parseJSON v
      <*> parseJSON v

data Bearer payload

instance (MonadIO m, FromJSON payload) => IsAuth (Bearer payload) m a where
  type Settings (Bearer payload) m a = JwtAuthSettings m payload a
  toAuth JwtAuthSettings{..} buildError req = do
    jwk_ <- jwk
    now <- getNow
    settings <- validationSettings
    payload <- Jose.runJOSE $ runMaybeT do
      authHeader <- Wai.requestHeaders req & lookup Http.hAuthorization & hoistMaybe
      compact <- hoistMaybe $ Bytes.stripPrefix prefix authHeader
      jwt <- Jose.decodeCompact $ Bytes.Lazy.fromStrict compact
      Jose.verifyJWTAt settings jwk_ now jwt
    case payload of
      Left e -> check (buildError req) (Left e)
      Right Nothing -> pure Indefinite
      Right (Just (v :: JwtPayload payload)) -> check (buildError req) (Right v)
   where
    prefix = "Bearer "

    hoistMaybe = MaybeT . pure