{-# LANGUAGE DuplicateRecordFields #-}

{- | JWT authentication support.

 Middlewares defined in this module add JWT authentication support to
 handlers. In most cases, you just need to use `JWTAuth` trait and
 `jwtAuth` middleware. The table below describes when to use other
 traits and middlewares.

 +----------+-------------+-----------------------+--------------------+
 | Type     | Auth Scheme | Trait                 | Middleware         |
 +----------+-------------+-----------------------+--------------------+
 | Required | Bearer      | 'JWTAuth'             | 'jwtAuth'          |
 +----------+-------------+-----------------------+--------------------+
 | Optional | Bearer      | 'JWTAuth'' 'Optional' | 'optionalJWTAuth'  |
 +----------+-------------+-----------------------+--------------------+
 | Required | Any scheme  | 'JWTAuth'' 'Required' | 'jwtAuth''         |
 +----------+-------------+-----------------------+--------------------+
 | Optional | Any scheme  | 'JWTAuth'' 'Optional' | 'optionalJWTAuth'' |
 +----------+-------------+-----------------------+--------------------+

 For example, given this handler:

 @
 myHandler :: ('Handler' h IO, 'HasTrait' ('JWTAuth' IO () 'JWT.ClaimsSet') req) => 'RequestHandler' h req
 myHandler = ....
 @

 and the following definitions:

 @
 authConfig :: 'JWTAuth' IO () 'JWT.ClaimsSet'
 authConfig = 'JWTAuth''
   { jwtValidationSettings = 'JWT.defaultJWTValidationSettings' (const True)
   , jwkSet = ....
   , toJWTAttribute = pure . Right
   }

 type ErrorTraits = [Status, RequiredHeader \"Content-Type\" Text, RequiredHeader \"WWW-Authenticate\" Text, Body Text]

 errorHandler :: ('Handler' h IO, Sets h ErrorTraits Response)
              => h (Linked req Request, 'JWTAuthError' e) Response
 errorHandler = 'respondUnauthorized' \"Bearer\" \"MyRealm\"
 @

 we can add JWT authentication to @myHandler@:

 @
 myHandlerWithAuth :: ('Handler' h IO, Get h ('JWTAuth' IO () 'JWT.ClaimsSet') Request, Sets h ErrorTraits Response)
                   => 'RequestHandler' h req
 myHandlerWithAuth = 'jwtAuth' authConfig errorHandler myHandler
 @

 The middlewares defined below take a 'JWTAuth'' parameter which has
 settings for validating a JWT. It also contains a function of type
 @'JWT.ClaimsSet' -> m (Either e a)@. This is used to convert the set
 of claims in the JWT to a value of type @a@ or fail with an error of
 type @e@. In this case @a@ is the type of the trait attribute and the
 next handler is invoked after this conversion.

 Middlewares marked as 'Required' take an additional error handling
 arrow as a parameter. This arrow is used when an error is encountered
 in authentication. This arrow receives the original request and a
 'JWTAuthError' as inputs and must produce a response as the output.

 Middlewares marked as 'Optional' do not have this additional error
 handling arrow. Instead, the trait attribute is of type @Either
 ('JWTAuthError' e) a@. The next handler will get the errors in this
 trait attribute and must handle it.
-}
module WebGear.Core.Trait.Auth.JWT (
  JWTAuth' (..),
  JWTAuth,
  Realm (..),
  JWTAuthError (..),
  jwtAuth,
  optionalJWTAuth,
  jwtAuth',
  optionalJWTAuth',
) where

import Control.Arrow (ArrowChoice, arr)
import qualified Crypto.JWT as JWT
import Data.Void (Void, absurd)
import GHC.TypeLits (Symbol)
import WebGear.Core.Handler
import WebGear.Core.Modifiers (Existence (..))
import WebGear.Core.Request (Request)
import WebGear.Core.Response (Response)
import WebGear.Core.Trait
import WebGear.Core.Trait.Auth.Common

{- | Trait for JWT authentication with a bearer token:
 https://tools.ietf.org/html/rfc6750

 This trait supports a custom scheme instead of the standard
 \"Bearer\" scheme.
-}
data JWTAuth' (x :: Existence) (scheme :: Symbol) m e a = JWTAuth'
  { -- | Settings to validate the JWT
    JWTAuth' x scheme m e a -> JWTValidationSettings
jwtValidationSettings :: JWT.JWTValidationSettings
  , -- | JWK to validate the JWT
    JWTAuth' x scheme m e a -> JWKSet
jwkSet :: JWT.JWKSet
  , -- | Convert the claims set to the trait attribute or an error
    JWTAuth' x scheme m e a -> ClaimsSet -> m (Either e a)
toJWTAttribute :: JWT.ClaimsSet -> m (Either e a)
  }

-- | Trait for JWT authentication with the \"Bearer\" scheme
type JWTAuth = JWTAuth' Required "Bearer"

-- | Error extracting a JWT from a request
data JWTAuthError e
  = JWTAuthHeaderMissing
  | JWTAuthSchemeMismatch
  | JWTAuthTokenBadFormat JWT.JWTError
  | JWTAuthAttributeError e
  deriving stock (JWTAuthError e -> JWTAuthError e -> Bool
(JWTAuthError e -> JWTAuthError e -> Bool)
-> (JWTAuthError e -> JWTAuthError e -> Bool)
-> Eq (JWTAuthError e)
forall e. Eq e => JWTAuthError e -> JWTAuthError e -> Bool
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
/= :: JWTAuthError e -> JWTAuthError e -> Bool
$c/= :: forall e. Eq e => JWTAuthError e -> JWTAuthError e -> Bool
== :: JWTAuthError e -> JWTAuthError e -> Bool
$c== :: forall e. Eq e => JWTAuthError e -> JWTAuthError e -> Bool
Eq, Int -> JWTAuthError e -> ShowS
[JWTAuthError e] -> ShowS
JWTAuthError e -> String
(Int -> JWTAuthError e -> ShowS)
-> (JWTAuthError e -> String)
-> ([JWTAuthError e] -> ShowS)
-> Show (JWTAuthError e)
forall e. Show e => Int -> JWTAuthError e -> ShowS
forall e. Show e => [JWTAuthError e] -> ShowS
forall e. Show e => JWTAuthError e -> String
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
showList :: [JWTAuthError e] -> ShowS
$cshowList :: forall e. Show e => [JWTAuthError e] -> ShowS
show :: JWTAuthError e -> String
$cshow :: forall e. Show e => JWTAuthError e -> String
showsPrec :: Int -> JWTAuthError e -> ShowS
$cshowsPrec :: forall e. Show e => Int -> JWTAuthError e -> ShowS
Show)

instance WebGear.Core.Trait.Trait (JWTAuth' Required scheme m e a) Request where
  type Attribute (JWTAuth' Required scheme m e a) Request = a

instance WebGear.Core.Trait.TraitAbsence (JWTAuth' Required scheme m e a) Request where
  type Absence (JWTAuth' Required scheme m e a) Request = JWTAuthError e

instance WebGear.Core.Trait.Trait (JWTAuth' Optional scheme m e a) Request where
  type Attribute (JWTAuth' Optional scheme m e a) Request = Either (JWTAuthError e) a

instance WebGear.Core.Trait.TraitAbsence (JWTAuth' Optional scheme m e a) Request where
  type Absence (JWTAuth' Optional scheme m e a) Request = Void

{- | Middleware to add JWT authentication protection for a
 handler. Expects the JWT to be available via a standard bearer
 authorization header in the format:

 > Authorization: Bearer <jwt>

 Example usage:

 > jwtAuth cfg errorHandler nextHandler

 The @errorHandler@ is invoked if the credentials are invalid or
 missing. The @nextHandler@ is invoked if the credentials were
 retrieved successfully.
-}
jwtAuth ::
  ( WebGear.Core.Trait.Get h (JWTAuth m e t) Request
  , ArrowChoice h
  ) =>
  -- | Authentication configuration
  JWTAuth m e t ->
  -- | Error handler
  h (WebGear.Core.Trait.Linked req Request, JWTAuthError e) Response ->
  Middleware h req (JWTAuth m e t : req)
jwtAuth :: JWTAuth m e t
-> h (Linked req Request, JWTAuthError e) Response
-> Middleware h req (JWTAuth m e t : req)
jwtAuth = forall e t (h :: * -> * -> *) (m :: * -> *) (req :: [*]).
(Get h (JWTAuth' 'Required "Bearer" m e t) Request,
 ArrowChoice h) =>
JWTAuth' 'Required "Bearer" m e t
-> h (Linked req Request, JWTAuthError e) Response
-> Middleware h req (JWTAuth' 'Required "Bearer" m e t : req)
forall (s :: Symbol) e t (h :: * -> * -> *) (m :: * -> *)
       (req :: [*]).
(Get h (JWTAuth' 'Required s m e t) Request, ArrowChoice h) =>
JWTAuth' 'Required s m e t
-> h (Linked req Request, JWTAuthError e) Response
-> Middleware h req (JWTAuth' 'Required s m e t : req)
jwtAuth' @"Bearer"

{- | Middleware to add optional JWT authentication protection for a
 handler. Expects the JWT to be available via a standard bearer
 authorization header in the format:

 > Authorization: Bearer <jwt>

 Example usage:

 > optionalJWTAuth cfg handler

 This middleware will not fail if authorization credentials are
 invalid or missing. Instead the trait attribute is of type @'Either'
 ('JWTAuthError' e) t@ so that the handler can process the
 authentication error appropriately.
-}
optionalJWTAuth ::
  ( WebGear.Core.Trait.Get h (JWTAuth' Optional "Bearer" m e t) Request
  , ArrowChoice h
  ) =>
  -- | Authentication configuration
  JWTAuth' Optional "Bearer" m e t ->
  Middleware h req (JWTAuth' Optional "Bearer" m e t : req)
optionalJWTAuth :: JWTAuth' 'Optional "Bearer" m e t
-> Middleware h req (JWTAuth' 'Optional "Bearer" m e t : req)
optionalJWTAuth = forall e t (h :: * -> * -> *) (m :: * -> *) (req :: [*]).
(Get h (JWTAuth' 'Optional "Bearer" m e t) Request,
 ArrowChoice h) =>
JWTAuth' 'Optional "Bearer" m e t
-> Middleware h req (JWTAuth' 'Optional "Bearer" m e t : req)
forall (s :: Symbol) e t (h :: * -> * -> *) (m :: * -> *)
       (req :: [*]).
(Get h (JWTAuth' 'Optional s m e t) Request, ArrowChoice h) =>
JWTAuth' 'Optional s m e t
-> Middleware h req (JWTAuth' 'Optional s m e t : req)
optionalJWTAuth' @"Bearer"

jwtAuthMiddleware ::
  forall s e t x h m req.
  ( WebGear.Core.Trait.Get h (JWTAuth' x s m e t) Request
  , ArrowChoice h
  ) =>
  JWTAuth' x s m e t ->
  h (WebGear.Core.Trait.Linked req Request, WebGear.Core.Trait.Absence (JWTAuth' x s m e t) Request) Response ->
  Middleware h req (JWTAuth' x s m e t : req)
jwtAuthMiddleware :: JWTAuth' x s m e t
-> h (Linked req Request, Absence (JWTAuth' x s m e t) Request)
     Response
-> Middleware h req (JWTAuth' x s m e t : req)
jwtAuthMiddleware JWTAuth' x s m e t
authCfg h (Linked req Request, Absence (JWTAuth' x s m e t) Request)
  Response
errorHandler RequestHandler h (JWTAuth' x s m e t : req)
nextHandler =
  proc Linked req Request
request -> do
    Either
  (Absence (JWTAuth' x s m e t) Request)
  (Linked (JWTAuth' x s m e t : req) Request)
result <- JWTAuth' x s m e t
-> h (Linked req Request)
     (Either
        (Absence (JWTAuth' x s m e t) Request)
        (Linked (JWTAuth' x s m e t : req) Request))
forall t (ts :: [*]) (h :: * -> * -> *) a.
Get h t a =>
t -> h (Linked ts a) (Either (Absence t a) (Linked (t : ts) a))
WebGear.Core.Trait.probe JWTAuth' x s m e t
authCfg -< Linked req Request
request
    case Either
  (Absence (JWTAuth' x s m e t) Request)
  (Linked (JWTAuth' x s m e t : req) Request)
result of
      Left Absence (JWTAuth' x s m e t) Request
err -> h (Linked req Request, Absence (JWTAuth' x s m e t) Request)
  Response
errorHandler -< (Linked req Request
request, Absence (JWTAuth' x s m e t) Request
err)
      Right Linked (JWTAuth' x s m e t : req) Request
val -> RequestHandler h (JWTAuth' x s m e t : req)
nextHandler -< Linked (JWTAuth' x s m e t : req) Request
val

{- | Middleware to add JWT authentication protection for a
 handler. Expects the JWT to be available via an authorization header
 in the format:

 > Authorization: <scheme> <jwt>

 Example usage:

 > jwtAuth' @"<scheme>" cfg errorHandler nextHandler

 The @errorHandler@ is invoked if the credentials are invalid or
 missing. The @nextHandler@ is invoked if the credentials were
 retrieved successfully.
-}
jwtAuth' ::
  forall s e t h m req.
  ( WebGear.Core.Trait.Get h (JWTAuth' Required s m e t) Request
  , ArrowChoice h
  ) =>
  -- | Authentication configuration
  JWTAuth' Required s m e t ->
  -- | Error handler
  h (WebGear.Core.Trait.Linked req Request, JWTAuthError e) Response ->
  Middleware h req (JWTAuth' Required s m e t : req)
jwtAuth' :: JWTAuth' 'Required s m e t
-> h (Linked req Request, JWTAuthError e) Response
-> Middleware h req (JWTAuth' 'Required s m e t : req)
jwtAuth' = JWTAuth' 'Required s m e t
-> h (Linked req Request, JWTAuthError e) Response
-> Middleware h req (JWTAuth' 'Required s m e t : req)
forall (s :: Symbol) e t (x :: Existence) (h :: * -> * -> *)
       (m :: * -> *) (req :: [*]).
(Get h (JWTAuth' x s m e t) Request, ArrowChoice h) =>
JWTAuth' x s m e t
-> h (Linked req Request, Absence (JWTAuth' x s m e t) Request)
     Response
-> Middleware h req (JWTAuth' x s m e t : req)
jwtAuthMiddleware

{- | Middleware to add JWT authentication protection for a
 handler. Expects the JWT to be available via an authorization header
 in the format:

 > Authorization: <scheme> <jwt>

 Example usage:

 > optionalJWTAuth' @"<scheme>" cfg nextHandler

 This middleware will not fail if authorization credentials are
 invalid or missing. Instead the trait attribute is of type @'Either'
 ('JWTAuthError' e) t@ so that the handler can process the
 authentication error appropriately.
-}
optionalJWTAuth' ::
  forall s e t h m req.
  ( WebGear.Core.Trait.Get h (JWTAuth' Optional s m e t) Request
  , ArrowChoice h
  ) =>
  -- | Authentication configuration
  JWTAuth' Optional s m e t ->
  Middleware h req (JWTAuth' Optional s m e t : req)
optionalJWTAuth' :: JWTAuth' 'Optional s m e t
-> Middleware h req (JWTAuth' 'Optional s m e t : req)
optionalJWTAuth' JWTAuth' 'Optional s m e t
cfg = JWTAuth' 'Optional s m e t
-> h (Linked req Request,
      Absence (JWTAuth' 'Optional s m e t) Request)
     Response
-> Middleware h req (JWTAuth' 'Optional s m e t : req)
forall (s :: Symbol) e t (x :: Existence) (h :: * -> * -> *)
       (m :: * -> *) (req :: [*]).
(Get h (JWTAuth' x s m e t) Request, ArrowChoice h) =>
JWTAuth' x s m e t
-> h (Linked req Request, Absence (JWTAuth' x s m e t) Request)
     Response
-> Middleware h req (JWTAuth' x s m e t : req)
jwtAuthMiddleware JWTAuth' 'Optional s m e t
cfg (h (Linked req Request,
    Absence (JWTAuth' 'Optional s m e t) Request)
   Response
 -> Middleware h req (JWTAuth' 'Optional s m e t : req))
-> h (Linked req Request,
      Absence (JWTAuth' 'Optional s m e t) Request)
     Response
-> Middleware h req (JWTAuth' 'Optional s m e t : req)
forall a b. (a -> b) -> a -> b
$ ((Linked req Request, Void) -> Response)
-> h (Linked req Request, Void) Response
forall (a :: * -> * -> *) b c. Arrow a => (b -> c) -> a b c
arr (Void -> Response
forall a. Void -> a
absurd (Void -> Response)
-> ((Linked req Request, Void) -> Void)
-> (Linked req Request, Void)
-> Response
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Linked req Request, Void) -> Void
forall a b. (a, b) -> b
snd)