Copyright | (c) 2020 Marcin Rzeźnicki |
---|---|
License | MPL-2.0 |
Maintainer | Marcin Rzeźnicki <marcin.rzeznicki@gmail.com> |
Safe Haskell | None |
Language | Haskell2010 |
The prelude for the library.
Creating a payload
Payload
consists of:
Private claims can be created from:
- "named" tuples (tuples with elements created via
->>
) - records that are instances of
ToPrivateClaims
Public claims can be created:
- directly, by setting fields of
Payload
record - via
JwtBuilder
Payload keeps track of names and types of private claims as a part of its type. In all the examples below the type is:
Payload
'["user_name"->>
String, "is_root"->>
Bool, "user_id"->>
Int] 'NoNs
From "named" tuples
mkPayload currentTime = let now =fromUTC
currentTime in def { iss =Iss
(Just "myApp") , aud =Aud
["https://myApp.com"] , iat =Iat
(Just now) , exp =Exp
(Just $ nowplusSeconds
300) , privateClaims =toPrivateClaims
( #user_name->>
"John Doe" , #is_root->>
False , #user_id->>
(12345 :: Int) ) }
From records
data UserClaims = UserClaims { user_name :: String , is_root :: Bool , user_id :: Int } deriving stock (Eq, Show, Generic) instanceToPrivateClaims
UserClaims mkPayload currentTime = let now =fromUTC
currentTime in def { iss = Iss (Just "myApp") , aud = Aud ["https://myApp.com"] , iat = Iat (Just now) , exp = Exp (Just $ nowplusSeconds
300) , privateClaims =toPrivateClaims
UserClaims { user_name = "John Doe" , is_root = False , user_id = 12345 } }
Using JwtBuilder
If you prefer more "fluent" style, you might want to use jwtPayload
function
mkPayload =jwtPayload
(withIssuer
"myApp" <>withRecipient
"https://myApp.com" <>setTtl
300) UserClaims { user_name = "John Doe" , is_root = False , user_id = 12345 }
For the list of available "builders", please see the docs of Libjwt.Payload module. This methods relies on Control.Monad.MonadTime to get the current time.
Namespaces
To ensure that private do not collide with claims from other resources, it is recommended to give them globally unique names . This is often done through namespacing, i.e. prefixing the names with the URI of a resource you control. This is handled entirely at the type-level.
As you may have noticed, Payload
types has a component of kind Namespace
.
It tracks the namespace assigned to private claims within the payload. If you change the last example to:
mkPayload' = jwtPayload (withIssuer "myApp" <> withRecipient "https://myApp.com" <> setTtl 300) $withNs
(Ns
@"https://myApp.com") UserClaims { user_name = "John Doe" , is_root = False , user_id = 12345 }
, you'll notice that the type has changed to accomodate the namespace, becoming
Payload
'["user_name"->>
String, "is_root"->>
Bool, "user_id"->>
Int] ('SomeNs
"https://myApp.com")
Consequently, in the generated token "user_id" becomes "https:myApp.comuser_id"/ etc.
Signing
Signing is the process of transforming the Jwt
structure with Payload
and Header
into a token with a cryptographic signature that can be sent over-the-wire.
Supported algorithms
To sign a token, you need to choose the algorithm.
Algorithm | Description |
---|---|
HS256 | HMAC with SHA-256 |
HS384 | HMAC with SHA-384 |
HS512 | HMAC with SHA-512 |
RS256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA with curve P-521 and SHA-512 |
The complete example:
hmac512 ::Alg
hmac512 =HS512
"MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\ \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\ \Y2IwMDZhYWY1MjY1OTQgIC0K" token :: IO ByteString token = fmap (getToken
.sign
hmac512) $ jwtPayload (withIssuer "myApp" <> withRecipient "https://myApp.com" <> setTtl 300) UserClaims { user_name = "John Doe" , is_root = False , user_id = 12345 }
Decoding
Decoding is a 2-step process. Step 1 is to take the token, validate its signature and check its structural correctness
(is it valid JSON, is it a valid JWT object, does it have all the claims?). If any of these tests fail,
we don't have a valid token and an exception is thrown (see SomeDecodeException
). In step 2, the decoded token is validated -
has it expired? does it have the right issuer? etc. The resulting value is of type ValidationNEL
ValidationFailure
(Validated
MyJwtType)
It is important to only work with valid tokens (if a token is not validated, it may be addressed to someone else or may be 2 weeks old),
so the rest of your program should only accept
, not Validated
MyJwt
, which is the result of step 1.Decoded
MyJwt
type MyJwt =Jwt
'["userId"->>
UUID, "userName"->>
Text, "isRoot"->>
Bool, "createdAt"->>
UTCTime, "accounts"->>
NonEmpty UUID] 'NoNs
decodeAndValidate :: IO (ValidationNEL
ValidationFailure
(Validated
MyJwt)) decodeAndValidate =jwtFromByteString
settings mempty hmac512 =<< token where settings =Settings
{ leeway = 5, appName = Just "https://myApp.com" }
By default only validations mandated by the RFC are performed:
- check exp claim against the current time,
- check nbf claim against the current time,
- check aud claim against
appName
You can add your own validations:
decodeAndValidate :: IO (ValidationNEL
ValidationFailure
(Validated
MyJwt)) decodeAndValidate =jwtFromByteString
settings (checkIssuer
"myApp" <>checkClaim
not #is_root) hmac512 =<< token where settings =Settings
{ leeway = 5, appName = Just "https://myApp.com" }
If for some reason, you do not want to validate a token, but only decode it, you can use decodeByteString
Types supported in claims
Currently, these types are supported:
- ByteString
- String
- Text
ASCII
JsonByteString
- Bool
NumericDate
Flag
- Int
- UUID
- UTCTime, ZonedTime, LocalTime, Day
- Maybes of the above type
- lists of the above types and lists of tuples created from them
- NonEmpty lists of the above types
If you want to support a different type, check out Libjwt.Classes. If you want to work with aeson, check Libjwt.JsonByteString
Synopsis
- data Validated t
- data Decoded t
- data Encoded t
- data Jwt pc ns = Jwt {}
- sign :: Encode (PrivateClaims pc ns) => Alg -> Payload pc ns -> Encoded (Jwt pc ns)
- signJwt :: Encode (PrivateClaims pc ns) => Jwt pc ns -> Encoded (Jwt pc ns)
- decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> String -> m (Decoded (Jwt pc ns))
- decodeByteString :: forall ns pc m. (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> ByteString -> m (Decoded (Jwt pc ns))
- validateJwt :: MonadTime m => ValidationSettings -> JwtValidation pc ns -> Decoded (Jwt pc ns) -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- jwtFromByteString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> ByteString -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- module Libjwt.Exceptions
- module Libjwt.Header
- module Libjwt.Keys
- module Libjwt.Payload
- module Libjwt.RegisteredClaims
- module Libjwt.PrivateClaims
- data JwtValidation pc any
- data ValidationFailure
- data ValidationSettings = Settings {}
- type ValidationNEL a b = Validation (NonEmpty a) b
- defaultValidationSettings :: ValidationSettings
- check :: String -> (a -> Bool) -> (Payload pc any -> a) -> JwtValidation pc any
- checkIssuer :: String -> JwtValidation any1 any2
- checkSubject :: String -> JwtValidation any1 any2
- checkAge :: NominalDiffTime -> JwtValidation any1 any2
- checkIssuedAfter :: UTCTime -> JwtValidation any1 any2
- checkJwtId :: UUID -> JwtValidation any1 any2
- checkClaim :: (CanGet n pc, a ~ LookupClaimType n pc) => (a -> Bool) -> ClaimName n -> JwtValidation pc any
- newtype NumericDate = NumericDate {}
- fromPOSIX :: POSIXTime -> NumericDate
- fromUTC :: UTCTime -> NumericDate
- plusSeconds :: NumericDate -> NominalDiffTime -> NumericDate
- newtype ASCII = ASCII {}
- class AFlag a where
- getFlagValue :: a -> ASCII
- setFlagValue :: ASCII -> Maybe a
- newtype Flag a = Flag {
- getFlag :: a
- class Encode c
- class Decode c
Documentation
Successfully validated value of type t
Decoded value of type t
Instances
base64url-encoded value of type t
JSON Web Token representation
Instances
Eq (PrivateClaims pc ns) => Eq (Jwt pc ns) Source # | |
Show (PrivateClaims pc ns) => Show (Jwt pc ns) Source # | |
Encode (PrivateClaims pc ns) => Encode (Jwt pc ns) Source # | |
Defined in Libjwt.Jwt |
decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> String -> m (Decoded (Jwt pc ns)) Source #
See decodeByteString
decodeByteString :: forall ns pc m. (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> ByteString -> m (Decoded (Jwt pc ns)) Source #
Parse the base64url-encoded representation to extract the serialized values for the components of the JWT. Verify that:
token
is a valid UTF-8 encoded representation of a completely valid JSON object,- input JWT signature matches,
- the correct algorithm was used,
- all required fields are present.
If steps 1-2 are unuccessful, DecodeException
will be thrown.
If step 3 fails, AlgorithmMismatch
will be thrown.
If the last step fails, MissingClaim
will be thrown.
:: MonadTime m | |
=> ValidationSettings | |
-> JwtValidation pc ns | additional validation rules |
-> Decoded (Jwt pc ns) | decoded token |
-> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns))) |
Accept or reject successfully decoded JWT value. In addition to the default rules mandated by the RFC, the application can add its own rules.
The default rules are:
- check
exp
claim to see if the current time is before the expiration time, - check
nbf
claim to see if the current time is after or equal the not-before time, - check
aud
claim if the application identifies itself with a value in theaud
list (if present)
You may allow a little leeway
when checking time-based claims.
jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns))) Source #
:: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) | |
=> ValidationSettings | |
-> JwtValidation pc ns | additional validation rules |
-> Alg | algorithm used to verify the signature |
-> ByteString | base64url-encoded representation (a token) |
-> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns))) |
jwtFromByteString =validateJwt
settings v <=<decodeByteString
alg
In other words, it:
Parses the base64url-encoded representation to extract the serialized values for the components of the JWT. Verifies that:
token
is a valid UTF-8 encoded representation of a completely valid JSON object,- input JWT signature matches,
- the correct algorithm was used,
- all required fields are present.
If steps 1-2 are unuccessful, DecodeException
will be thrown.
If step 3 fails, AlgorithmMismatch
will be thrown.
If the last step fails, MissingClaim
will be thrown.
Once the token has been successfully decoded, it is validated.
In addition to the default rules mandated by the RFC, the application can add its own rules.
The default rules are:
- check
exp
claim to see if the current time is before the expiration time, - check
nbf
claim to see if the current time is after or equal the not-before time, - check
aud
claim if the application identifies itself with a value in theaud
list (if present)
You may allow a little leeway
when checking time-based claims.
module Libjwt.Exceptions
module Libjwt.Header
module Libjwt.Keys
module Libjwt.Payload
module Libjwt.RegisteredClaims
module Libjwt.PrivateClaims
data JwtValidation pc any Source #
Instances
Semigroup (JwtValidation pc any) Source # | |
Defined in Libjwt.JwtValidation (<>) :: JwtValidation pc any -> JwtValidation pc any -> JwtValidation pc any # sconcat :: NonEmpty (JwtValidation pc any) -> JwtValidation pc any # stimes :: Integral b => b -> JwtValidation pc any -> JwtValidation pc any # | |
Monoid (JwtValidation any1 any2) Source # | |
Defined in Libjwt.JwtValidation mempty :: JwtValidation any1 any2 # mappend :: JwtValidation any1 any2 -> JwtValidation any1 any2 -> JwtValidation any1 any2 # mconcat :: [JwtValidation any1 any2] -> JwtValidation any1 any2 # |
data ValidationFailure Source #
Reasons for rejecting a JWT token
InvalidClaim String | User check failed |
TokenExpired NominalDiffTime | exp check failed: the current time was after or equal to the expiration time (plus possible |
TokenNotReady NominalDiffTime | nbf check failed: the current time was before the not-before time (minus possible |
WrongRecipient | aud check failed: the application processing this claim did not identify itself ( |
TokenTooOld NominalDiffTime | iat check failed: the current time minus the time the JWT was issued (plus possible |
Instances
Eq ValidationFailure Source # | |
Defined in Libjwt.JwtValidation (==) :: ValidationFailure -> ValidationFailure -> Bool # (/=) :: ValidationFailure -> ValidationFailure -> Bool # | |
Show ValidationFailure Source # | |
Defined in Libjwt.JwtValidation showsPrec :: Int -> ValidationFailure -> ShowS # show :: ValidationFailure -> String # showList :: [ValidationFailure] -> ShowS # |
data ValidationSettings Source #
User-defined parameters of an validation
Instances
Show ValidationSettings Source # | |
Defined in Libjwt.JwtValidation showsPrec :: Int -> ValidationSettings -> ShowS # show :: ValidationSettings -> String # showList :: [ValidationSettings] -> ShowS # |
type ValidationNEL a b = Validation (NonEmpty a) b Source #
defaultValidationSettings :: ValidationSettings Source #
ValidationSettings
with leeway
set to 0
and appName
set to Nothing
:: String | claim |
-> (a -> Bool) | p |
-> (Payload pc any -> a) | prop |
-> JwtValidation pc any |
Check the property prop
of a payload with the predicate p
If p
is False
, then signal InvalidClaim
claim
:: String | issuer |
-> JwtValidation any1 any2 |
Check that iss is present and equal to issuer
. If not, then signal InvalidClaim
"iss"
:: String | subject |
-> JwtValidation any1 any2 |
Check that sub is present and equal to subject
. If not, then signal InvalidClaim
"sub"
:: NominalDiffTime | maxAge |
-> JwtValidation any1 any2 |
Check that iat (if present) is not further than maxAge
from currentTime
(minus possible leeway
). Otherwise signal TokenTooOld
.
:: UTCTime | time |
-> JwtValidation any1 any2 |
Check that iat (if present) is after time
. If false, signal
.InvalidClaim
"iat"
:: UUID | jwtId |
-> JwtValidation any1 any2 |
Check that jti is present and equal to jwtId
. If not, then signal InvalidClaim
"jti"
:: (CanGet n pc, a ~ LookupClaimType n pc) | |
=> (a -> Bool) | p |
-> ClaimName n | n |
-> JwtValidation pc any |
Check that p a == True
, where a
is a value of private claim n
. If not, signal InvalidClaim
n
Example:
checkClaim
not #is_root
newtype NumericDate Source #
Represents the number of seconds elapsed since 1970-01-01
Instances
fromPOSIX :: POSIXTime -> NumericDate Source #
fromUTC :: UTCTime -> NumericDate Source #
plusSeconds :: NumericDate -> NominalDiffTime -> NumericDate Source #
Add some seconds to the date
Represents a string consisting of only ASCII characters. JWT encoding and decoding can safely skip conversion to/from UTF-8 for these values
Instances
Eq ASCII Source # | |
Ord ASCII Source # | |
Read ASCII Source # | |
Show ASCII Source # | |
JsonParser ASCII Source # | |
Defined in Libjwt.Classes | |
JsonBuilder ASCII Source # | |
Defined in Libjwt.Classes jsonBuilder :: ASCII -> Builder Source # | |
JwtRep ByteString ASCII Source # | |
Defined in Libjwt.Classes | |
JwtRep ASCII ZonedTime Source # | |
JwtRep ASCII LocalTime Source # | |
JwtRep ASCII UTCTime Source # | |
JwtRep ASCII Day Source # | |
AFlag a => JwtRep ASCII (Flag a) Source # | |
Types that can be used as flags . That is, they support conversion to/from ASCII values, for example, simple sum types are good candidates that can even be generically derived
data Scope = Login | Extended | UserRead | UserWrite | AccountRead | AccountWrite deriving stock (Show, Eq, Generic) instance AFlag Scope
>>>
getFlagValue UserWrite
ASCII {getASCII = "userWrite"}
>>>
setFlagValue (ASCII "userWrite") :: Maybe Scope
Just UserWrite
Nothing
getFlagValue :: a -> ASCII Source #
setFlagValue :: ASCII -> Maybe a Source #
Value that is encoded and decoded as AFlag
Flags provide a way to automatically encode and decode simple sum types.
data Scope = Login | Extended | UserRead | UserWrite | AccountRead | AccountWrite deriving stock (Show, Eq, Generic) instance AFlag Scope mkPayload = jwtPayload (withIssuer "myApp" <> withRecipient "https://myApp.com" <> setTtl 300) ( #user_name ->> "John Doe" , #is_root ->> False , #user_id ->> (12345 :: Int) , #scope ->> Flag Login )
Instances
AFlag a => JwtRep ASCII (Flag a) Source # | |
Eq a => Eq (Flag a) Source # | |
Show a => Show (Flag a) Source # | |
AFlag a => AFlag (Flag a) Source # | |
Defined in Libjwt.Flag | |
AFlag a => JsonParser (Flag a) Source # | |
Defined in Libjwt.Classes | |
AFlag a => JsonBuilder (Flag a) Source # | |
Defined in Libjwt.Classes jsonBuilder :: Flag a -> Builder Source # |
Definition of claims encoding.
The only use for the user is probably to write a function that is polymorphic in the payload type.
Instances
Encode Header Source # | |
Defined in Libjwt.Header | |
Encode Jti Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Iat Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Nbf Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Exp Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Aud Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Sub Source # | |
Defined in Libjwt.RegisteredClaims | |
Encode Iss Source # | |
Defined in Libjwt.RegisteredClaims | |
(ClaimEncoder a, KnownSymbol name, KnownNamespace ns, Encode (PrivateClaims tl ns)) => Encode (PrivateClaims ((name ->> a) ': tl) ns) Source # | |
Defined in Libjwt.PrivateClaims encode :: PrivateClaims ((name ->> a) ': tl) ns -> JwtT -> EncodeResult Source # | |
Encode (PrivateClaims Empty ns) Source # | |
Defined in Libjwt.PrivateClaims encode :: PrivateClaims Empty ns -> JwtT -> EncodeResult Source # | |
Encode (PrivateClaims pc ns) => Encode (Payload pc ns) Source # | |
Defined in Libjwt.Payload | |
Encode (PrivateClaims pc ns) => Encode (Jwt pc ns) Source # | |
Defined in Libjwt.Jwt |
Definition of claims decoding.
The only use for the user is probably to write a function that is polymorphic in the payload type
Instances
Decode Jti Source # | |
Decode Iat Source # | |
Decode Nbf Source # | |
Decode Exp Source # | |
Decode Aud Source # | |
Decode Sub Source # | |
Decode Iss Source # | |
(ty ~ DecodeAuxDef a, DecodeAux ty ns name a, CanAdd name tl, Decode (PrivateClaims tl ns)) => Decode (PrivateClaims ((name ->> a) ': tl) ns) Source # | |
Defined in Libjwt.PrivateClaims | |
Decode (PrivateClaims Empty ns) Source # | |
Defined in Libjwt.PrivateClaims | |
Decode (PrivateClaims pc ns) => Decode (Payload pc ns) Source # | |