jwt-0.9.0: JSON Web Token (JWT) decoding and encoding

LicenseMIT
MaintainerStefan Saasen <stefan@saasen.me>
Stabilityexperimental
Safe HaskellNone
LanguageHaskell2010

Web.JWT

Contents

Description

This implementation of JWT is based on https://tools.ietf.org/html/rfc7519 but currently only implements the minimum required to work with the Atlassian Connect framework and GitHub App

Known limitations:

  • Only HMAC SHA-256 and RSA SHA-256 algorithms are currently a supported signature algorithm
  • There is currently no verification of time related information (exp, nbf, iat).
  • Registered claims are not validated
Synopsis

Encoding & Decoding JWTs

Decoding

There are three use cases supported by the set of decoding/verification functions:

  1. Unsecured JWTs (http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-30#section-6). This is supported by the decode function decode. As a client you don't care about signing or encrypting so you only get back a JWT UnverifiedJWT. I.e. the type makes it clear that no signature verification was attempted.
  2. Signed JWTs you want to verify using a known secret. This is what decodeAndVerifySignature supports, given a secret and JSON it will return a JWT VerifiedJWT if the signature can be verified.
  3. Signed JWTs that need to be verified using a secret that depends on information contained in the JWT. E.g. the secret depends on some claim, therefore the JWT needs to be decoded first and after retrieving the appropriate secret value, verified in a subsequent step. This is supported by using the verify function which given a JWT UnverifiedJWT and a secret will return a JWT VerifiedJWT iff the signature can be verified.

decode :: JSON -> Maybe (JWT UnverifiedJWT) Source #

Decode a claims set without verifying the signature. This is useful if information from the claim set is required in order to verify the claim (e.g. the secret needs to be retrieved based on unverified information from the claims set).

>>> :{
 let
     input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text
     mJwt = decode input
 in fmap header mJwt
:}
Just (JOSEHeader {typ = Just "JWT", cty = Nothing, alg = Just HS256})

and

>>> :{
 let
     input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text
     mJwt = decode input
 in fmap claims mJwt
:}
Just (JWTClaimsSet {iss = Nothing, sub = Nothing, aud = Nothing, exp = Nothing, nbf = Nothing, iat = Nothing, jti = Nothing, unregisteredClaims = ClaimsMap {unClaimsMap = fromList [("some",String "payload")]}})

verify :: Signer -> JWT UnverifiedJWT -> Maybe (JWT VerifiedJWT) Source #

Using a known secret and a decoded claims set verify that the signature is correct and return a verified JWT token as a result.

This will return a VerifiedJWT if and only if the signature can be verified using the given secret.

The separation between decode and verify is very useful if you are communicating with multiple different services with different secrets and it allows you to lookup the correct secret for the unverified JWT before trying to verify it. If this is not an isuse for you (there will only ever be one secret) then you should just use decodeAndVerifySignature.

>>> :{
 let
     input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text
     mUnverifiedJwt = decode input
     mVerifiedJwt = verify (hmacSecret "secret") =<< mUnverifiedJwt
 in signature =<< mVerifiedJwt
:}
Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U")

decodeAndVerifySignature :: Signer -> JSON -> Maybe (JWT VerifiedJWT) Source #

Decode a claims set and verify that the signature matches by using the supplied secret. The algorithm is based on the supplied header value.

This will return a VerifiedJWT if and only if the signature can be verified using the given secret.

>>> :{
 let
     input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text
     mJwt = decodeAndVerifySignature (hmacSecret "secret") input
 in signature =<< mJwt
:}
Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U")

Encoding

encodeSigned :: Signer -> JWTClaimsSet -> JSON Source #

Encode a claims set using the given secret

 let
     cs = mempty { -- mempty returns a default JWTClaimsSet
        iss = stringOrURI Foo
      , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]
     }
     key = hmacSecret "secret-key"
 in encodeSigned key cs
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiRm9vIn0.vHQHuG3ujbnBUmEp-fSUtYxk27rLiP2hrNhxpyWhb2E"

