libjwt-typed-0.1: src/Libjwt/Payload.hs
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
{-# OPTIONS_HADDOCK show-extensions #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- | JWT payload structure and convenient builders.
module Libjwt.Payload
( Payload(..)
, withIssuer
, issuedBy
, withSubject
, issuedTo
, withRecipient
, intendedFor
, withAudience
, setTtl
, expiresAt
, notBefore
, notBeforeNow
, notUntil
, issuedNow
, withJwtId
, JwtBuilder
, jwtPayload
)
where
import Libjwt.Encoding
import Libjwt.Decoding
import Libjwt.NumericDate
import Libjwt.RegisteredClaims
import Libjwt.PrivateClaims
import Control.Monad.Time
import Control.Monad.Trans.Reader
import Data.Default
import Data.Function ( (&) )
import Data.Monoid
import Data.Time.Clock
import Data.UUID ( UUID )
import Prelude hiding ( exp )
-- | JWT payload representation
data Payload pc ns = ClaimsSet { iss :: Iss -- ^ /iss/ (Issuer) claim
, sub :: Sub -- ^ /sub/ (Subject) claim
, aud :: Aud -- ^ /aud/ (Audience) claim
, exp :: Exp -- ^ /exp/ (Expiration Time) claim
, nbf :: Nbf -- ^ /nbf/ (Not Before) claim
, iat :: Iat -- ^ /iat/ (Issued At) claim
, jti :: Jti -- ^ /jti/ (JWT ID) claim
, privateClaims :: PrivateClaims pc ns -- ^ private claims
}
deriving stock instance Show (PrivateClaims pc ns) => Show (Payload pc ns)
deriving stock instance Eq (PrivateClaims pc ns) => Eq (Payload pc ns)
instance (pc ~ Empty, ns ~ 'NoNs) => Default (Payload pc ns) where
def = ClaimsSet { iss = def
, sub = def
, aud = mempty
, exp = def
, nbf = def
, iat = def
, jti = def
, privateClaims = def
}
instance Encode (PrivateClaims pc ns) => Encode (Payload pc ns) where
encode ClaimsSet { iss, sub, aud, exp, nbf, iat, jti, privateClaims } jwt =
encode iss jwt
>> encode sub jwt
>> encode aud jwt
>> encode exp jwt
>> encode nbf jwt
>> encode iat jwt
>> encode jti jwt
>> encode privateClaims jwt
instance Decode (PrivateClaims pc ns) => Decode (Payload pc ns) where
decode jwt =
ClaimsSet
<$> decode jwt
<*> decode jwt
<*> decode jwt
<*> decode jwt
<*> decode jwt
<*> decode jwt
<*> decode jwt
<*> decode jwt
newtype JwtBuilder any1 any2 = JwtBuilder { steps :: Ap (Reader UTCTime) (Endo (Payload any1 any2)) }
deriving newtype (Semigroup, Monoid)
-- | Create a payload from the builder and the value representing private claims
--
-- For example:
--
-- @
-- jwtPayload
-- ('withIssuer' "myApp" <> 'withRecipient' "https://myApp.com" <> 'setTtl' 300)
-- ( #userName v'->>' "John Doe"
-- , #isRoot v'->>' False
-- , #userId v'->>' (12345 :: Int)
-- )
-- @
--
-- The resulting payload will be the equivalent of:
--
-- > {
-- > "aud": [
-- > "https://myApp.com"
-- > ],
-- > "exp": 1599499073,
-- > "iat": 1599498773,
-- > "isRoot": false,
-- > "iss": "myApp",
-- > "userId": 12345,
-- > "userName": "JohnDoe"
-- > }
--
-- An identical payload can be constructed from the following record type:
--
-- @
-- data MyClaims = MyClaims { userName :: String
-- , isRoot :: Bool
-- , userId :: Int
-- }
-- deriving stock (Eq, Show, Generic)
--
-- instance 'ToPrivateClaims' UserClaims
--
-- jwtPayload
-- ('withIssuer' "myApp" <> 'withRecipient' "https://myApp.com" <> 'setTtl' 300)
-- MyClaims { userName = "John Doe"
-- , isRoot = False
-- , userId = 12345
-- }
-- @
--
-- If you want to assign a /namespace/ to your private claims, you can do:
--
-- @
-- jwtPayload
-- (withIssuer "myApp" <> withRecipient "https://myApp.com" <> setTtl 300)
-- $ 'withNs'
-- ('Ns' @"https://myApp.com")
-- MyClaims
-- { userId = 12345
-- , userName = "JohnDoe"
-- , isRoot = False
-- }
-- @
--
-- The resulting payload will be the equivalent of:
--
-- > {
-- > "aud": [
-- > "https://myApp.com"
-- > ],
-- > "exp": 1599499073,
-- > "iat": 1599498773,
-- > "https://myApp.com/isRoot": false,
-- > "iss": "myApp",
-- > "https://myApp.com/userId": 12345,
-- > "https://myApp.com/userName": "JohnDoe"
-- > }
jwtPayload
:: (MonadTime m, ToPrivateClaims a, Claims a ~ b, OutNs a ~ ns)
=> JwtBuilder b ns
-> a
-> m (Payload b ns)
jwtPayload builder a =
(&) initial . appEndo . runReader (getAp $ steps builder) <$> currentTime
where initial = def { privateClaims = toPrivateClaims a }
step :: (Payload any1 any2 -> Payload any1 any2) -> JwtBuilder any1 any2
step = JwtBuilder . Ap . pure . Endo
stepWithCurrentTime
:: (NumericDate -> Payload any1 any2 -> Payload any1 any2)
-> JwtBuilder any1 any2
stepWithCurrentTime f = JwtBuilder . Ap $ fmap (Endo . f) now
-- | Set /iss/ claim
withIssuer :: String -> JwtBuilder any1 any2
withIssuer issuer = step $ \p -> p { iss = Iss $ Just issuer }
-- | Set /iss/ claim
issuedBy :: String -> JwtBuilder any1 any2
issuedBy = withIssuer
-- | Set /sub/ claim
withSubject :: String -> JwtBuilder any1 any2
withSubject subject = step $ \p -> p { sub = Sub $ Just subject }
-- | Set /sub/ claim
issuedTo :: String -> JwtBuilder any1 any2
issuedTo = withSubject
-- | Append one item to /aud/ claim
withRecipient :: String -> JwtBuilder any1 any2
withRecipient recipient = step $ \p -> p { aud = Aud [recipient] <> aud p }
-- | Append one item to /aud/ claim
intendedFor :: String -> JwtBuilder any1 any2
intendedFor = withRecipient
-- | Set /aud/ claim
withAudience :: [String] -> JwtBuilder any1 any2
withAudience audience = step $ \p -> p { aud = Aud audience }
-- | Set /exp/ claim
expiresAt :: UTCTime -> JwtBuilder any1 any2
expiresAt time = step $ \p -> p { exp = Exp $ Just $ fromUTC time }
-- | Set /nbf/ claim
notBefore :: UTCTime -> JwtBuilder any1 any2
notBefore time = step $ \p -> p { nbf = Nbf $ Just $ fromUTC time }
-- | Set /nbf/ claim to 'currentTime'
notBeforeNow :: JwtBuilder any1 any2
notBeforeNow = stepWithCurrentTime $ \t p -> p { nbf = Nbf $ Just t }
-- | Set /nbf/ claim to 'currentTime' plus the argument
notUntil :: NominalDiffTime -> JwtBuilder any1 any2
notUntil s =
stepWithCurrentTime $ \t p -> p { nbf = Nbf $ Just $ t `plusSeconds` s }
-- | Set /iat/ claim to 'currentTime'
issuedNow :: JwtBuilder any1 any2
issuedNow = stepWithCurrentTime $ \t p -> p { iat = Iat $ Just t }
-- | Set /iat/ claim to 'currentTime' and /exp/ claim to 'currentTime' plus the argument
setTtl :: NominalDiffTime -> JwtBuilder any1 any2
setTtl ttl = issuedNow <> stepWithCurrentTime
(\t p -> p { exp = Exp $ Just $ t `plusSeconds` ttl })
-- | Set /jti/ claim
withJwtId :: UUID -> JwtBuilder any1 any2
withJwtId jwtId = step $ \p -> p { jti = Jti $ Just jwtId }