libjwt-typed-0.1: A Haskell implementation of JSON Web Token (JWT)
Copyright(c) 2020 Marcin Rzeźnicki
LicenseMPL-2.0
MaintainerMarcin Rzeźnicki <marcin.rzeznicki@gmail.com>
Safe HaskellNone
LanguageHaskell2010

Web.Libjwt

Description

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:

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 $ now plusSeconds 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)

instance ToPrivateClaims 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 $ now plusSeconds 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.

AlgorithmDescription
HS256HMAC with SHA-256
HS384HMAC with SHA-384
HS512HMAC with SHA-512
RS256RSASSA-PKCS1-v1_5 with SHA-256
RS384RSASSA-PKCS1-v1_5 with SHA-384
RS512RSASSA-PKCS1-v1_5 with SHA-512
ES256ECDSA with curve P-256 and SHA-256
ES384ECDSA with curve P-384 and SHA-384
ES512ECDSA 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 Validated MyJwt, not Decoded MyJwt, which is the result of step 1.

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

Documentation

data Validated t Source #

Successfully validated value of type t

Instances

Instances details
Eq t => Eq (Validated t) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

(==) :: Validated t -> Validated t -> Bool #

(/=) :: Validated t -> Validated t -> Bool #

Show t => Show (Validated t) Source # 
Instance details

Defined in Libjwt.Jwt

data Decoded t Source #

Decoded value of type t

Instances

Instances details
Eq t => Eq (Decoded t) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

(==) :: Decoded t -> Decoded t -> Bool #

(/=) :: Decoded t -> Decoded t -> Bool #

Show t => Show (Decoded t) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

showsPrec :: Int -> Decoded t -> ShowS #

show :: Decoded t -> String #

showList :: [Decoded t] -> ShowS #

data Encoded t Source #

base64url-encoded value of type t

Instances

Instances details
Eq (Encoded t) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

(==) :: Encoded t -> Encoded t -> Bool #

(/=) :: Encoded t -> Encoded t -> Bool #

Show (Encoded t) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

showsPrec :: Int -> Encoded t -> ShowS #

show :: Encoded t -> String #

showList :: [Encoded t] -> ShowS #

data Jwt pc ns Source #

JSON Web Token representation

Constructors

Jwt 

Fields

Instances

Instances details
Eq (PrivateClaims pc ns) => Eq (Jwt pc ns) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

(==) :: Jwt pc ns -> Jwt pc ns -> Bool #

(/=) :: Jwt pc ns -> Jwt pc ns -> Bool #

Show (PrivateClaims pc ns) => Show (Jwt pc ns) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

showsPrec :: Int -> Jwt pc ns -> ShowS #

show :: Jwt pc ns -> String #

showList :: [Jwt pc ns] -> ShowS #

Encode (PrivateClaims pc ns) => Encode (Jwt pc ns) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

encode :: Jwt pc ns -> JwtT -> EncodeResult Source #

sign :: Encode (PrivateClaims pc ns) => Alg -> Payload pc ns -> Encoded (Jwt pc ns) Source #

Compute the encoded JWT value with the JWS Signature in the manner defined for the algorithm alg . typ of the JWT Header is set to JWT

Creates the serialized ouput, that is: BASE64URL(UTF8(JWT Header)) || . || BASE64URL(JWT Payload) || . || BASE64URL(JWT Signature)

signJwt :: Encode (PrivateClaims pc ns) => Jwt pc ns -> Encoded (Jwt pc ns) Source #

Compute the encoded JWT value with the JWS Signature in the manner defined for the algorithm alg present in the JWT's header .

Creates the serialized ouput, that is: BASE64URL(UTF8(JWT Header)) || . || BASE64URL(JWT Payload) || . || BASE64URL(JWT Signature)

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:

  1. token is a valid UTF-8 encoded representation of a completely valid JSON object,
  2. input JWT signature matches,
  3. the correct algorithm was used,
  4. 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.

validateJwt Source #

Arguments

:: MonadTime m 
=> ValidationSettings

leeway and appName

-> 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 the aud list (if present)

You may allow a little leeway when checking time-based claims.

aud claim is checked against appName.

jwtFromByteString Source #

Arguments

:: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) 
=> ValidationSettings

leeway and appName