encodeUnsigned :: JWTClaimsSet -> JSON Source #

Encode a claims set without signing it

 let
     cs = mempty { -- mempty returns a default JWTClaimsSet
     iss = stringOrURI Foo
   , iat = numericDate 1394700934
   , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]
 }
 in encodeUnsigned cs
 
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjEzOTQ3MDA5MzQsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJGb28ifQ."

Utility functions

Common

tokenIssuer :: JSON -> Maybe StringOrURI Source #

Try to extract the value for the issue claim field iss from the web token in JSON form

hmacSecret :: Text -> Signer Source #

Create a Secret using the given key. Consider using binarySecret instead if your key is not already a Data.Text.

readRsaSecret :: ByteString -> Maybe PrivateKey Source #

Create an RSA PrivateKey from PEM contents

readRsaSecret <$> BS.readFile "foo.pem"
>>> :{
  -- A random example key created with `ssh-keygen -t rsa`
  fromJust . readRsaSecret . C8.pack $ unlines
      [ "-----BEGIN RSA PRIVATE KEY-----"
      , "MIIEowIBAAKCAQEAkkmgbLluo5HommstpHr1h53uWfuN3CwYYYR6I6a2MzAHIMIv"
      , "8Ak2ha+N2UDeYsfVhZ/DOnE+PMm2RpYSaiYT0l2a7ZkmRSbcyvVFt3XLePJbmUgo"
      , "ieyccS4uYHeqRggdWH9His3JaR2N71N9iU0+mY5nu2+15iYw3naT/PSx01IzBqHN"
      , "Zie1z3FYX09FgOs31mcR8VWj8DefxbKE08AW+vDMT2AmUC2b+Gqk6SqRz29HuPBs"
      , "yyV4Xl9CgzcCWjuXTv6mevDygo5RVZg34U6L1iFRgwwHbrLcd2N97wlKz+OiDSgM"
      , "sbZWA0i2D9ZsDR9rdEdXzUIw6toIRYZfeI9QYQIDAQABAoIBAEXkh5Fqx0G/ZLLi"
      , "olwDo2u4OTkkxxJ6vutYsEJ4VHUAbWdpYB3/SN12kv9JzvbDI3FEc7JoiKPifAQd"
      , "j47HwpCvyGXc1jwT5UnTBgwxa5XNtZX2s+ex9Mzek6njgqcTGXI+3Z+j0qc2R6og"
      , "6cm/7jjPoSAcr3vWo2KmpO4muw+LbYoSGo0Jydoa5cGtkmDfsjjrMw7mDoRttdhw"
      , "WdhS+q2aJPFI7q7itoYUd7KLe3nOeM0zd35Pc8Qc6jGk+JZxQdXrb/NrSNgAATcN"
      , "GGS226Q444N0pAfc188IDcAtQPSJpzbs/1+TPzE4ov/lpHTr91hXr3RLyVgYBI01"
      , "jrggfAECgYEAwaC4iDSZQ+8eUx/zR973Lu9mvQxC2BZn6QcOtBcIRBdGRlXfhwuD"
      , "UgwVZ2M3atH5ZXFuQ7pRtJtj7KCFy7HUFAJC15RCfLjx+n39bISNp5NOJEdI+UM+"
      , "G2xMHv5ywkULV7Jxb+tSgsYIvJ0tBjACkif8ahNjgVJmgMSOgdHR2pkCgYEAwWkN"
      , "uquRqKekx4gx1gJYV7Y6tPWcsZpEcgSS7AGNJ4UuGZGGHdStpUoJICn2cFUngYNz"
      , "eJXOg+VhQJMqQx9c+u85mg/tJluGaw95tBAafspwvhKewlO9OhQeVInPbXMUwrJ0"
      , "PS3XV7c74nxm6Nn4QHlM07orn3lOiWxZF8BBSQkCgYATjwSU3ZtNvW22v9d3PxKA"
      , "7zXVitOFuF2usEPP9TOkjSVQHYSCw6r0MrxGwULry2IB2T9mH//42mlxkZVySfg+"
      , "PSw7UoKUzqnCv89Fku4sKzkNeRXp99ziMEJQLyuwbAEFTsUepQqkoxRm2QmfQmJA"
      , "GUHqBSNcANLR1wj+HA+yoQKBgQCBlqj7RQ+AaGsQwiFaGhIlGtU1AEgv+4QWvRfQ"
      , "B64TJ7neqdGp1SFP2U5J/bPASl4A+hl5Vy6a0ysZQEGV3cLH41e98SPdin+C5kiO"
      , "LCgEghGOWR2EaOUlr+sui3OvCueDGFynzTo27G+0bdPp+nnKgTvHtTqbTIUhsLX1"
      , "IvzbOQKBgH4q36jgBb9T3hjXtWyrytlmFtBdw0i+UiMvMlnOqujGhcnOk5UMyxOQ"
      , "sQI+/31jIGbmlE7YaYykR1FH3LzAjO4J1+m7vv5fIRdG8+sI01xTc8UAdbmWtK+5"
      , "TK1oLP43BHH5gRAfIlXj2qmap5lEG6If/xYB4MOs8Bui5iKaJlM5"
      , "-----END RSA PRIVATE KEY-----"
      ]
:}
PrivateKey {private_pub = PublicKey {public_size = 256, public_n = 1846..., public_e = 65537}, private_d = 8823..., private_p = 135..., private_q = 1358..., private_dP = 1373..., private_dQ = 9100..., private_qinv = 8859...}

