License | MIT |
---|---|
Maintainer | Stefan Saasen <stefan@saasen.me> |
Stability | experimental |
Safe Haskell | None |
Language | Haskell2010 |
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:
Synopsis
- decode :: Text -> Maybe (JWT UnverifiedJWT)
- verify :: Signer -> JWT UnverifiedJWT -> Maybe (JWT VerifiedJWT)
- decodeAndVerifySignature :: Signer -> Text -> Maybe (JWT VerifiedJWT)
- encodeSigned :: Signer -> JOSEHeader -> JWTClaimsSet -> Text
- encodeUnsigned :: JWTClaimsSet -> JOSEHeader -> Text
- tokenIssuer :: Text -> Maybe StringOrURI
- hmacSecret :: Text -> Signer
- readRsaSecret :: ByteString -> Maybe PrivateKey
- claims :: JWT r -> JWTClaimsSet
- header :: JWT r -> JOSEHeader
- signature :: JWT r -> Maybe Signature
- auds :: JWTClaimsSet -> [StringOrURI]
- intDate :: NominalDiffTime -> Maybe IntDate
- numericDate :: NominalDiffTime -> Maybe NumericDate
- stringOrURI :: Text -> Maybe StringOrURI
- stringOrURIToText :: StringOrURI -> Text
- secondsSinceEpoch :: NumericDate -> NominalDiffTime
- data UnverifiedJWT
- data VerifiedJWT
- data Signature
- data Signer
- data JWT r
- data Algorithm
- data JWTClaimsSet = JWTClaimsSet {
- iss :: Maybe StringOrURI
- sub :: Maybe StringOrURI
- aud :: Maybe (Either StringOrURI [StringOrURI])
- exp :: Maybe IntDate
- nbf :: Maybe IntDate
- iat :: Maybe IntDate
- jti :: Maybe StringOrURI
- unregisteredClaims :: ClaimsMap
- newtype ClaimsMap = ClaimsMap {
- unClaimsMap :: Map Text Value
- type IntDate = NumericDate
- data NumericDate
- data StringOrURI
- type JWTHeader = JOSEHeader
- data JOSEHeader = JOSEHeader {}
- rsaKeySecret :: String -> IO (Maybe Signer)
Encoding & Decoding JWTs
Decoding
There are three use cases supported by the set of decoding/verification functions:
- 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 aJWT
UnverifiedJWT
. I.e. the type makes it clear that no signature verification was attempted. - Signed JWTs you want to verify using a known secret.
This is what
decodeAndVerifySignature
supports, given a secret and JSON it will return aJWT
VerifiedJWT
if the signature can be verified. - 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 aJWT
UnverifiedJWT
and a secret will return aJWT
VerifiedJWT
iff the signature can be verified.
decode :: Text -> 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, kid = Nothing})
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 -> Text -> 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 -> JOSEHeader -> JWTClaimsSet -> Text 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 mempty cs
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiRm9vIn0.vHQHuG3ujbnBUmEp-fSUtYxk27rLiP2hrNhxpyWhb2E"
encodeUnsigned :: JWTClaimsSet -> JOSEHeader -> Text 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 mempty
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjEzOTQ3MDA5MzQsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJGb28ifQ."
Utility functions
Common
tokenIssuer :: Text -> 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 HMACSecret
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
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
Types
data UnverifiedJWT Source #
JSON Web Token without signature verification
data VerifiedJWT Source #
JSON Web Token that has been successfully verified
The JSON Web Token
data JWTClaimsSet Source #
The JWT Claims Set represents a JSON object whose members are the claims conveyed by the JWT.
JWTClaimsSet | |
|
Instances
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.
Instances
Eq NumericDate Source # | |
Defined in Web.JWT (==) :: NumericDate -> NumericDate -> Bool # (/=) :: NumericDate -> NumericDate -> Bool # | |
Ord NumericDate Source # | |
Defined in Web.JWT compare :: NumericDate -> NumericDate -> Ordering # (<) :: NumericDate -> NumericDate -> Bool # (<=) :: NumericDate -> NumericDate -> Bool # (>) :: NumericDate -> NumericDate -> Bool # (>=) :: NumericDate -> NumericDate -> Bool # max :: NumericDate -> NumericDate -> NumericDate # min :: NumericDate -> NumericDate -> NumericDate # | |
Show NumericDate Source # | |
Defined in Web.JWT showsPrec :: Int -> NumericDate -> ShowS # show :: NumericDate -> String # showList :: [NumericDate] -> ShowS # | |
ToJSON NumericDate Source # | |
Defined in Web.JWT toJSON :: NumericDate -> Value # toEncoding :: NumericDate -> Encoding # toJSONList :: [NumericDate] -> Value # toEncodingList :: [NumericDate] -> Encoding # | |
FromJSON NumericDate Source # | |
Defined in Web.JWT parseJSON :: Value -> Parser NumericDate # parseJSONList :: Value -> Parser [NumericDate] # |
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 # | |
Defined in Web.JWT (==) :: StringOrURI -> StringOrURI -> Bool # (/=) :: StringOrURI -> StringOrURI -> Bool # | |
Show StringOrURI Source # | |
Defined in Web.JWT showsPrec :: Int -> StringOrURI -> ShowS # show :: StringOrURI -> String # showList :: [StringOrURI] -> ShowS # | |
ToJSON StringOrURI Source # | |
Defined in Web.JWT toJSON :: StringOrURI -> Value # toEncoding :: StringOrURI -> Encoding # toJSONList :: [StringOrURI] -> Value # toEncodingList :: [StringOrURI] -> Encoding # | |
FromJSON StringOrURI Source # | |
Defined in Web.JWT parseJSON :: Value -> Parser StringOrURI # parseJSONList :: Value -> Parser [StringOrURI] # |
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
JOSEHeader | |
|
Instances
Eq JOSEHeader Source # | |
Defined in Web.JWT (==) :: JOSEHeader -> JOSEHeader -> Bool # (/=) :: JOSEHeader -> JOSEHeader -> Bool # | |
Show JOSEHeader Source # | |
Defined in Web.JWT showsPrec :: Int -> JOSEHeader -> ShowS # show :: JOSEHeader -> String # showList :: [JOSEHeader] -> ShowS # | |
Semigroup JOSEHeader Source # | |
Defined in Web.JWT (<>) :: JOSEHeader -> JOSEHeader -> JOSEHeader # sconcat :: NonEmpty JOSEHeader -> JOSEHeader # stimes :: Integral b => b -> JOSEHeader -> JOSEHeader # | |
Monoid JOSEHeader Source # | |
Defined in Web.JWT mempty :: JOSEHeader # mappend :: JOSEHeader -> JOSEHeader -> JOSEHeader # mconcat :: [JOSEHeader] -> JOSEHeader # | |
ToJSON JOSEHeader Source # | |
Defined in Web.JWT toJSON :: JOSEHeader -> Value # toEncoding :: JOSEHeader -> Encoding # toJSONList :: [JOSEHeader] -> Value # toEncodingList :: [JOSEHeader] -> Encoding # | |
FromJSON JOSEHeader Source # | |
Defined in Web.JWT parseJSON :: Value -> Parser JOSEHeader # parseJSONList :: Value -> Parser [JOSEHeader] # |
Deprecated
rsaKeySecret :: String -> IO (Maybe Signer) Source #
Create an RSAPrivateKey from PEM contents
Please, consider using readRsaSecret
instead.