-> 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:

  1. token is a valid UTF-8 encoded representation of a completely valid JSON object,
  2. input JWT signature matches,
  3. the correct algorithm was used,
  4. 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 the aud list (if present)

You may allow a little leeway when checking time-based claims.

aud claim is checked against appName.

data JwtValidation pc any Source #

Instances

Instances details
Semigroup (JwtValidation pc any) Source # 
Instance details

Defined in Libjwt.JwtValidation

Methods

(<>) :: 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 # 
Instance details

Defined in Libjwt.JwtValidation

Methods

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

Constructors

InvalidClaim String

User check failed

TokenExpired NominalDiffTime

exp check failed: the current time was after or equal to the expiration time (plus possible leeway)

TokenNotReady NominalDiffTime

nbf check failed: the current time was before the not-before time (minus possible leeway)

WrongRecipient

aud check failed: the application processing this claim did not identify itself (appName) with a value in the aud claim

TokenTooOld NominalDiffTime

iat check failed: the current time minus the time the JWT was issued (plus possible leeway) was greater than expected

data ValidationSettings Source #

User-defined parameters of an validation

Constructors

Settings 

Fields

Instances

Instances details
Show ValidationSettings Source # 
Instance details

Defined in Libjwt.JwtValidation

check Source #

Arguments

:: 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

checkIssuer Source #

Arguments

:: String

issuer

-> JwtValidation any1 any2 

Check that iss is present and equal to issuer. If not, then signal InvalidClaim "iss"

checkSubject Source #

Arguments

:: String

subject

-> JwtValidation any1 any2 

Check that sub is present and equal to subject. If not, then signal InvalidClaim "sub"

checkAge Source #

Arguments

:: NominalDiffTime

maxAge

-> JwtValidation any1 any2 

Check that iat (if present) is not further than maxAge from currentTime (minus possible leeway). Otherwise signal TokenTooOld.

checkIssuedAfter Source #

Arguments

:: UTCTime

time

-> JwtValidation any1 any2 

Check that iat (if present) is after time. If false, signal InvalidClaim "iat".

checkJwtId Source #

Arguments

:: UUID

jwtId

-> JwtValidation any1 any2 

Check that jti is present and equal to jwtId. If not, then signal InvalidClaim "jti"

checkClaim Source #

Arguments

:: (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

Used in accordance with the RFC in Exp, Nbf and Iat claims

Constructors

NumericDate 

plusSeconds :: NumericDate -> NominalDiffTime -> NumericDate Source #

Add some seconds to the date

newtype ASCII Source #

Represents a string consisting of only ASCII characters. JWT encoding and decoding can safely skip conversion to/from UTF-8 for these values

Constructors

ASCII 

Fields

Instances

Instances details
Eq ASCII Source # 
Instance details

Defined in Libjwt.ASCII

Methods

(==) :: ASCII -> ASCII -> Bool #

(/=) :: ASCII -> ASCII -> Bool #

Ord ASCII Source # 
Instance details

Defined in Libjwt.ASCII

Methods

compare :: ASCII -> ASCII -> Ordering #

(<) :: ASCII -> ASCII -> Bool #

(<=) :: ASCII -> ASCII -> Bool #

(>) :: ASCII -> ASCII -> Bool #

(>=) :: ASCII -> ASCII -> Bool #

max :: ASCII -> ASCII -> ASCII #

min :: ASCII -> ASCII -> ASCII #

Read ASCII Source # 
Instance details

Defined in Libjwt.ASCII

Show ASCII Source # 
Instance details

Defined in Libjwt.ASCII

Methods

showsPrec :: Int -> ASCII -> ShowS #

show :: ASCII -> String #

showList :: [ASCII] -> ShowS #

JsonParser ASCII Source # 
Instance details

Defined in Libjwt.Classes

JsonBuilder ASCII Source # 
Instance details

Defined in Libjwt.Classes

JwtRep ByteString ASCII Source # 
Instance details

Defined in Libjwt.Classes

JwtRep ASCII ZonedTime Source # 
Instance details

Defined in Libjwt.Classes

JwtRep ASCII LocalTime Source # 
Instance details

Defined in Libjwt.Classes

JwtRep ASCII UTCTime Source # 
Instance details

Defined in Libjwt.Classes

JwtRep ASCII Day Source # 
Instance details

Defined in Libjwt.Classes

AFlag a => JwtRep ASCII (Flag a) Source # 
Instance details

Defined in Libjwt.Classes

Methods

rep :: Flag a -> ASCII Source #

unRep :: ASCII -> Maybe (Flag a) Source #

class AFlag a where 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

Minimal complete definition

Nothing

Methods

getFlagValue :: a -> ASCII Source #

default getFlagValue :: (Generic a, GFlag (Rep a)) => a -> ASCII Source #

setFlagValue :: ASCII -> Maybe a Source #

default setFlagValue :: (Generic a, GFlag (Rep a)) => ASCII -> Maybe a Source #

Instances

Instances details
AFlag a => AFlag (Flag a) Source # 
Instance details

Defined in Libjwt.Flag

newtype Flag 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
    )