JWT structure

claims :: JWT r -> JWTClaimsSet Source #

Extract the claims set from a JSON Web Token

header :: JWT r -> JOSEHeader Source #

Extract the header from a JSON Web Token

signature :: JWT r -> Maybe Signature Source #

Extract the signature from a verified JSON Web Token

JWT claims set

auds :: JWTClaimsSet -> [StringOrURI] Source #

Convert the aud claim in a JWTClaimsSet into a `[StringOrURI]`

intDate :: NominalDiffTime -> Maybe IntDate Source #

Deprecated: Use numericDate instead. intDate will be removed in 1.0

Convert the NominalDiffTime into an IntDate. Returns a Nothing if the argument is invalid (e.g. the NominalDiffTime must be convertible into a positive Integer representing the seconds since epoch).

numericDate :: NominalDiffTime -> Maybe NumericDate Source #

Convert the NominalDiffTime into an NumericDate. Returns a Nothing if the argument is invalid (e.g. the NominalDiffTime must be convertible into a positive Integer representing the seconds since epoch).

stringOrURI :: Text -> Maybe StringOrURI Source #

Convert a Text into a StringOrURI. Returns a Nothing if the String cannot be converted (e.g. if the String contains a : but is *not* a valid URI).

stringOrURIToText :: StringOrURI -> Text Source #

Convert a StringOrURI into a Text. Returns the T.Text representing the String as-is or a Text representation of the URI otherwise.

secondsSinceEpoch :: NumericDate -> NominalDiffTime Source #

Return the seconds since 1970-01-01T0:0:0Z UTC for the given IntDate

JWT header

typ :: JOSEHeader -> Maybe Text Source #

The typ (type) Header Parameter defined by [JWS] and [JWE] is used to declare the MIME Media Type [IANA.MediaTypes] of this complete JWT in contexts where this is useful to the application. This parameter has no effect upon the JWT processing.

cty :: JOSEHeader -> Maybe Text Source #

The cty (content type) Header Parameter defined by [JWS] and [JWE] is used by this specification to convey structural information about the JWT.

alg :: JOSEHeader -> Maybe Algorithm Source #

The alg (algorithm) used for signing the JWT. The HS256 (HMAC using SHA-256) is the only required algorithm in addition to "none" which means that no signature will be used.