Constructors

Flag 

Fields

Instances

Instances details
AFlag a => JwtRep ASCII (Flag a) Source # 
Instance details

Defined in Libjwt.Classes

Methods

rep :: Flag a -> ASCII Source #

unRep :: ASCII -> Maybe (Flag a) Source #

Eq a => Eq (Flag a) Source # 
Instance details

Defined in Libjwt.Flag

Methods

(==) :: Flag a -> Flag a -> Bool #

(/=) :: Flag a -> Flag a -> Bool #

Show a => Show (Flag a) Source # 
Instance details

Defined in Libjwt.Flag

Methods

showsPrec :: Int -> Flag a -> ShowS #

show :: Flag a -> String #

showList :: [Flag a] -> ShowS #

AFlag a => AFlag (Flag a) Source # 
Instance details

Defined in Libjwt.Flag

AFlag a => JsonParser (Flag a) Source # 
Instance details

Defined in Libjwt.Classes

AFlag a => JsonBuilder (Flag a) Source # 
Instance details

Defined in Libjwt.Classes

class Encode c Source #

Definition of claims encoding.

The only use for the user is probably to write a function that is polymorphic in the payload type.

Minimal complete definition

encode

Instances

Instances details
Encode Header Source # 
Instance details

Defined in Libjwt.Header

Encode Jti Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Iat Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Nbf Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Exp Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Aud Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Sub Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Encode Iss Source # 
Instance details

Defined in Libjwt.RegisteredClaims

(ClaimEncoder a, KnownSymbol name, KnownNamespace ns, Encode (PrivateClaims tl ns)) => Encode (PrivateClaims ((name ->> a) ': tl) ns) Source # 
Instance details

Defined in Libjwt.PrivateClaims

Methods

encode :: PrivateClaims ((name ->> a) ': tl) ns -> JwtT -> EncodeResult Source #

Encode (PrivateClaims Empty ns) Source # 
Instance details

Defined in Libjwt.PrivateClaims

Encode (PrivateClaims pc ns) => Encode (Payload pc ns) Source # 
Instance details

Defined in Libjwt.Payload

Methods

encode :: Payload pc ns -> JwtT -> EncodeResult Source #

Encode (PrivateClaims pc ns) => Encode (Jwt pc ns) Source # 
Instance details

Defined in Libjwt.Jwt

Methods

encode :: Jwt pc ns -> JwtT -> EncodeResult Source #

class Decode c Source #

Definition of claims decoding.

The only use for the user is probably to write a function that is polymorphic in the payload type

Minimal complete definition

decode

Instances

Instances details
Decode Jti Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Jti Source #

Decode Iat Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Iat Source #

Decode Nbf Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Nbf Source #

Decode Exp Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Exp Source #

Decode Aud Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Aud Source #

Decode Sub Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Sub Source #

Decode Iss Source # 
Instance details

Defined in Libjwt.RegisteredClaims

Methods

decode :: JwtT -> JwtIO Iss Source #

(ty ~ DecodeAuxDef a, DecodeAux ty ns name a, CanAdd name tl, Decode (PrivateClaims tl ns)) => Decode (PrivateClaims ((name ->> a) ': tl) ns) Source # 
Instance details

Defined in Libjwt.PrivateClaims

Methods

decode :: JwtT -> JwtIO (PrivateClaims ((name ->> a) ': tl) ns) Source #

Decode (PrivateClaims Empty ns) Source # 
Instance details

Defined in Libjwt.PrivateClaims

Decode (PrivateClaims pc ns) => Decode (Payload pc ns) Source # 
Instance details

Defined in Libjwt.Payload

Methods

decode :: JwtT -> JwtIO (Payload pc ns) Source #