See http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-23#page-6

Types

data UnverifiedJWT Source #

JSON Web Token without signature verification

data VerifiedJWT Source #

JSON Web Token that has been successfully verified

data Signature Source #

Instances
Eq Signature Source # 
Instance details

Defined in Web.JWT

Show Signature Source # 
Instance details

Defined in Web.JWT

data JWT r Source #

The JSON Web Token

Instances
Show (JWT r) Source # 
Instance details

Defined in Web.JWT

Methods

showsPrec :: Int -> JWT r -> ShowS #

show :: JWT r -> String #

showList :: [JWT r] -> ShowS #

type JSON = Text Source #

data Algorithm Source #

Constructors

HS256

HMAC using SHA-256 hash algorithm

RS256

RSA using SHA-256 hash algorithm

Instances
Eq Algorithm Source # 
Instance details

Defined in Web.JWT

Show Algorithm Source # 
Instance details

Defined in Web.JWT

ToJSON Algorithm Source # 
Instance details

Defined in Web.JWT

FromJSON Algorithm Source # 
Instance details

Defined in Web.JWT

data JWTClaimsSet Source #

The JWT Claims Set represents a JSON object whose members are the claims conveyed by the JWT.

Constructors

JWTClaimsSet 

Fields

  • iss :: Maybe StringOrURI

    The iss (issuer) claim identifies the principal that issued the JWT.

  • sub :: Maybe StringOrURI

    The sub (subject) claim identifies the principal that is the subject of the JWT.

  • aud :: Maybe (Either StringOrURI [StringOrURI])

    The aud (audience) claim identifies the audiences that the JWT is intended for according to draft 18 of the JWT spec, the aud claim is option and may be present in singular or as a list.

  • exp :: Maybe IntDate

    The exp (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. Its value MUST be a number containing an IntDate value.

  • nbf :: Maybe IntDate

    The nbf (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing.

  • iat :: Maybe IntDate

    The iat (issued at) claim identifies the time at which the JWT was issued.

  • jti :: Maybe StringOrURI

    The jti (JWT ID) claim provides a unique identifier for the JWT.

  • unregisteredClaims :: ClaimsMap
     

newtype ClaimsMap Source #

Constructors

ClaimsMap 
Instances
Eq ClaimsMap Source # 
Instance details

Defined in Web.JWT

Show ClaimsMap Source # 
Instance details

Defined in Web.JWT

Semigroup ClaimsMap Source # 
Instance details

Defined in Web.JWT

Monoid ClaimsMap Source # 
Instance details

Defined in Web.JWT

type IntDate = NumericDate Source #

Deprecated: Use NumericDate instead. IntDate will be removed in 1.0

A JSON numeric value representing the number of seconds from 1970-01-01T0:0:0Z UTC until the specified UTC date/time.

data NumericDate Source #

A JSON numeric value representing the number of seconds from 1970-01-01T0:0:0Z UTC until the specified UTC date/time.

data StringOrURI Source #

A JSON string value, with the additional requirement that while arbitrary string values MAY be used, any value containing a ":" character MUST be a URI [RFC3986]. StringOrURI values are compared as case-sensitive strings with no transformations or canonicalizations applied.

Instances
Eq StringOrURI Source # 
Instance details

Defined in Web.JWT

Show StringOrURI Source # 
Instance details

Defined in Web.JWT

ToJSON StringOrURI Source # 
Instance details

Defined in Web.JWT

FromJSON StringOrURI Source # 
Instance details

Defined in Web.JWT

type JWTHeader = JOSEHeader Source #

Deprecated: Use JOSEHeader instead. JWTHeader will be removed in 1.0

data JOSEHeader Source #

JOSE Header, describes the cryptographic operations applied to the JWT

Deprecated

rsaKeySecret :: String -> IO (Maybe Signer) Source #

Create an RSAPrivateKey from PEM contents

Please, consider using readRsaSecret instead.