packages feed

libjwt-typed 0.1 → 0.2

raw patch · 20 files changed

+916/−727 lines, 20 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Libjwt.Header: instance Libjwt.Encoding.Encode Libjwt.Header.Header
- Libjwt.Jwt: instance Libjwt.Encoding.Encode (Libjwt.PrivateClaims.PrivateClaims pc ns) => Libjwt.Encoding.Encode (Libjwt.Jwt.Jwt pc ns)
- Libjwt.Jwt: signJwt :: Encode (PrivateClaims pc ns) => Jwt pc ns -> Encoded (Jwt pc ns)
- Web.Libjwt: signJwt :: Encode (PrivateClaims pc ns) => Jwt pc ns -> Encoded (Jwt pc ns)
+ Libjwt.Algorithms: [AlgNone] :: Algorithm ()
+ Libjwt.Algorithms: [ECDSA256] :: EcKey e => e -> Algorithm e
+ Libjwt.Algorithms: [ECDSA384] :: EcKey e => e -> Algorithm e
+ Libjwt.Algorithms: [ECDSA512] :: EcKey e => e -> Algorithm e
+ Libjwt.Algorithms: [HMAC256] :: Secret -> Algorithm Secret
+ Libjwt.Algorithms: [HMAC384] :: Secret -> Algorithm Secret
+ Libjwt.Algorithms: [HMAC512] :: Secret -> Algorithm Secret
+ Libjwt.Algorithms: [RSA256] :: RsaKey r => r -> Algorithm r
+ Libjwt.Algorithms: [RSA384] :: RsaKey r => r -> Algorithm r
+ Libjwt.Algorithms: [RSA512] :: RsaKey r => r -> Algorithm r
+ Libjwt.Algorithms: data Algorithm k
+ Libjwt.Algorithms: instance GHC.Show.Show k => GHC.Show.Show (Libjwt.Algorithms.Algorithm k)
+ Libjwt.Algorithms: jwtAlgWithKey :: Algorithm k -> (JwtAlgT, k)
+ Libjwt.Algorithms: toHeaderAlg :: Algorithm k -> Alg
+ Libjwt.Algorithms: type family EcKey t :: Constraint
+ Libjwt.Header: instance Libjwt.Decoding.Decode Libjwt.Header.Alg
+ Libjwt.Header: instance Libjwt.Decoding.Decode Libjwt.Header.Header
+ Libjwt.Header: instance Libjwt.Decoding.Decode Libjwt.Header.Typ
+ Libjwt.Header: instance Libjwt.Encoding.Encode Libjwt.Header.Typ
+ Libjwt.Jwt: sign' :: (Encode (PrivateClaims pc ns), SigningKey k) => Typ -> Algorithm k -> Payload pc ns -> Encoded (Jwt pc ns)
+ Libjwt.Keys: FromEcPub :: ByteString -> EcPubKey
+ Libjwt.Keys: FromRsaPub :: ByteString -> RsaPubKey
+ Libjwt.Keys: [ecPublicKey] :: EcPubKey -> ByteString
+ Libjwt.Keys: [rsaPublicKey] :: RsaPubKey -> ByteString
+ Libjwt.Keys: class DecodingKey k
+ Libjwt.Keys: class DecodingKey k => SigningKey k
+ Libjwt.Keys: getDecodingKey :: DecodingKey k => k -> ByteString
+ Libjwt.Keys: getSigningKey :: SigningKey k => k -> ByteString
+ Libjwt.Keys: instance GHC.Classes.Eq Libjwt.Keys.EcPubKey
+ Libjwt.Keys: instance GHC.Classes.Eq Libjwt.Keys.RsaPubKey
+ Libjwt.Keys: instance GHC.Show.Show Libjwt.Keys.EcPubKey
+ Libjwt.Keys: instance GHC.Show.Show Libjwt.Keys.RsaPubKey
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey ()
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey Libjwt.Keys.EcKeyPair
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey Libjwt.Keys.EcPubKey
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey Libjwt.Keys.RsaKeyPair
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey Libjwt.Keys.RsaPubKey
+ Libjwt.Keys: instance Libjwt.Keys.DecodingKey Libjwt.Keys.Secret
+ Libjwt.Keys: instance Libjwt.Keys.SigningKey ()
+ Libjwt.Keys: instance Libjwt.Keys.SigningKey Libjwt.Keys.EcKeyPair
+ Libjwt.Keys: instance Libjwt.Keys.SigningKey Libjwt.Keys.RsaKeyPair
+ Libjwt.Keys: instance Libjwt.Keys.SigningKey Libjwt.Keys.Secret
+ Libjwt.Keys: newtype EcPubKey
+ Libjwt.Keys: newtype RsaPubKey
+ Web.Libjwt: [AlgNone] :: Algorithm ()
+ Web.Libjwt: [ECDSA256] :: EcKey e => e -> Algorithm e
+ Web.Libjwt: [ECDSA384] :: EcKey e => e -> Algorithm e
+ Web.Libjwt: [ECDSA512] :: EcKey e => e -> Algorithm e
+ Web.Libjwt: [HMAC256] :: Secret -> Algorithm Secret
+ Web.Libjwt: [HMAC384] :: Secret -> Algorithm Secret
+ Web.Libjwt: [HMAC512] :: Secret -> Algorithm Secret
+ Web.Libjwt: [RSA256] :: RsaKey r => r -> Algorithm r
+ Web.Libjwt: [RSA384] :: RsaKey r => r -> Algorithm r
+ Web.Libjwt: [RSA512] :: RsaKey r => r -> Algorithm r
+ Web.Libjwt: data Algorithm k
+ Web.Libjwt: sign' :: (Encode (PrivateClaims pc ns), SigningKey k) => Typ -> Algorithm k -> Payload pc ns -> Encoded (Jwt pc ns)
- Libjwt.FFI.Jwt: jwtDecode :: Maybe ByteString -> ByteString -> JwtIO JwtT
+ Libjwt.FFI.Jwt: jwtDecode :: ByteString -> ByteString -> JwtIO JwtT
- Libjwt.Header: ES256 :: EcKeyPair -> Alg
+ Libjwt.Header: ES256 :: Alg
- Libjwt.Header: ES384 :: EcKeyPair -> Alg
+ Libjwt.Header: ES384 :: Alg
- Libjwt.Header: ES512 :: EcKeyPair -> Alg
+ Libjwt.Header: ES512 :: Alg
- Libjwt.Header: HS256 :: Secret -> Alg
+ Libjwt.Header: HS256 :: Alg
- Libjwt.Header: HS384 :: Secret -> Alg
+ Libjwt.Header: HS384 :: Alg
- Libjwt.Header: HS512 :: Secret -> Alg
+ Libjwt.Header: HS512 :: Alg
- Libjwt.Header: RS256 :: RsaKeyPair -> Alg
+ Libjwt.Header: RS256 :: Alg
- Libjwt.Header: RS384 :: RsaKeyPair -> Alg
+ Libjwt.Header: RS384 :: Alg
- Libjwt.Header: RS512 :: RsaKeyPair -> Alg
+ Libjwt.Header: RS512 :: Alg
- Libjwt.Jwt: decodeByteString :: forall ns pc m. (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> ByteString -> m (Decoded (Jwt pc ns))
+ Libjwt.Jwt: decodeByteString :: forall ns pc m k. (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k) => Algorithm k -> ByteString -> m (Decoded (Jwt pc ns))
- Libjwt.Jwt: decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> String -> m (Decoded (Jwt pc ns))
+ Libjwt.Jwt: decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k) => Algorithm k -> String -> m (Decoded (Jwt pc ns))
- Libjwt.Jwt: jwtFromByteString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> ByteString -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
+ Libjwt.Jwt: jwtFromByteString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k) => ValidationSettings -> JwtValidation pc ns -> Algorithm k -> ByteString -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- Libjwt.Jwt: jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
+ Libjwt.Jwt: jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k) => ValidationSettings -> JwtValidation pc ns -> Algorithm k -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- Libjwt.Jwt: sign :: Encode (PrivateClaims pc ns) => Alg -> Payload pc ns -> Encoded (Jwt pc ns)
+ Libjwt.Jwt: sign :: (Encode (PrivateClaims pc ns), SigningKey k) => Algorithm k -> Payload pc ns -> Encoded (Jwt pc ns)
- Web.Libjwt: decodeByteString :: forall ns pc m. (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> ByteString -> m (Decoded (Jwt pc ns))
+ Web.Libjwt: decodeByteString :: forall ns pc m k. (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k) => Algorithm k -> ByteString -> m (Decoded (Jwt pc ns))
- Web.Libjwt: decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns)) => Alg -> String -> m (Decoded (Jwt pc ns))
+ Web.Libjwt: decodeString :: (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k) => Algorithm k -> String -> m (Decoded (Jwt pc ns))
- Web.Libjwt: jwtFromByteString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> ByteString -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
+ Web.Libjwt: jwtFromByteString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k) => ValidationSettings -> JwtValidation pc ns -> Algorithm k -> ByteString -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- Web.Libjwt: jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m) => ValidationSettings -> JwtValidation pc ns -> Alg -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
+ Web.Libjwt: jwtFromString :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k) => ValidationSettings -> JwtValidation pc ns -> Algorithm k -> String -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))
- Web.Libjwt: sign :: Encode (PrivateClaims pc ns) => Alg -> Payload pc ns -> Encoded (Jwt pc ns)
+ Web.Libjwt: sign :: (Encode (PrivateClaims pc ns), SigningKey k) => Algorithm k -> Payload pc ns -> Encoded (Jwt pc ns)

Files

CHANGELOG.md view
@@ -3,6 +3,16 @@ `libjwt-typed` uses [PVP Versioning][1]. The changelog is available [on GitHub][2]. +## 0.2++New features++* Ability to **use public keys only if you do not intend to sign tokens**. This feature is backwards incompatible:+  * Function `signJwt` has been removed in favor of `sign` and `sign'`+  * Signing and decoding functions now depend on the new `Algorithm k` type and their contexts have been extended to accomodate new key classes (`SigningKey` and `DecodingKey` respectively)+  * JWT header `alg` is now an enumeration, handling of all key data has been moved to `Algorithm`++ ## 0.1  * Initially created.
README.md view
@@ -78,11 +78,11 @@ ```haskell {-# LANGUAGE OverloadedStrings #-} -import Web.Libjwt ( Alg(HS512) )+import Web.Libjwt -hmac512 :: Alg+hmac512 :: Algorithm Secret hmac512 =-  HS512+  HMAC512     "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\     \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\     \Y2IwMDZhYWY1MjY1OTQgIC0K"@@ -96,10 +96,8 @@ Obtaining or reading keys is beyond the scope of this library. It accepts PEM-encoded RSA/ECDSA keys as `ByteString`s  ```haskell-import           Web.Libjwt                     ( Alg(..)-                                                , EcKeyPair(..)-                                                , RsaKeyPair(..)-                                                )+import           Web.Libjwt+ import qualified Data.ByteString.Char8         as C8  rsa2048KeyPair :: RsaKeyPair@@ -118,8 +116,8 @@         ]   in  FromRsaPem { privKey = private, pubKey = public } -rs512 :: Alg-rs512 = RS512 rsa2048KeyPair+rsa512 :: Algorithm RsaKeyPair+rsa512 = RSA512 rsa2048KeyPair  ecP521KeyPair :: EcKeyPair ecP521KeyPair =@@ -137,8 +135,8 @@         ]   in  FromEcPem { ecPrivKey = private, ecPubKey = public } -es512 :: Alg-es512 = ES512 ecP521KeyPair+ecdsa512 :: Algorithm EcKeyPair+ecdsa512 = ECDSA512 ecP521KeyPair ```  A key of size 2048 bits or larger MUST be used for RSA algorithms.@@ -148,6 +146,28 @@    and the SHA-384 hash function, and ECDSA with the P-521 curve [secp521r1] and the    SHA-512 hash function." +As of version **0.2**, you do not need private keys as long as you only decode tokens. This is obviously a type-safe feature, so you cannot pass a public-key to the signing function.+Type system checks it for you.++```haskell+import           Web.Libjwt++import qualified Data.ByteString.Char8         as C8++rsaPub :: RsaPubKey+rsaPub =+  let public = C8.pack $ unlines+        [ "-----BEGIN PUBLIC KEY-----"+        , "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwCXp2P+qboao0tjUyU+D"+        -- ...+        , "-----END PUBLIC KEY-----"+        ]+  in  FromRsaPub { rsaPublicKey = public }++rsa512 :: Algorithm RsaPubKey+rsa512 = RSA512 rsaPub+```+ ## Usage   ### Create a payload@@ -341,7 +361,7 @@  ``` -JWT validation is monoid. You can append additional validations based on public and private claims, for example `checkIssuer "myApp" <> checkClaim (== True) #isRoot`. You will certainly like the fact that private claims' types are fullly known, so you can operate on type-safe Haskell values (`checkClaim ( > 0) #isRoot` will not compile). The `mempty` validation (the default validation) checks (according to [the rules in the RFC](https://tools.ietf.org/html/rfc7519#section-4.1) ) whether:+JWT validation is a monoid. You can append additional validations based on public and private claims, for example `checkIssuer "myApp" <> checkClaim (== True) #isRoot`. You will certainly like the fact that private claims' types are fullly known, so you can operate on type-safe Haskell values (`checkClaim ( > 0) #isRoot` will not compile). The `mempty` validation (the default validation) checks (according to [the rules in the RFC](https://tools.ietf.org/html/rfc7519#section-4.1) ) whether: * token has not expired (`exp` claim), * token is ready to use (`nbf` claim), * token is intended for you (`aud` claim)
bench/Algorithms.hs view
@@ -13,7 +13,8 @@   ) where -import           Web.Libjwt                     ( Alg(..)+import           Web.Libjwt                     ( Algorithm(..)+                                                , Secret                                                 , EcKeyPair(..)                                                 , RsaKeyPair(..)                                                 )@@ -30,9 +31,9 @@  import           Data.Maybe                     ( fromJust ) -hs512 :: Alg+hs512 :: Algorithm Secret hs512 =-  HS512+  HMAC512     "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\     \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\     \Y2IwMDZhYWY1MjY1OTQgIC0K"@@ -81,11 +82,11 @@         ]   in  FromRsaPem { privKey = private, pubKey = public } -rs256 :: Alg-rs256 = RS256 rsa2048KeyPair+rs256 :: Algorithm RsaKeyPair+rs256 = RSA256 rsa2048KeyPair -rs512 :: Alg-rs512 = RS512 rsa2048KeyPair+rs512 :: Algorithm RsaKeyPair+rs512 = RSA512 rsa2048KeyPair  ecP256KeyPair :: EcKeyPair ecP256KeyPair =@@ -104,8 +105,8 @@         ]   in  FromEcPem { ecPrivKey = private, ecPubKey = public } -es256 :: Alg-es256 = ES256 ecP256KeyPair+es256 :: Algorithm EcKeyPair+es256 = ECDSA256 ecP256KeyPair  ecP521KeyPair :: EcKeyPair ecP521KeyPair =@@ -128,8 +129,8 @@         ]   in  FromEcPem { ecPrivKey = private, ecPubKey = public } -es512 :: Alg-es512 = ES512 ecP521KeyPair+es512 :: Algorithm EcKeyPair+es512 = ECDSA512 ecP521KeyPair  jwkHS512 :: (Jose.Alg, JWK) jwkHS512 =
bench/Benchmarks.hs view
@@ -1,4 +1,3 @@- module Main       ( main       )@@ -17,74 +16,49 @@             "signing"             [ bgroup                   "HMAC512"-                  [ my $ mySigning <*> pure hs512-                  , jose $ joseSigning <*> pure jwkHS512+                  [ my $ My.signing <*> pure hs512+                  , jose $ Jose.signing <*> pure jwkHS512                   ]             , bgroup                   "RSA512"-                  [ my $ mySigning <*> pure rs512-                  , jose $ joseSigning <*> pure jwkRS512+                  [ my $ My.signing <*> pure rs512+                  , jose $ Jose.signing <*> pure jwkRS512                   ]             , bgroup                   "ECDSA256"-                  [ my $ mySigning <*> pure es256-                  , jose $ joseSigning <*> pure jwkES256+                  [ my $ My.signing <*> pure es256+                  , jose $ Jose.signing <*> pure jwkES256                   ]             , bgroup                   "ECDSA512"-                  [ my $ mySigning <*> pure es512-                  , jose $ joseSigning <*> pure jwkES512+                  [ my $ My.signing <*> pure es512+                  , jose $ Jose.signing <*> pure jwkES512                   ]             ]       , bgroup             "decoding"             [ bgroup                   "HMAC512"-                  [ my $ myDecoding <*> pure hs512-                  , jose $ joseDecoding <*> pure jwkHS512+                  [ my $ My.decoding <*> pure hs512+                  , jose $ Jose.decoding <*> pure jwkHS512                   ]             , bgroup                   "RSA512"-                  [ my $ myDecoding <*> pure rs512-                  , jose $ joseDecoding <*> pure jwkRS512+                  [ my $ My.decoding <*> pure rs512+                  , jose $ Jose.decoding <*> pure jwkRS512                   ]             , bgroup                   "ECDSA256"-                  [ my $ myDecoding <*> pure es256-                  , jose $ joseDecoding <*> pure jwkES256+                  [ my $ My.decoding <*> pure es256+                  , jose $ Jose.decoding <*> pure jwkES256                   ]             , bgroup                   "ECDSA512"-                  [ my $ myDecoding <*> pure es512-                  , jose $ joseDecoding <*> pure jwkES512+                  [ my $ My.decoding <*> pure es512+                  , jose $ Jose.decoding <*> pure jwkES512                   ]             ]       ]    where       jose = bgroup "jose"-      joseSigning =-            [ Jose.signSimple-            , Jose.signCustomClaims-            , Jose.signCustomClaimsWithNs-            , Jose.signComplexClaims-            ]-      joseDecoding =-            [ Jose.decodeSimple-            , Jose.decodeCustomClaims-            , Jose.decodeCustomClaimsWithNs-            , Jose.decodeComplexClaims-            ]--      my = bgroup "libjwt"-      mySigning =-            [ My.signSimple-            , My.signCustomClaims-            , My.signWithNs-            , My.signComplexCustomClaims-            ]-      myDecoding =-            [ My.decodeSimple-            , My.decodeCustomClaims-            , My.decodeWithNs-            , My.decodeComplexCustomClaims-            ]+      my   = bgroup "libjwt"
bench/Benchmarks/Jose.hs view
@@ -2,14 +2,8 @@ {-# LANGUAGE RecordWildCards #-}  module Benchmarks.Jose-  ( signSimple-  , decodeSimple-  , signCustomClaims-  , decodeCustomClaims-  , signCustomClaimsWithNs-  , decodeCustomClaimsWithNs-  , signComplexClaims-  , decodeComplexClaims+  ( signing+  , decoding   ) where @@ -60,6 +54,18 @@  import           Data.UUID                      ( UUID ) import           Data.List.NonEmpty             ( NonEmpty )++signing :: [(Alg, JWK) -> Benchmark]+signing =+  [signSimple, signCustomClaims, signCustomClaimsWithNs, signComplexClaims]++decoding :: [(Alg, JWK) -> Benchmark]+decoding =+  [ decodeSimple+  , decodeCustomClaims+  , decodeCustomClaimsWithNs+  , decodeComplexClaims+  ]   doSign :: Alg -> JWK -> ClaimsSet -> IO ByteString
bench/Benchmarks/Libjwt.hs view
@@ -6,14 +6,7 @@ {-# LANGUAGE TypeOperators #-}  module Benchmarks.Libjwt-  ( signSimple-  , signCustomClaims-  , signWithNs-  , signComplexCustomClaims-  , decodeSimple-  , decodeCustomClaims-  , decodeWithNs-  , decodeComplexCustomClaims+  ( signing, decoding   ) where @@ -42,6 +35,22 @@  import           Prelude                 hiding ( exp ) +signing :: SigningKey k => [Algorithm k -> Benchmark]+signing =+  [ signSimple+  , signCustomClaims+  , signWithNs+  , signComplexCustomClaims+  ]++decoding :: SigningKey k => [Algorithm k -> Benchmark]+decoding =+  [ decodeSimple+  , decodeCustomClaims+  , decodeWithNs+  , decodeComplexCustomClaims+  ]+ basePayload :: BenchEnv -> Payload Empty 'NoNs basePayload LocalEnv {..} = def { iss = Iss (Just "benchmarks")                                 , aud = Aud ["https://example.com"]@@ -52,31 +61,27 @@                                 }  prepareToken-  :: Encode (PrivateClaims pc ns)-  => Alg-  -> (Alg -> BenchEnv -> Jwt pc ns)+  :: (SigningKey k, Encode (PrivateClaims pc ns))+  => Algorithm k+  -> (BenchEnv -> Payload pc ns)   -> IO ByteString-prepareToken a jwt = getToken . signJwt . jwt a <$> localEnv+prepareToken a mkPayload = getToken . sign a . mkPayload <$> localEnv    type SimpleJwt = Jwt Empty 'NoNs -mkSimpleJwt :: Alg -> BenchEnv -> SimpleJwt-mkSimpleJwt a e = Jwt { header = Header a JWT, payload = basePayload e }--signSimple :: Alg -> Benchmark+signSimple :: SigningKey k => Algorithm k -> Benchmark signSimple a = env localEnv $ bench "simple" . nf benchmark-  where benchmark = getToken . signJwt . mkSimpleJwt a+  where benchmark = getToken . sign a . basePayload -decodeSimple :: Alg -> Benchmark+decodeSimple :: SigningKey k => Algorithm k -> Benchmark decodeSimple a =-  env (prepareToken a mkSimpleJwt) $ bench "simple" . whnfAppIO benchmark+  env (prepareToken a basePayload) $ bench "simple" . whnfAppIO benchmark  where-  benchmark :: ByteString -> IO (ValidationNEL ValidationFailure SimpleJwt)-  benchmark =-    fmap (fmap getValid)-      . jwtFromByteString validationSettings (checkIssuer "benchmarks") a+  benchmark+    :: ByteString -> IO (ValidationNEL ValidationFailure (Validated SimpleJwt))+  benchmark = jwtFromByteString validationSettings (checkIssuer "benchmarks") a   @@ -85,27 +90,30 @@       '["userName" ->> Text, "isRoot" ->> Bool, "clientId" ->> UUID, "created" ->> UTCTime, "scope" ->> Flag Scope]       'NoNs -mkCustomJwt :: Alg -> BenchEnv -> CustomJwt-mkCustomJwt a e@LocalEnv {..} = Jwt-  { header  = Header a JWT-  , payload = (basePayload e)-                { privateClaims = toPrivateClaims-                                    ( #userName ->> shortPrintableText-                                    , #isRoot ->> flipBit-                                    , #clientId ->> uuid-                                    , #created ->> currentTimeUtc-                                    , #scope ->> Flag Login-                                    )-                }+customPayload :: BenchEnv+                   -> Payload+                        '["userName" ->> Text, "isRoot" ->> Bool, "clientId" ->> UUID,+                          "created" ->> UTCTime, "scope" ->> Flag Scope]+                        'NoNs+customPayload e@LocalEnv {..} = (basePayload e)+  { privateClaims = toPrivateClaims+                      ( #userName ->> shortPrintableText+                      , #isRoot ->> flipBit+                      , #clientId ->> uuid+                      , #created ->> currentTimeUtc+                      , #scope ->> Flag Login+                      )   } -signCustomClaims :: Alg -> Benchmark+signCustomClaims :: SigningKey k => Algorithm k -> Benchmark signCustomClaims a = env localEnv $ bench "custom-claims" . nf benchmark-  where benchmark = getToken . signJwt . mkCustomJwt a+  where benchmark = getToken . sign a . customPayload -decodeCustomClaims :: Alg -> Benchmark+decodeCustomClaims :: SigningKey k => Algorithm k -> Benchmark decodeCustomClaims a =-  env (prepareToken a mkCustomJwt) $ bench "custom-claims" . whnfAppIO benchmark+  env (prepareToken a customPayload)+    $ bench "custom-claims"+    . whnfAppIO benchmark  where   benchmark     :: ByteString -> IO (ValidationNEL ValidationFailure (Validated CustomJwt))@@ -118,28 +126,29 @@       '["userName" ->> Text, "isRoot" ->> Bool, "clientId" ->> UUID, "created" ->> UTCTime, "scope" ->> Flag Scope]       ( 'SomeNs "https://www.example.com/test") -mkCustomJwtWithNs :: Alg -> BenchEnv -> CustomJwtWithNs-mkCustomJwtWithNs a e@LocalEnv {..} = Jwt-  { header  = Header a JWT-  , payload = (basePayload e)-                { privateClaims = toPrivateClaims $ withNs-                                    (Ns @"https://www.example.com/test")-                                    ( #userName ->> shortPrintableText-                                    , #isRoot ->> flipBit-                                    , #clientId ->> uuid-                                    , #created ->> currentTimeUtc-                                    , #scope ->> Flag Login-                                    )-                }+customPayloadWithNs :: BenchEnv+                         -> Payload+                              '["userName" ->> Text, "isRoot" ->> Bool, "clientId" ->> UUID,+                                "created" ->> UTCTime, "scope" ->> Flag Scope]+                              ('SomeNs "https://www.example.com/test")+customPayloadWithNs e@LocalEnv {..} = (basePayload e)+  { privateClaims = toPrivateClaims $ withNs+                      (Ns @"https://www.example.com/test")+                      ( #userName ->> shortPrintableText+                      , #isRoot ->> flipBit+                      , #clientId ->> uuid+                      , #created ->> currentTimeUtc+                      , #scope ->> Flag Login+                      )   } -signWithNs :: Alg -> Benchmark+signWithNs :: SigningKey k => Algorithm k -> Benchmark signWithNs a = env localEnv $ bench "custom-claims-with-ns" . nf benchmark-  where benchmark = getToken . signJwt . mkCustomJwtWithNs a+  where benchmark = getToken . sign a . customPayloadWithNs -decodeWithNs :: Alg -> Benchmark+decodeWithNs :: SigningKey k => Algorithm k -> Benchmark decodeWithNs a =-  env (prepareToken a mkCustomJwtWithNs)+  env (prepareToken a customPayloadWithNs)     $ bench "custom-claims-with-ns"     . whnfAppIO benchmark  where@@ -155,37 +164,39 @@       '["user_name" ->> Text, "is_root" ->> Bool, "client_id" ->> UUID, "created" ->> UTCTime, "accounts" ->> NonEmpty (UUID, Text), "scopes" ->> [Flag Scope], "emails" ->> [String]]       'NoNs -mkComplexJwt :: Alg -> BenchEnv -> ComplexJwt-mkComplexJwt a e@LocalEnv {..} = Jwt-  { header  = Header a JWT-  , payload = (basePayload e)-                { privateClaims = toPrivateClaims-                                    ( #user_name ->> shortPrintableText-                                    , #is_root ->> flipBit-                                    , #client_id ->> uuid-                                    , #created ->> currentTimeUtc-                                    , #accounts ->> accountList-                                    , #scopes-                                      ->> [ Flag Login-                                          , Flag Extended-                                          , Flag UserRead-                                          , Flag UserWrite-                                          , Flag AccountRead-                                          , Flag AccountWrite-                                          ]-                                    , #emails ->> emailsList-                                    )-                }+complexPayload :: BenchEnv+                    -> Payload+                         '["user_name" ->> Text, "is_root" ->> Bool, "client_id" ->> UUID,+                           "created" ->> UTCTime, "accounts" ->> NonEmpty (UUID, Text),+                           "scopes" ->> [Flag Scope], "emails" ->> [String]]+                         'NoNs+complexPayload e@LocalEnv {..} = (basePayload e)+  { privateClaims = toPrivateClaims+                      ( #user_name ->> shortPrintableText+                      , #is_root ->> flipBit+                      , #client_id ->> uuid+                      , #created ->> currentTimeUtc+                      , #accounts ->> accountList+                      , #scopes+                        ->> [ Flag Login+                            , Flag Extended+                            , Flag UserRead+                            , Flag UserWrite+                            , Flag AccountRead+                            , Flag AccountWrite+                            ]+                      , #emails ->> emailsList+                      )   } -signComplexCustomClaims :: Alg -> Benchmark+signComplexCustomClaims :: SigningKey k => Algorithm k -> Benchmark signComplexCustomClaims a =   env localEnv $ bench "complex-claims" . nf benchmark-  where benchmark = getToken . signJwt . mkComplexJwt a+  where benchmark = getToken . sign a . complexPayload -decodeComplexCustomClaims :: Alg -> Benchmark+decodeComplexCustomClaims :: SigningKey k => Algorithm k -> Benchmark decodeComplexCustomClaims a =-  env (prepareToken a mkComplexJwt)+  env (prepareToken a complexPayload)     $ bench "complex-claims"     . whnfAppIO benchmark  where
libjwt-typed.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.4 name:                libjwt-typed-version:             0.1+version:             0.2 synopsis:            A Haskell implementation of JSON Web Token (JWT) description:         A Haskell implementation of JSON Web Token (JWT)                      .@@ -49,6 +49,7 @@                      CHANGELOG.md tested-with:         GHC == 8.8.3                      GHC == 8.10.1+                     GHC == 8.10.2  source-repository head   type:                git@@ -108,6 +109,7 @@                        Libjwt.Encoding                        Libjwt.Decoding                        Libjwt.Keys+                       Libjwt.Algorithms                        Libjwt.JsonByteString                        Libjwt.FFI.Jwt   other-modules:       Libjwt.FFI.Jsmn
+ src/Libjwt/Algorithms.hs view
@@ -0,0 +1,112 @@+--   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/.++{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- | Algorithms used to sign and validate JWT signatures+module Libjwt.Algorithms+  ( Algorithm(..)+  , RsaKey+  , EcKey+  , toHeaderAlg+  , jwtAlgWithKey+  )+where++import           Libjwt.Header                  ( Alg(..) )+import           Libjwt.FFI.Libjwt+import           Libjwt.Keys++import           Data.Kind                      ( Constraint )++import           GHC.TypeLits++-- | Cryptographic algorithm used to secure the JWT+data Algorithm k where+  -- | HMAC SHA-256 (secret key must be __at least 256 bits in size__)+  HMAC256  ::Secret  -> Algorithm Secret+  -- | HMAC SHA-384 (secret key must be __at least 384 bits in size__)+  HMAC384  ::Secret  -> Algorithm Secret+  -- | HMAC SHA-512 (secret key must be __at least 512 bits in size__)+  HMAC512  ::Secret  -> Algorithm Secret+  -- | RSASSA-PKCS1-v1_5 SHA-256 (a key of size __2048 bits or larger__ must be used with this algorithm)+  RSA256   ::RsaKey r => r -> Algorithm r+  -- | RSASSA-PKCS1-v1_5 SHA-384 (a key of size __2048 bits or larger__ must be used with this algorithm) +  RSA384   ::RsaKey r => r -> Algorithm r+  -- | RSASSA-PKCS1-v1_5 SHA-512 (a key of size __2048 bits or larger__ must be used with this algorithm)+  RSA512   ::RsaKey r => r -> Algorithm r+  -- | ECDSA with P-256 curve and SHA-256+  ECDSA256 ::EcKey e  => e -> Algorithm e+  -- | ECDSA with P-384 curve and SHA-384+  ECDSA384 ::EcKey e  => e -> Algorithm e+  -- | ECDSA with P-521 curve and SHA-512+  ECDSA512 ::EcKey e  => e -> Algorithm e+  -- | None+  AlgNone  ::Algorithm ()++deriving stock instance Show k => Show (Algorithm k)++type family RsaKey t :: Constraint where+  RsaKey RsaKeyPair = ()+  RsaKey RsaPubKey  = ()+  RsaKey a          = TypeError ('Text "RSASSA-PKCS-v1_5 cannot be used with " ':<>: 'ShowType a)++type family EcKey t :: Constraint where+  EcKey EcKeyPair = ()+  EcKey EcPubKey  = ()+  EcKey a         = TypeError ('Text "ECDSA cannot be used with " ':<>: 'ShowType a)++jwtAlgWithKey :: Algorithm k -> (JwtAlgT, k)+jwtAlgWithKey (HMAC256  secret) = (jwtAlgHs256, secret)+jwtAlgWithKey (HMAC384  secret) = (jwtAlgHs384, secret)+jwtAlgWithKey (HMAC512  secret) = (jwtAlgHs512, secret)+jwtAlgWithKey (RSA256   key   ) = (jwtAlgRs256, key)+jwtAlgWithKey (RSA384   key   ) = (jwtAlgRs384, key)+jwtAlgWithKey (RSA512   key   ) = (jwtAlgRs512, key)+jwtAlgWithKey (ECDSA256 key   ) = (jwtAlgEs256, key)+jwtAlgWithKey (ECDSA384 key   ) = (jwtAlgEs384, key)+jwtAlgWithKey (ECDSA512 key   ) = (jwtAlgEs512, key)+jwtAlgWithKey AlgNone           = (jwtAlgNone, ())++-- | Get the header parameter "alg" from the algorithm+--+-- +------------+-----------++-- |Algorithm   |  'alg'    |   +-- +============+===========++-- |'HMAC256'   |  'HS256'  |+-- +------------+-----------++-- |'HMAC384'   |  'HS384'  |+-- +------------+-----------++-- |'HMAC512'   |  'HS512'  |+-- +------------+-----------++-- |'RSA256'    |  'RS256'  | +-- +------------+-----------++-- |'RSA384'    |  'RS384'  |+-- +------------+-----------++-- |'RSA512'    |  'RS512'  |+-- +------------+-----------++-- |'ECDSA256'  |  'ES256'  |+-- +------------+-----------++-- |'ECDSA384'  |  'ES384'  |+-- +------------+-----------++-- |'ECDSA512'  |  'ES512'  |+-- +------------+-----------++--+toHeaderAlg :: Algorithm k -> Alg+toHeaderAlg (HMAC256  _) = HS256+toHeaderAlg (HMAC384  _) = HS384+toHeaderAlg (HMAC512  _) = HS512+toHeaderAlg (RSA256   _) = RS256+toHeaderAlg (RSA384   _) = RS384+toHeaderAlg (RSA512   _) = RS512+toHeaderAlg (ECDSA256 _) = ES256+toHeaderAlg (ECDSA384 _) = ES384+toHeaderAlg (ECDSA512 _) = ES512+toHeaderAlg AlgNone      = None
src/Libjwt/FFI/Jwt.hs view
@@ -70,6 +70,7 @@                                                 , useAsCString                                                 , packCString                                                 , packCStringLen+                                                , null                                                 ) import           Data.ByteString.Unsafe         ( unsafePackMallocCString                                                 , unsafeUseAsCStringLen@@ -85,6 +86,8 @@  import           System.IO.Unsafe               ( unsafePerformIO ) +import           Prelude                 hiding ( null )+ -- | IO restricted to calling /libjwt/ and /jsmn/ newtype JwtIO a = JIO (IO a)  deriving newtype (Functor, Applicative, Monad, MonadThrow, MonadCatch)@@ -98,9 +101,9 @@ mkJwtT :: JwtIO JwtT mkJwtT = JIO $ mkJwtT_ "jwt_new" c_jwt_new -jwtDecode :: Maybe ByteString -> ByteString -> JwtIO JwtT-jwtDecode maybeKey token = JIO-  $ maybe ($ (nullPtr, 0)) unsafeUseAsCStringLen maybeKey doDecode+jwtDecode :: ByteString -> ByteString -> JwtIO JwtT+jwtDecode key token = JIO+  $ (if null key then ($ (nullPtr, 0)) else unsafeUseAsCStringLen key) doDecode  where   doDecode (p_key, key_len) = useAsCString token $ \p_token ->     mkJwtT_ "jwt_decode"
src/Libjwt/Header.hs view
@@ -15,61 +15,61 @@   ) where +import           Libjwt.Decoding import           Libjwt.Encoding-import           Libjwt.Keys-import           Libjwt.FFI.Libjwt import           Libjwt.FFI.Jwt--import           Control.Monad                  ( when )+import           Libjwt.FFI.Libjwt  import           Data.ByteString                ( ByteString )-import qualified Data.ByteString               as ByteString +import qualified Data.CaseInsensitive          as CI+ -- | @"alg"@ header parameter data Alg = None-         -- | HMAC SHA-256 (secret key must be __at least 256 bits in size__)-         | HS256 Secret-         -- | HMAC SHA-384 (secret key must be __at least 384 bits in size__)-         | HS384 Secret-         -- | HMAC SHA-512 (secret key must be __at least 512 bits in size__)-         | HS512 Secret-         -- | RSASSA-PKCS1-v1_5 SHA-256 (a key of size __2048 bits or larger__ must be used with this algorithm)-         | RS256 RsaKeyPair-         -- | RSASSA-PKCS1-v1_5 SHA-384 (a key of size __2048 bits or larger__ must be used with this algorithm)-         | RS384 RsaKeyPair-         -- | RSASSA-PKCS1-v1_5 SHA-512 (a key of size __2048 bits or larger__ must be used with this algorithm)-         | RS512 RsaKeyPair-         -- | ECDSA with P-256 curve and SHA-256-         | ES256 EcKeyPair-         -- | ECDSA with P-384 curve and SHA-384-         | ES384 EcKeyPair-         -- | ECDSA with P-521 curve and SHA-512-         | ES512 EcKeyPair+         | HS256+         | HS384+         | HS512+         | RS256+         | RS384+         | RS512+         | ES256+         | ES384+         | ES512   deriving stock (Show, Eq) +instance Decode Alg where+  decode = fmap matchJwtAlg . jwtGetAlg+   where+    matchJwtAlg jwtAlg | jwtAlg == jwtAlgHs256 = HS256+                       | jwtAlg == jwtAlgHs384 = HS384+                       | jwtAlg == jwtAlgHs512 = HS512+                       | jwtAlg == jwtAlgRs256 = RS256+                       | jwtAlg == jwtAlgRs384 = RS384+                       | jwtAlg == jwtAlgRs512 = RS512+                       | jwtAlg == jwtAlgEs256 = ES256+                       | jwtAlg == jwtAlgEs384 = ES384+                       | jwtAlg == jwtAlgEs512 = ES512+                       | otherwise             = None+ -- | @"typ"@ header parameter data Typ = JWT | Typ (Maybe ByteString)   deriving stock (Show, Eq) +instance Encode Typ where+  encode (Typ (Just s)) = addHeader "typ" s+  encode _              = nullEncode++instance Decode Typ where+  decode =+    fmap+        ( maybe (Typ Nothing)+        $ \s -> if CI.mk s == "jwt" then JWT else Typ $ Just s+        )+      . getHeader "typ"+ -- | JWT header representation data Header = Header { alg :: Alg, typ :: Typ }   deriving stock (Show, Eq) -instance Encode Header where-  encode header jwt = encodeAlg (alg header) jwt >> encodeTyp (typ header) jwt-   where-    encodeAlg None           = jwtSetAlg jwtAlgNone ByteString.empty >> forceTyp-    encodeAlg (HS256 secret) = jwtSetAlg jwtAlgHs256 $ reveal secret-    encodeAlg (HS384 secret) = jwtSetAlg jwtAlgHs384 $ reveal secret-    encodeAlg (HS512 secret) = jwtSetAlg jwtAlgHs512 $ reveal secret-    encodeAlg (RS256 pem   ) = jwtSetAlg jwtAlgRs256 $ privKey pem-    encodeAlg (RS384 pem   ) = jwtSetAlg jwtAlgRs384 $ privKey pem-    encodeAlg (RS512 pem   ) = jwtSetAlg jwtAlgRs512 $ privKey pem-    encodeAlg (ES256 pem   ) = jwtSetAlg jwtAlgEs256 $ ecPrivKey pem-    encodeAlg (ES384 pem   ) = jwtSetAlg jwtAlgEs384 $ ecPrivKey pem-    encodeAlg (ES512 pem   ) = jwtSetAlg jwtAlgEs512 $ ecPrivKey pem--    encodeTyp (Typ (Just s)) = addHeader "typ" s-    encodeTyp _              = nullEncode--    forceTyp = when (typ header == JWT) . addHeader "typ" "JWT"+instance Decode Header where+  decode jwt = Header <$> decode jwt <*> decode jwt
src/Libjwt/Jwt.hs view
@@ -6,7 +6,6 @@  {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-}@@ -19,7 +18,7 @@   , Encoded   , getToken   , sign-  , signJwt+  , sign'   , Decoded   , getDecoded   , decodeString@@ -32,6 +31,7 @@   ) where +import           Libjwt.Algorithms import           Libjwt.Encoding import           Libjwt.Exceptions              ( SomeDecodeException                                                 , AlgorithmMismatch(..)@@ -51,13 +51,13 @@ import           Control.Monad.Extra            ( unlessM )  import           Control.Monad.Time-import           Control.Monad                  ( (<=<) )+import           Control.Monad                  ( (<=<)+                                                , when+                                                )  import           Data.ByteString                ( ByteString ) import qualified Data.ByteString.Char8         as C8 -import qualified Data.CaseInsensitive          as CI- import           GHC.IO.Exception               ( IOErrorType(InvalidArgument) )  import           System.IO.Error                ( ioeGetErrorType )@@ -67,38 +67,58 @@ deriving stock instance Show (PrivateClaims pc ns) => Show (Jwt pc ns) deriving stock instance Eq (PrivateClaims pc ns) => Eq (Jwt pc ns) -instance Encode (PrivateClaims pc ns) => Encode (Jwt pc ns) where-  encode Jwt { header, payload } jwt = encode payload jwt >> encode header jwt- -- | base64url-encoded value of type @t@ newtype Encoded t = MkEncoded { getToken :: ByteString -- ^ octets of the UTF-8 representation                               }   deriving stock (Show, Eq) --- | Compute the encoded JWT value with the JWS Signature in the manner defined for the algorithm @alg@ .+-- | Compute the encoded JWT value with the JWS Signature in the manner defined for the @algorithm@. --   'typ' of the JWT 'Header' is set to "JWT"+--   'alg' of the JWT 'Header' is set according to the algorithm used (see 'toHeaderAlg') -- --   Creates the serialized ouput, that is:  --   @ --   BASE64URL(UTF8(JWT Header)) || . || BASE64URL(JWT Payload) || . || BASE64URL(JWT Signature) --   @ sign-  :: Encode (PrivateClaims pc ns) => Alg -> Payload pc ns -> Encoded (Jwt pc ns)-sign alg payload =-  signJwt $ Jwt { header = Header { alg, typ = JWT }, payload }+  :: (Encode (PrivateClaims pc ns), SigningKey k)+  => Algorithm k -- ^ algorithm+  -> Payload pc ns -- ^ JWT payload+  -> Encoded (Jwt pc ns)+sign = sign' JWT --- | Compute the encoded JWT value with the JWS Signature in the manner defined for the algorithm 'alg' present in the JWT's 'header' .+-- | Compute the encoded JWT value with the JWS Signature in the manner defined for the @algorithm@ .+--   'typ' of the JWT 'Header' is set to @typ@+--   'alg' of the JWT 'Header' is set according to the algorithm used (see 'toHeaderAlg') -- --   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)-signJwt it = MkEncoded $ unsafePerformJwtIO signTokenJwtIo+sign'+  :: (Encode (PrivateClaims pc ns), SigningKey k)+  => Typ -- ^ typ+  -> Algorithm k -- ^ algorithm+  -> Payload pc ns -- ^ JWT payload+  -> Encoded (Jwt pc ns)+sign' ty algorithm = signJwt jwtAlg (getSigningKey key) ty+  where (jwtAlg, key) = jwtAlgWithKey algorithm++signJwt+  :: Encode (PrivateClaims pc ns)+  => JwtAlgT+  -> ByteString+  -> Typ+  -> Payload pc ns+  -> Encoded (Jwt pc ns)+signJwt jwtAlg key ty it = MkEncoded $ unsafePerformJwtIO signTokenJwtIo  where   signTokenJwtIo = do     jwt <- mkJwtT     encode it jwt+    encode ty jwt+    jwtSetAlg jwtAlg key jwt+    when (jwtAlg == jwtAlgNone && ty == JWT) $ addHeader "typ" "JWT" jwt     jwtEncode jwt  {-# NOINLINE signJwt #-}@@ -109,17 +129,17 @@  -- | See 'decodeByteString' decodeString-  :: (MonadThrow m, Decode (PrivateClaims pc ns))-  => Alg+  :: (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k)+  => Algorithm k   -> String   -> m (Decoded (Jwt pc ns))-decodeString alg = decodeByteString alg . C8.pack+decodeString algorithm = decodeByteString algorithm . C8.pack  -- | 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,---       (1) input JWT signature matches,+--       (1) input JWT signature matches the @algorithm@, --       (1) the correct algorithm was used, --       (1) all required fields are present. --@@ -127,61 +147,32 @@ --   If step 3 fails, 'AlgorithmMismatch' will be thrown. --   If the last step fails, 'Libjwt.Exceptions.MissingClaim' will be thrown. decodeByteString-  :: forall ns pc m-   . (MonadThrow m, Decode (PrivateClaims pc ns))-  => Alg-  -> ByteString+  :: forall ns pc m k+   . (MonadThrow m, Decode (PrivateClaims pc ns), DecodingKey k)+  => Algorithm k -- ^ algorithm used to verify the signature+  -> ByteString -- ^ token   -> m (Decoded (Jwt pc ns))-decodeByteString alg token = either throwM (pure . MkDecoded)+decodeByteString algorithm token = either throwM (pure . MkDecoded)   $ unsafePerformJwtIO decodeTokenJwtIo  where   decodeTokenJwtIo :: JwtIO (Either SomeDecodeException (Jwt pc ns))-  decodeTokenJwtIo = try $ do-    jwt <- safeJwtDecode alg token-    unlessM (matchAlg alg <$> jwtGetAlg jwt) $ throwM AlgorithmMismatch-    Jwt <$> decodeHeader jwt <*> decode jwt+  decodeTokenJwtIo =+    let (jwtAlg, key) = jwtAlgWithKey algorithm+    in  try $ do+          jwt <- safeJwtDecode (getDecodingKey key) token+          unlessM ((== jwtAlg) <$> jwtGetAlg jwt) $ throwM AlgorithmMismatch+          Jwt <$> decode jwt <*> decode jwt -  decodeHeader = fmap (Header alg) . decodeTyp -  decodeTyp =-    fmap-        ( maybe (Typ Nothing)-        $ \s -> if CI.mk s == "jwt" then JWT else Typ $ Just s-        )-      . getHeader "typ"--  matchAlg (HS256 _) = (== jwtAlgHs256)-  matchAlg (HS384 _) = (== jwtAlgHs384)-  matchAlg (HS512 _) = (== jwtAlgHs512)-  matchAlg (RS256 _) = (== jwtAlgRs256)-  matchAlg (RS384 _) = (== jwtAlgRs384)-  matchAlg (RS512 _) = (== jwtAlgRs512)-  matchAlg (ES256 _) = (== jwtAlgEs256)-  matchAlg (ES384 _) = (== jwtAlgEs384)-  matchAlg (ES512 _) = (== jwtAlgEs512)-  matchAlg None      = (== jwtAlgNone)- {-# NOINLINE decodeByteString #-} -safeJwtDecode :: Alg -> ByteString -> JwtIO JwtT-safeJwtDecode alg token =-  catchIf (\e -> ioeGetErrorType e == InvalidArgument)-          (jwtDecode (getKey alg) token)+safeJwtDecode :: ByteString -> ByteString -> JwtIO JwtT+safeJwtDecode key token =+  catchIf (\e -> ioeGetErrorType e == InvalidArgument) (jwtDecode key token)     $ const     $ throwM     $ DecodeException     $ C8.unpack token- where-  getKey (HS256 secret) = Just $ reveal secret-  getKey (HS384 secret) = Just $ reveal secret-  getKey (HS512 secret) = Just $ reveal secret-  getKey (RS256 pem   ) = Just $ pubKey pem-  getKey (RS384 pem   ) = Just $ pubKey pem-  getKey (RS512 pem   ) = Just $ pubKey pem-  getKey (ES256 pem   ) = Just $ ecPubKey pem-  getKey (ES384 pem   ) = Just $ ecPubKey pem-  getKey (ES512 pem   ) = Just $ ecPubKey pem-  getKey None           = Nothing  -- | Successfully validated value of type @t@ newtype Validated t = MkValid { getValid :: t }@@ -210,13 +201,14 @@  -- | See 'jwtFromByteString' jwtFromString-  :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m)+  :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k)   => ValidationSettings   -> JwtValidation pc ns-  -> Alg+  -> Algorithm k   -> String   -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))-jwtFromString settings v alg = validateJwt settings v <=< decodeString alg+jwtFromString settings v algorithm =+  validateJwt settings v <=< decodeString algorithm  -- | @jwtFromByteString = 'validateJwt' settings v <=< 'decodeByteString' alg@ --@@ -227,7 +219,7 @@ --    --       (1) @token@ is a valid UTF-8 encoded representation of a completely valid JSON object, --       (1) input JWT signature matches,---       (1) the correct algorithm was used,+--       (1) the correct @algorithm@ was used, --       (1) all required fields are present. -- --   If steps 1-2 are unuccessful, 'DecodeException' will be thrown.@@ -248,12 +240,12 @@ -- --   'aud' claim is checked against 'appName'. jwtFromByteString-  :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m)+  :: (Decode (PrivateClaims pc ns), MonadTime m, MonadThrow m, DecodingKey k)   => ValidationSettings -- ^ 'leeway' and 'appName'   -> JwtValidation pc ns -- ^ additional validation rules -  -> Alg -- ^ algorithm used to verify the signature+  -> Algorithm k -- ^ algorithm used to verify the signature   -> ByteString -- ^ base64url-encoded representation (a token)   -> m (ValidationNEL ValidationFailure (Validated (Jwt pc ns)))-jwtFromByteString settings v alg =-  validateJwt settings v <=< decodeByteString alg+jwtFromByteString settings v algorithm =+  validateJwt settings v <=< decodeByteString algorithm 
src/Libjwt/Keys.hs view
@@ -10,11 +10,16 @@ module Libjwt.Keys   ( Secret(..)   , RsaKeyPair(..)+  , RsaPubKey(..)   , EcKeyPair(..)+  , EcPubKey(..)+  , SigningKey(..)+  , DecodingKey(..)   ) where  import           Data.ByteString                ( ByteString )+import qualified Data.ByteString               as ByteString  import qualified Data.ByteString.UTF8          as UTF8 @@ -95,6 +100,25 @@ data RsaKeyPair = FromRsaPem { privKey :: ByteString, pubKey :: ByteString }   deriving stock (Show, Eq) +-- | RSA public-key (/PEM-encoded/) used in /RSA/ algorithms for decoding+--+-- >+-- >rsaPub =+-- >  let public = C8.pack $ unlines+-- >        [ "-----BEGIN PUBLIC KEY-----"+-- >        , "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwCXp2P+qboao0tjUyU+D"+-- >        , "3YI+sgBn8dkGaxOvPFLBFQMNkhbL0HEoRKNnQCubZNc0jXnMK5hCeGRnDS7lYclR"+-- >        , "OXocRWUn5s2W3jP5xn7lM4otIpuE3FStthMCrPSEQiBCXE4cyKiHaZqmbqXlHAHV"+-- >        , "EuGMM7oddiB6s3zjwf2h1v0SEiHf5ZFzTVarStablqh6wVDAiYyM+8aUM0x9p3Jc"+-- >        , "aWW+eDk/UU3jCfCke7R3t2rbD1ZCj1cO08Uir3Lhf65TfU+iIrgLU3umV4B3gRcp"+-- >        , "d8iz0ZTLaG8Qnm0GsPQjR3PTZYECxEnFaRgXcQLHYYMAW9YaX6T3rlTGZAaP5Ybo"+-- >        , "xQIDAQAB"+-- >        , "-----END PUBLIC KEY-----"+-- >        ]+-- >  in  FromRsaPub { rsaPublicKey = public }+newtype RsaPubKey = FromRsaPub { rsaPublicKey :: ByteString }+  deriving stock (Show, Eq)+ -- | Elliptic curves parameters used in /ECDSA/ algorithms -- --   According to RFC, the following curves are to be used:@@ -132,3 +156,56 @@ -- >   in  FromEcPem { ecPrivKey = private, ecPubKey = public } data EcKeyPair = FromEcPem { ecPrivKey :: ByteString, ecPubKey :: ByteString }   deriving stock (Show, Eq)++-- | Elliptic curve public key (/PEM-encoded/) used in /ECDSA/ algorithms for decoding+--+--+-- > ecPub =+-- >   let public = C8.pack $ unlines+-- >         [ "-----BEGIN PUBLIC KEY-----"+-- >         , "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKZL0X84AvdnGZdsIdAS60OnvF3FN"+-- >         , "lsrCnaXRoJUVdOYZldzb4po2uDXF5W58DS8C31fV+z+0lTG5RvuAqfkdbA=="+-- >         , "-----END PUBLIC KEY-----"+-- >         ]+-- >   in  FromEcPub { ecPublicKey = public }+newtype EcPubKey = FromEcPub { ecPublicKey :: ByteString }+  deriving stock (Show, Eq)++-- | Class of keys that can be used (only) for decoding+class DecodingKey k where+  getDecodingKey :: k -> ByteString++instance DecodingKey Secret where+  getDecodingKey = reveal++instance DecodingKey RsaKeyPair where+  getDecodingKey = pubKey++instance DecodingKey RsaPubKey where+  getDecodingKey = rsaPublicKey++instance DecodingKey EcKeyPair where+  getDecodingKey = ecPubKey++instance DecodingKey EcPubKey where+  getDecodingKey = ecPublicKey++instance DecodingKey () where+  getDecodingKey _ = ByteString.empty++-- | Class of keys that can be used for signing+class DecodingKey k => SigningKey k where+  getSigningKey :: k -> ByteString++instance SigningKey Secret where+  getSigningKey = reveal++instance SigningKey RsaKeyPair where+  getSigningKey = privKey++instance SigningKey EcKeyPair where+  getSigningKey = ecPrivKey++instance SigningKey () where+  getSigningKey _ = ByteString.empty+
src/Web/Libjwt.hs view
@@ -136,36 +136,36 @@  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   |-+----------+---------------------------------------+++------------+---------------------------------------+-------------++|Algorithm   |  Description                          |  Key type   |++============+=======================================+=============++|'HMAC256'   |  HMAC with SHA-256                    |  'Secret'   |++------------+---------------------------------------+             |+|'HMAC384'   |  HMAC with SHA-384                    |             |++------------+---------------------------------------+             |+|'HMAC512'   |  HMAC with SHA-512                    |             |++------------+---------------------------------------+-------------++|'RSA256'    |  RSASSA-PKCS1-v1_5 with SHA-256       | 'RsaKeyPair'|++------------+---------------------------------------+ 'RsaPubKey' |+|'RSA384'    |  RSASSA-PKCS1-v1_5 with SHA-384       |             |++------------+---------------------------------------+             |+|'RSA512'    |  RSASSA-PKCS1-v1_5 with SHA-512       |             |++------------+---------------------------------------+-------------++|'ECDSA256'  |  ECDSA with curve P-256 and SHA-256   | 'EcKeyPair' |++------------+---------------------------------------+ 'EcPubKey'  |+|'ECDSA384'  |  ECDSA with curve P-384 and SHA-384   |             |++------------+---------------------------------------+             |+|'ECDSA512'  |  ECDSA with curve P-521 and SHA-512   |             |++------------+---------------------------------------+-------------+  The complete example:   @ {-# LANGUAGE OverloadedStrings #-} -hmac512 :: 'Alg'+hmac512 :: 'Algorithm' 'Secret' hmac512 =-    'HS512'+    'HMAC512'         "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\\         \\YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\\         \\Y2IwMDZhYWY1MjY1OTQgIC0K"@@ -244,6 +244,7 @@  module Web.Libjwt     ( module Libjwt.Jwt+    , module Libjwt.Algorithms     , module Libjwt.Exceptions     , module Libjwt.Header     , module Libjwt.Keys@@ -259,6 +260,7 @@     ) where +import           Libjwt.Algorithms              ( Algorithm(..) ) import           Libjwt.ASCII                   ( ASCII(..) ) import           Libjwt.Decoding                ( Decode ) import           Libjwt.Encoding                ( Encode )@@ -269,7 +271,7 @@ import           Libjwt.Header import           Libjwt.Jwt                     ( Jwt(..)                                                 , sign-                                                , signJwt+                                                , sign'                                                 , Encoded                                                 , getToken                                                 , jwtFromString
src/Web/Libjwt/Tutorial.hs view
@@ -48,9 +48,9 @@ instance ToPrivateClaims UserClaims instance FromPrivateClaims UserClaims -hmac512 :: Alg+hmac512 :: Algorithm Secret hmac512 =-  HS512+  HMAC512     "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\     \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\     \Y2IwMDZhYWY1MjY1OTQgIC0K"
test/Env.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE GeneralisedNewtypeDeriving #-} {-# LANGUAGE NamedFieldPuns #-} @@ -8,6 +9,7 @@                                                 , EcKeyPair(..)                                                 ) + import           Control.Arrow                  ( left )  import           Control.Monad.Catch@@ -22,9 +24,12 @@  import           Data.Either.Extra              ( fromEither ) -import           Data.Time.Clock                ( UTCTime )+import           Data.Time.Clock                ( UTCTime+                                                , NominalDiffTime+                                                ) import           Data.Time.Clock.POSIX          ( POSIXTime                                                 , posixSecondsToUTCTime+                                                , utcTimeToPOSIXSeconds                                                 )  import           Data.UUID                      ( UUID )@@ -144,11 +149,26 @@ newtype TestEnv a = MkTest { runTest :: ReaderT UTCTime (Either SomeException) a }   deriving newtype (Functor, Applicative, Monad, MonadTime, MonadThrow, MonadCatch) +expectationOk :: Expectation+expectationOk = pure ()+ pass :: TestEnv Expectation-pass = MkTest (pure alwaysTrue) where alwaysTrue = pure ()+pass = MkTest (pure expectationOk) +fail :: String -> TestEnv Expectation+fail reason = MkTest (pure $ expectationFailure reason)++withTime :: (UTCTime -> a) -> TestEnv a+withTime f = f <$> currentTime++withEpochTime :: (POSIXTime -> a) -> TestEnv a+withEpochTime f = withTime (f . utcTimeToPOSIXSeconds)++epochTime :: NominalDiffTime+epochTime = 1595386660+ presetTime :: UTCTime-presetTime = posixSecondsToUTCTime 1595386660+presetTime = posixSecondsToUTCTime epochTime  runTestWithPresetTime :: TestEnv a -> Either SomeException a runTestWithPresetTime = runTestWithGivenTime presetTime@@ -179,5 +199,3 @@             ++ displayException e         )     . runTestWithGivenTime t0--
test/Generators.hs view
@@ -2,12 +2,14 @@  {-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeOperators #-}@@ -16,9 +18,8 @@ module Generators   ( JwtString(..)   , JwtText(..)-  , genHeader-  , genJwt-  , shrinkJwt+  , SomeAlgorithm(..)+  , genAlgorithm   ) where @@ -139,27 +140,6 @@ instance Arbitrary a => Arbitrary (Flag a) where   arbitrary = coerce $ arbitrary @a -instance Arbitrary Alg where-  arbitrary = genAlg--genAlg :: Gen Alg-genAlg = elements [hs256, hs512, rs256, rs512, es256, es384, es512, None]- where-  hs256 =-    HS256-      "MWNmYzExODA5OWFjOGM3NDNmMmM5Zjg5ZDc0YTM3M2VhMGNkMzA2MDY3ZjFhZDk5N2I3OTc5Yjdm\-      \NDg3NDBkMiAgLQo"-  hs512 =-    HS512-      "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\-      \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\-      \Y2IwMDZhYWY1MjY1OTQgIC0K"-  rs256 = RS256 E.testRsa2048KeyPair-  rs512 = RS512 E.testRsa2048KeyPair-  es256 = ES256 E.testEcP256KeyPair-  es384 = ES384 E.testEcP384KeyPair-  es512 = ES512 E.testEcP521KeyPair- instance Arbitrary ValidationSettings where   arbitrary = Settings <$> genLeeway <*> arbitrary     where genLeeway = arbitrary `suchThat` (>= 0)@@ -188,21 +168,43 @@   shrink =     shrinkMap (T . T.pack . correctJwtString) $ S . T.unpack . correctJwtText -instance Arbitrary Header where-  arbitrary = genHeader--genHeader :: Gen Header-genHeader = Header <$> genAlg <*> pure JWT+data SomeAlgorithm = forall k . (Show k, SigningKey k) => SomeAlgorithm (Algorithm k)+deriving stock instance Show SomeAlgorithm -instance Arbitrary (PrivateClaims ts ns) => Arbitrary (Jwt ts ns) where-  arbitrary = genJwt-  shrink    = shrinkJwt+instance Arbitrary SomeAlgorithm where+  arbitrary = genAlgorithm -genJwt :: Arbitrary (PrivateClaims ts ns) => Gen (Jwt ts ns)-genJwt = Jwt <$> genHeader <*> arbitrary+genAlgorithm :: Gen SomeAlgorithm+genAlgorithm = elements+  [ SomeAlgorithm hs256+  , SomeAlgorithm hs512+  , SomeAlgorithm rs256+  , SomeAlgorithm rs384+  , SomeAlgorithm rs512+  , SomeAlgorithm es256+  , SomeAlgorithm es384+  , SomeAlgorithm es512+  , SomeAlgorithm AlgNone+  ]+ where+  hs256 =+    HMAC256+      "MWNmYzExODA5OWFjOGM3NDNmMmM5Zjg5ZDc0YTM3M2VhMGNkMzA2MDY3ZjFhZDk5N2I3OTc5YjdmNDg3NDBkMiAgLQo"+  hs512 =+    HMAC512+      "MjZkMDY2OWFiZmRjYTk5YjczZWFiZjYzMmRjMzU5NDYyMjMxODBjMTg3ZmY5OTZjM2NhM2NhN2Mx\+          \YzFiNDNlYjc4NTE1MjQxZGI0OWM1ZWI2ZDUyZmMzZDlhMmFiNjc5OWJlZTUxNjE2ZDRlYTNkYjU5\+          \Y2IwMDZhYWY1MjY1OTQgIC0K"+  rs256 = RSA256 E.testRsa2048KeyPair+  rs384 = RSA384 E.testRsa2048KeyPair+  rs512 = RSA512 E.testRsa2048KeyPair+  es256 = ECDSA256 E.testEcP256KeyPair+  es384 = ECDSA384 E.testEcP384KeyPair+  es512 = ECDSA512 E.testEcP521KeyPair -shrinkJwt :: Arbitrary (PrivateClaims pc ns) => Jwt pc ns -> [Jwt pc ns]-shrinkJwt jwt = shrinkMap (\payload' -> jwt { payload = payload' }) payload jwt+instance Arbitrary Typ where+  arbitrary = frequency+    [(16, pure JWT), (3, pure $ Typ Nothing), (1, pure $ Typ $ Just "some-typ")]  instance Arbitrary (PrivateClaims ts ns) => Arbitrary (Payload ts ns) where   arbitrary =
test/Interop/JWTDecoding.hs view
@@ -46,21 +46,32 @@  spec :: Spec spec = do-  describe "HS256"-    $ runTests-    $ HS256-        "MWNmYzExODA5OWFjOGM3NDNmMmM5Zjg5ZDc0YTM3M2VhMGNkMzA2MDY3ZjFhZDk5N2I3OTc5Yjdm\-        \NDg3NDBkMiAgLQo"-  describe "none" $ runTests None-  describe "RS256" $ runTests $ RS256 E.testRsa2048KeyPair+  testAlgValidation+  describe "HS256" $ runTests hmac256+  describe "none" $ runTests none+  describe "RS256" $ runTests rsa256 -runTests :: Alg -> Spec-runTests a = sequence_ $ tests <*> [a]+testAlgValidation :: Spec+testAlgValidation =+  let maliciousSigner = JWT.HMACSecret $ getDecodingKey E.testRsa2048KeyPair+      token = TE.encodeUtf8 $ JWT.encodeSigned maliciousSigner mempty mempty+  in  E.specify "alg-validation"+        $       fmap+                  (  const+                  $  expectationFailure+                  $  "Web.JWT: unexpectedly decoded token\n"+                  ++ show token+                  )+                  (decodeByteString @ 'NoNs @Empty (RSA256 E.testRsa2048KeyPair) token+                  )+        `catch` (\AlgorithmMismatch -> E.pass) -tests :: [Alg -> Spec]+runTests :: SomeAlgorithm -> Spec+runTests sa = sequence_ $ tests <*> [sa]++tests :: [SomeAlgorithm -> Spec] tests =   [ basicTest-  , typTest   , richHeaderTest   , audSingleTest   , audListTest@@ -74,73 +85,77 @@   , invalidTokenTest   ] -basicTest :: Alg -> Spec-basicTest a =-  let joseHeader =-          JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"-                  , JWT.sub = JWT.stringOrURI "test"-                  , JWT.iat = JWT.numericDate 1595386660-                  , JWT.nbf = JWT.numericDate 1595386660-                  }-  in  E.specify "basic" $ mkTest joseHeader cs a nullClaims+basicTest :: SomeAlgorithm -> Spec+basicTest sa =+  let joseHeader = JWT.JOSEHeader (Just "JWT") Nothing Nothing Nothing -- Web.JWT always replaces 'alg'+      cs         = mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+                          , JWT.sub = JWT.stringOrURI "test"+                          , JWT.iat = JWT.numericDate E.epochTime+                          , JWT.nbf = JWT.numericDate E.epochTime+                          }+  in  E.specify "basic"+        $   mkTest joseHeader cs sa+        =<< jwtPayload+              (  withIssuer "libjwt-typed-test"+              <> withSubject "test"+              <> issuedNow+              <> notBeforeNow+              )+              () -typTest :: Alg -> Spec-typTest a =+richHeaderTest :: SomeAlgorithm -> Spec+richHeaderTest sa =   let joseHeader =-          JWT.JOSEHeader (Just "jose") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"-                  , JWT.sub = JWT.stringOrURI "test-typ"-                  }-  in  E.xspecify "typ" $ mkTest joseHeader cs a nullClaims -- Web.JWT always uses JWT--richHeaderTest :: Alg -> Spec-richHeaderTest a =-  let joseHeader = JWT.JOSEHeader (Just "JWT")-                                  (Just "test")-                                  (mkJWTAlgorithm a)-                                  (Just "test")+          JWT.JOSEHeader (Just "JWT") (Just "test") Nothing (Just "test")       cs = mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"                   , JWT.sub = JWT.stringOrURI "test-rich-header"                   }-  in  E.specify "rich-header" $ mkTest joseHeader cs a nullClaims+  in  E.specify "rich-header"+        $   mkTest joseHeader cs sa+        =<< jwtPayload+              (withIssuer "libjwt-typed-test" <> withSubject "test-rich-header")+              () -audSingleTest :: Alg -> Spec-audSingleTest a =-  let joseHeader =-          JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"-                  , JWT.sub = JWT.stringOrURI "test-aud-single"+audSingleTest :: SomeAlgorithm -> Spec+audSingleTest sa =+  let cs = mempty { JWT.sub = JWT.stringOrURI "test-aud-single"                   , JWT.aud = Left <$> JWT.stringOrURI "nobody"                   }-  in  E.specify "aud-single" $ mkTest joseHeader cs a nullClaims+  in  E.specify "aud-single"+        $   mkTest mempty cs sa+        =<< jwtPayload+              (withSubject "test-aud-single" <> withRecipient "nobody")+              () -audListTest :: Alg -> Spec-audListTest a =-  let joseHeader =-          JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty-        { JWT.iss = JWT.stringOrURI "libjwt-typed-test"-        , JWT.sub = JWT.stringOrURI "test-aud-list"+audListTest :: SomeAlgorithm -> Spec+audListTest sa =+  let cs = mempty+        { JWT.sub = JWT.stringOrURI "test-aud-list"         , JWT.aud = fmap Right                     .   sequence                     $   [JWT.stringOrURI]                     <*> ["nobody-1", "nobody-2", "nobody-3"]         }-  in  E.specify "aud-list" $ mkTest joseHeader cs a nullClaims+  in  E.specify "aud-list"+        $   mkTest mempty cs sa+        =<< jwtPayload+              (  withSubject "test-aud-list"+              <> withRecipient "nobody-1"+              <> withRecipient "nobody-2"+              <> withRecipient "nobody-3"+              )+              ()  data PreferredContact = Email | Phone   deriving stock (Show, Eq, Generic)  instance AFlag PreferredContact -unregisteredClaimsTest :: Alg -> Spec-unregisteredClaimsTest a =+unregisteredClaimsTest :: SomeAlgorithm -> Spec+unregisteredClaimsTest sa =   let-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty-      { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"-      , JWT.sub                = JWT.stringOrURI "test-unregistered"+    cs = mempty+      { JWT.sub                = JWT.stringOrURI "test-unregistered"       , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList         [ ("name"            , JSON.String "John Doe")         , ("userId"          , JSON.Number 12345)@@ -150,23 +165,21 @@         , ("createdAt"       , JSON.String "2020-07-31T11:45:00Z")         ]       }-  in-    E.specify "unregistered-claims" $ mkTest joseHeader cs a $ toPrivateClaims-      ( #name ->> ("John Doe" :: String)-      , #userId ->> (12345 :: Int)-      , #isAdmin ->> False-      , #preferredContact ->> Flag Email-      , #systemId ->> E.testJTI-      , #createdAt ->> Just (read "2020-07-31 11:45:00 UTC" :: UTCTime)-      )+  in  E.specify "unregistered-claims" $ mkTest mempty cs sa =<< jwtPayload+        (withSubject "test-unregistered")+        ( #name ->> ("John Doe" :: String)+        , #userId ->> (12345 :: Int)+        , #isAdmin ->> False+        , #preferredContact ->> Flag Email+        , #systemId ->> E.testJTI+        , #createdAt ->> Just (read "2020-07-31 11:45:00 UTC" :: UTCTime)+        ) -unregisteredClaimsOptionsTest :: Alg -> Spec-unregisteredClaimsOptionsTest a =+unregisteredClaimsOptionsTest :: SomeAlgorithm -> Spec+unregisteredClaimsOptionsTest sa =   let-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty-      { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"-      , JWT.sub                = JWT.stringOrURI "test-unregistered-opt"+    cs = mempty+      { JWT.sub                = JWT.stringOrURI "test-unregistered-opt"       , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList                                    [ ("name", JSON.String "John Doe")                                    , ("userId"          , JSON.Number 12345)@@ -174,27 +187,25 @@                                    , ("preferredContact", JSON.String "email")                                    ]       }-  in-    E.specify "unregistered-claims-options"-    $ mkTest joseHeader cs a-    $ toPrivateClaims-        ( #name ->> ("John Doe" :: String)-        , #userId ->> (12345 :: Int)-        , #isAdmin ->> False-        , #preferredContact ->> Just (Flag Email)-        , #systemId ->> (Nothing :: Maybe UUID.UUID)-        , #prefs ->> ([] :: [String])-        )+  in  E.specify "unregistered-claims-options"+        $   mkTest mempty cs sa+        =<< jwtPayload+              (withSubject "test-unregistered-opt")+              ( #name ->> ("John Doe" :: String)+              , #userId ->> (12345 :: Int)+              , #isAdmin ->> False+              , #preferredContact ->> Just (Flag Email)+              , #systemId ->> (Nothing :: Maybe UUID.UUID)+              , #prefs ->> ([] :: [String])+              ) -unregisteredClaimsNsTest :: Alg -> Spec-unregisteredClaimsNsTest a =+unregisteredClaimsNsTest :: SomeAlgorithm -> Spec+unregisteredClaimsNsTest sa =   let-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty-      { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"-      , JWT.sub                = JWT.stringOrURI "test-unregistered"-      , JWT.iat                = JWT.numericDate 1595386660-      , JWT.nbf                = JWT.numericDate 1595386660+    cs = mempty+      { JWT.sub                = JWT.stringOrURI "test-unregistered"+      , JWT.iat                = JWT.numericDate E.epochTime+      , JWT.nbf                = JWT.numericDate E.epochTime       , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList         [ ("http://example.com/name"    , JSON.String "John Doe")         , ("http://example.com/userId"  , JSON.Number 12345)@@ -202,47 +213,42 @@         , ("http://example.com/systemId", JSON.String $ UUID.toText E.testJTI)         ]       }-  in-    E.specify "unregistered-claims-ns"-    $ mkTest joseHeader cs a-    $ toPrivateClaims-    $ withNs-        (Ns @"http://example.com")-        ( #name ->> ("John Doe" :: String)-        , #userId ->> (12345 :: Int)-        , #isAdmin ->> False-        , #systemId ->> E.testJTI+  in  E.specify "unregistered-claims-ns" $ mkTest mempty cs sa =<< jwtPayload+        (withSubject "test-unregistered" <> issuedNow <> notBeforeNow)+        (withNs+          (Ns @"http://example.com")+          ( #name ->> ("John Doe" :: String)+          , #userId ->> (12345 :: Int)+          , #isAdmin ->> False+          , #systemId ->> E.testJTI+          )         ) -utfTest :: Alg -> Spec-utfTest a =-  let joseHeader =-          JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty-        { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"-        , JWT.sub                = JWT.stringOrURI "test-utf"+utfTest :: SomeAlgorithm -> Spec+utfTest sa =+  let cs = mempty+        { JWT.sub                = JWT.stringOrURI "test-utf"         , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList                                      [ ("name"  , JSON.String "孔子")                                      , ("status", JSON.String "不患人之不己知,患不知人也")                                      ]         }-  in  E.specify "utf" $ mkTest joseHeader cs a $ toPrivateClaims+  in  E.specify "utf" $ mkTest mempty cs sa =<< jwtPayload+        (withSubject "test-utf")         (#name ->> ("孔子" :: String), #status ->> ("不患人之不己知,患不知人也" :: T.Text)) -invalidClaimTest :: Alg -> Spec-invalidClaimTest a =-  let joseHeader =-          JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-      cs = mempty+invalidClaimTest :: SomeAlgorithm -> Spec+invalidClaimTest sa =+  let cs = mempty         { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"         , JWT.sub                = JWT.stringOrURI "invalid-claim-test"         , JWT.unregisteredClaims = JWT.ClaimsMap                                      $ Map.singleton "not-a-number" JSON.Null         }-      token = jwtEncode cs joseHeader a+      token = jwtEncode cs mempty sa   in  E.specify "invalid-claim"         $       decodeTest @'["not-a-number" ->> Int] @ 'NoNs-                  a+                  sa                   token                   (\decoded ->                     expectationFailure@@ -253,31 +259,24 @@                   )         `catch` (\(Missing missing) -> pure $ missing `shouldBe` "not-a-number") -invalidOptClaimTest :: Alg -> Spec-invalidOptClaimTest a =-  let-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty-      { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"-      , JWT.sub                = JWT.stringOrURI "invalid-opt-claim-test"-      , JWT.unregisteredClaims = JWT.ClaimsMap-                                   $ Map.singleton "maybeNumber" JSON.Null-      }-  in-    E.specify "invalid-opt-claim"-    $   mkTest joseHeader cs a-    $   toPrivateClaims-    $   #maybeNumber-    ->> (Nothing :: Maybe Int)+invalidOptClaimTest :: SomeAlgorithm -> Spec+invalidOptClaimTest sa =+  let cs = mempty+        { JWT.sub                = JWT.stringOrURI "invalid-opt-claim-test"+        , JWT.unregisteredClaims = JWT.ClaimsMap+                                     $ Map.singleton "maybeNumber" JSON.Null+        }+  in  E.specify "invalid-opt-claim" $ mkTest mempty cs sa =<< jwtPayload+        (withSubject "invalid-opt-claim-test")+        (#maybeNumber ->> (Nothing :: Maybe Int)) -validTokenTest :: Alg -> Spec-validTokenTest a =+validTokenTest :: SomeAlgorithm -> Spec+validTokenTest sa =   let-    t0         = 1597945008-    ttl        = 60-    t1         = t0 + 30-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty+    t0  = 1597945008+    ttl = 60+    t1  = t0 + 30+    cs  = mempty       { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"       , JWT.sub                = JWT.stringOrURI "valid-token-test"       , JWT.iat                = JWT.numericDate t0@@ -289,8 +288,8 @@   in     E.specifyWithTime t1 "valid-token"     $ validationTest @'["number" ->> Int] @ 'NoNs-        a-        (jwtEncode cs joseHeader a)+        sa+        (jwtEncode cs mempty sa)         (  checkAge 3600         <> checkIssuer "libjwt-typed-test"         <> checkSubject "valid-token-test"@@ -300,14 +299,13 @@         (V.Success _) -> True         (V.Failure _) -> False -invalidTokenTest :: Alg -> Spec-invalidTokenTest a =+invalidTokenTest :: SomeAlgorithm -> Spec+invalidTokenTest sa =   let-    t0         = 1597945008-    ttl        = 60-    t1         = t0 + 120-    joseHeader = JWT.JOSEHeader (Just "JWT") Nothing (mkJWTAlgorithm a) Nothing-    cs         = mempty+    t0  = 1597945008+    ttl = 60+    t1  = t0 + 120+    cs  = mempty       { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"       , JWT.sub                = JWT.stringOrURI "invalid-token-test"       , JWT.iat                = JWT.numericDate t0@@ -319,8 +317,8 @@   in     E.specifyWithTime t1 "invalid-token"     $ validationTest @'["number" ->> Int] @ 'NoNs-        a-        (jwtEncode cs joseHeader a)+        sa+        (jwtEncode cs mempty sa)         (  checkAge 3600         <> checkIssuer "libjwt-typed-test"         <> checkSubject "valid-token-test"@@ -340,40 +338,42 @@      )   => JWT.JOSEHeader   -> JWT.JWTClaimsSet-  -> Alg-  -> PrivateClaims cs ns+  -> SomeAlgorithm+  -> Payload cs ns   -> E.TestEnv Expectation-mkTest inHeader inClaims a expected =-  let token = jwtEncode inClaims inHeader a-  in  handle (handleDecodeException token) $ decodeTest a token $ \jwt -> do-        expectHeader inHeader $ header jwt-        expectPayload inClaims (payload jwt) expected+mkTest inHeader inClaims sa expected =+  let token = jwtEncode inClaims inHeader sa+  in  handle (handleDecodeException token) $ decodeTest sa token $ \actual ->+        actual `shouldBe` expected  decodeTest   :: Decode (PrivateClaims pc ns)-  => Alg+  => SomeAlgorithm   -> T.Text-  -> (Jwt pc ns -> Expectation)+  -> (Payload pc ns -> Expectation)   -> E.TestEnv Expectation-decodeTest a token test =-  fmap (test . getDecoded) $ E.MkTest $ decodeByteString a $ TE.encodeUtf8 token+decodeTest (SomeAlgorithm a) token test =+  fmap (test . payload . getDecoded)+    $ E.MkTest+    $ decodeByteString a+    $ TE.encodeUtf8 token  validationTest   :: Decode (PrivateClaims pc ns)-  => Alg+  => SomeAlgorithm   -> T.Text   -> JwtValidation pc ns   -> (  ValidationNEL ValidationFailure (Validated (Jwt pc ns))      -> Expectation      )   -> E.TestEnv Expectation-validationTest a token validation test =+validationTest (SomeAlgorithm a) token validation test =   fmap test     $ E.MkTest     $ jwtFromByteString defaultValidationSettings validation a     $ TE.encodeUtf8 token -jwtEncode :: JWT.JWTClaimsSet -> JWT.JOSEHeader -> Alg -> T.Text+jwtEncode :: JWT.JWTClaimsSet -> JWT.JOSEHeader -> SomeAlgorithm -> T.Text jwtEncode inClaims inHeader =   maybe (JWT.encodeUnsigned inClaims inHeader)         (\signer -> JWT.encodeSigned signer inHeader inClaims)@@ -381,56 +381,8 @@  handleDecodeException :: T.Text -> SomeDecodeException -> E.TestEnv Expectation handleDecodeException token e =-  pure-    $  expectationFailure+  E.fail     $  "Web.JWT: Decoding token\n"     ++ show token     ++ "\n\tthrew exception:\n"     ++ show e--expectHeader :: JWT.JOSEHeader -> Header -> Expectation-expectHeader expected got = do-  typ got `shouldBe` typ' (JWT.typ expected)-  mkJWTAlgorithm (alg got) `shouldBe` JWT.alg expected- where-  typ' Nothing      = Typ Nothing-  typ' (Just "JWT") = JWT-  typ' (Just "jwt") = JWT-  typ' (Just text ) = Typ $ Just $ TE.encodeUtf8 text--expectPayload-  :: (Show (PrivateClaims cs ns), Eq (PrivateClaims cs ns))-  => JWT.JWTClaimsSet-  -> Payload cs ns-  -> PrivateClaims cs ns-  -> Expectation-expectPayload expected got expectedPc = got `shouldBe` ClaimsSet-  { iss           = iss' (JWT.iss expected)-  , sub           = sub' (JWT.sub expected)-  , aud           = aud' (JWT.aud expected)-  , exp           = exp' (JWT.exp expected)-  , iat           = iat' (JWT.iat expected)-  , nbf           = nbf' (JWT.nbf expected)-  , jti           = jti' (JWT.jti expected)-  , privateClaims = expectedPc-  }- where-  stringOrURIToString  = T.unpack . JWT.stringOrURIToText--  intDateToNumericDate = fromPOSIX . JWT.secondsSinceEpoch--  iss' mt = Iss $ stringOrURIToString <$> mt--  sub' mt = Sub $ stringOrURIToString <$> mt--  aud' Nothing           = mempty-  aud' (Just (Left  s )) = Aud [stringOrURIToString s]-  aud' (Just (Right ss)) = Aud $ map stringOrURIToString ss--  exp' md = Exp $ intDateToNumericDate <$> md--  iat' md = Iat $ intDateToNumericDate <$> md--  nbf' md = Nbf $ intDateToNumericDate <$> md--  jti' mt = Jti $ UUID.fromText . JWT.stringOrURIToText =<< mt
test/Interop/JWTEncoding.hs view
@@ -1,7 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-}-{-# LANGUAGE ExplicitForAll #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLabels #-}@@ -14,16 +13,14 @@ where  import           Web.Libjwt+import           Env                            ( expectationOk ) import qualified Env                           as E import           Interop.JWTHelpers-import           Libjwt.NumericDate             ( toPOSIX )  import qualified Data.Aeson                    as JSON  import qualified Data.Map.Strict               as Map -import           Data.Maybe                     ( fromMaybe )- import qualified Data.Text                     as T import qualified Data.Text.Encoding            as TE @@ -40,22 +37,17 @@  spec :: Spec spec = do-  describe "HS256"-    $ runTests-    $ HS256-        "MWNmYzExODA5OWFjOGM3NDNmMmM5Zjg5ZDc0YTM3M2VhMGNkMzA2MDY3ZjFhZDk5N2I3OTc5Yjdm\-        \NDg3NDBkMiAgLQo"-  xdescribe "none" $ runTests None -- JWT does not work at all with none-  describe "RS256" $ runTests $ RS256 E.testRsa2048KeyPair+  describe "HS256" $ runTests hmac256+  xdescribe "none" $ runTests none -- JWT does not work at all with none+  describe "RS256" $ runTests rsa256 -runTests :: Alg -> Spec-runTests alg = sequence_ $ tests <*> [alg]+runTests :: SomeAlgorithm -> Spec+runTests sa = sequence_ $ tests <*> [sa] -tests :: [Alg -> Spec]+tests :: [SomeAlgorithm -> Spec] tests =   [ basicTest   , typTest-  , audSingleTest   , audListTest   , privateClaimsSimpleTest   , privateClaimsComplexTest@@ -63,10 +55,10 @@   , utfTest   ] -basicTest :: Alg -> Spec-basicTest alg =+basicTest :: SomeAlgorithm -> Spec+basicTest sa =   E.specify "basic"-    $   mkTest Header { alg, typ = JWT } mempty+    $   mkTest sa JWT     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test"@@ -75,33 +67,38 @@           <> withJwtId E.testJTI           )           ()+    <*> E.withEpochTime+          (\t -> mempty+            { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub = JWT.stringOrURI "test"+            , JWT.iat = JWT.numericDate t+            , JWT.nbf = JWT.numericDate t+            , JWT.jti = JWT.stringOrURI $ T.pack $ UUID.toString E.testJTI+            }+          ) -typTest :: Alg -> Spec-typTest alg =+typTest :: SomeAlgorithm -> Spec+typTest sa =   E.specify "typ"-    $   mkTest Header { alg, typ = Typ $ Just "jose" } mempty+    $   mkTest sa (Typ $ Just "jose")     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test-typ"           <> withJwtId E.testJTI           )           ()--audSingleTest :: Alg -> Spec-audSingleTest alg =-  E.specify "aud-single"-    $   mkTest Header { alg, typ = JWT } mempty-    <$> jwtPayload-          (  withIssuer "libjwt-typed-test"-          <> withSubject "test-aud-single"-          <> withRecipient "nobody"+    <*> pure+          (mempty { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+                  , JWT.sub = JWT.stringOrURI "test-typ"+                  , JWT.jti = JWT.stringOrURI $ T.pack $ UUID.toString E.testJTI+                  }           )-          () -audListTest :: Alg -> Spec-audListTest alg =++audListTest :: SomeAlgorithm -> Spec+audListTest sa =   E.specify "aud-list"-    $   mkTest Header { alg, typ = JWT } mempty+    $   mkTest sa JWT     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test-aud-list"@@ -110,18 +107,21 @@           <> withRecipient "nobody-3"           )           ()+    <*> pure+          (mempty+            { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub = JWT.stringOrURI "test-aud-list"+            , JWT.aud = fmap Right+                        .   sequence+                        $   [JWT.stringOrURI]+                        <*> ["nobody-1", "nobody-2", "nobody-3"]+            }+          ) -privateClaimsSimpleTest :: Alg -> Spec-privateClaimsSimpleTest alg =+privateClaimsSimpleTest :: SomeAlgorithm -> Spec+privateClaimsSimpleTest sa =   E.specify "private-claims-simple"-    $   mkTest-          Header { alg, typ = JWT }-          (JWT.ClaimsMap $ Map.fromList-            [ ("name"   , JSON.String "John Doe")-            , ("userId" , JSON.Number 12345)-            , ("isAdmin", JSON.Bool False)-            ]-          )+    $   mkTest sa JWT     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test-private-claims-simple"@@ -130,6 +130,17 @@           , #userId ->> (12345 :: Int)           , #isAdmin ->> False           )+    <*> pure+          (mempty+            { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub = JWT.stringOrURI "test-private-claims-simple"+            , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList+                                         [ ("name"   , JSON.String "John Doe")+                                         , ("userId" , JSON.Number 12345)+                                         , ("isAdmin", JSON.Bool False)+                                         ]+            }+          )  data UserRole = Admin | RegularUser   deriving stock (Show, Eq, Generic)@@ -142,17 +153,10 @@ instance ToPrivateClaims ClaimObj instance FromPrivateClaims ClaimObj -privateClaimsComplexTest :: Alg -> Spec-privateClaimsComplexTest alg =+privateClaimsComplexTest :: SomeAlgorithm -> Spec+privateClaimsComplexTest sa =   E.specify "private-claims-complex"-    $   mkTest-          Header { alg, typ = JWT }-          (JWT.ClaimsMap $ Map.fromList-            [ ("name"  , JSON.String "John Doe")-            , ("userId", JSON.Number 12345)-            , ("role"  , JSON.String "regularUser")-            ]-          )+    $   mkTest sa JWT     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test-private-claims-complex"@@ -161,18 +165,22 @@                    , userId = 12345                    , role   = Flag RegularUser                    }+    <*> pure+          (mempty+            { JWT.iss = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub = JWT.stringOrURI "test-private-claims-complex"+            , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList+                                         [ ("name"  , JSON.String "John Doe")+                                         , ("userId", JSON.Number 12345)+                                         , ("role"  , JSON.String "regularUser")+                                         ]+            }+          ) -privateClaimsNsTest :: Alg -> Spec-privateClaimsNsTest alg =+privateClaimsNsTest :: SomeAlgorithm -> Spec+privateClaimsNsTest sa =   E.specify "private-claims-ns"-    $   mkTest-          Header { alg, typ = JWT }-          (JWT.ClaimsMap $ Map.fromList-            [ ("http://example.com/name"   , JSON.String "John Doe")-            , ("http://example.com/userId" , JSON.Number 12345)-            , ("http://example.com/isAdmin", JSON.Bool False)-            ]-          )+    $   mkTest sa JWT     <$> jwtPayload           (  withIssuer "libjwt-typed-test"           <> withSubject "test-private-claims-ns"@@ -186,43 +194,66 @@             , #isAdmin ->> False             )           )+    <*> E.withEpochTime+          (\t -> mempty+            { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub                = JWT.stringOrURI "test-private-claims-ns"+            , JWT.iat                = JWT.numericDate t+            , JWT.nbf                = JWT.numericDate t+            , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList+              [ ("http://example.com/name"   , JSON.String "John Doe")+              , ("http://example.com/userId" , JSON.Number 12345)+              , ("http://example.com/isAdmin", JSON.Bool False)+              ]+            }+          ) -utfTest :: Alg -> Spec-utfTest alg =+utfTest :: SomeAlgorithm -> Spec+utfTest sa =   E.specify "utf"-    $   mkTest-          Header { alg, typ = JWT }-          (JWT.ClaimsMap $ Map.fromList-            [("name", JSON.String "孔子"), ("status", JSON.String "不患人之不己知,患不知人也")]-          )+    $   mkTest sa JWT     <$> jwtPayload           (withIssuer "libjwt-typed-test" <> withSubject "test-utf")           (#name ->> ("孔子" :: String), #status ->> ("不患人之不己知,患不知人也" :: T.Text))+    <*> pure+          (mempty+            { JWT.iss                = JWT.stringOrURI "libjwt-typed-test"+            , JWT.sub                = JWT.stringOrURI "test-utf"+            , JWT.unregisteredClaims = JWT.ClaimsMap $ Map.fromList+                                         [ ("name", JSON.String "孔子")+                                         , ( "status"+                                           , JSON.String "不患人之不己知,患不知人也"+                                           )+                                         ]+            }+          )  mkTest   :: Encode (PrivateClaims cs ns)-  => Header-  -> JWT.ClaimsMap+  => SomeAlgorithm+  -> Typ   -> Payload cs ns+  -> JWT.JWTClaimsSet   -> Expectation-mkTest outHeader expected payload =-  let outJwt = Jwt { header = outHeader, payload }-      token  = TE.decodeASCII $ getToken $ signJwt outJwt-  in  expectDecodable token $ \unverifiedJwt -> do-        expectHeader outHeader $ JWT.header unverifiedJwt-        expectClaimsSet payload expected $ JWT.claims unverifiedJwt-        case mkSigner $ alg outHeader of-          Nothing -> pure ()-          Just signer ->-            maybe-                (  expectationFailure-                $  "Web.JWT: Unverifiable token\n"-                ++ show token-                ++ "\nheader:\n"-                ++ show outHeader-                )-                (const $ pure ())-              $ JWT.verify signer unverifiedJwt+mkTest sa@(SomeAlgorithm a) typ payload expected =+  expectDecodable token $ \unverifiedJwt -> do+    expectHeader outHeader $ JWT.header unverifiedJwt+    JWT.claims unverifiedJwt `shouldBe` expected+    case mkSigner sa of+      Nothing -> expectationOk+      Just signer ->+        maybe+            (  expectationFailure+            $  "Web.JWT: Unverifiable token\n"+            ++ show token+            ++ "\nheader:\n"+            ++ show outHeader+            )+            (const expectationOk)+          $ JWT.verify signer unverifiedJwt+ where+  outHeader = Header { alg = toHeaderAlg a, typ }+  token     = TE.decodeASCII $ getToken $ sign' typ a payload  expectDecodable   :: T.Text -> (JWT.JWT JWT.UnverifiedJWT -> Expectation) -> Expectation@@ -242,46 +273,6 @@   typ' JWT       = Just "JWT"   typ' (Typ mbs) = TE.decodeUtf8 <$> mbs -  alg' (HS256 _) = Just JWT.HS256-  alg' (RS256 _) = Just JWT.RS256-  alg' _         = Nothing--expectClaimsSet-  :: Payload cs ns -> JWT.ClaimsMap -> JWT.JWTClaimsSet -> Expectation-expectClaimsSet expected expectedUnreg got =-  got-    `shouldBe` JWT.JWTClaimsSet (iss' $ iss expected)-                                (sub' $ sub expected)-                                (aud' $ aud expected)-                                (exp' $ exp expected)-                                (nbf' $ nbf expected)-                                (iat' $ iat expected)-                                (jti' $ jti expected)-                                expectedUnreg- where-  stringToStringOrURI s =-    fromMaybe (error $ "JWT.stringOrURI on " ++ show s)-      $ JWT.stringOrURI-      $ T.pack s--  numericDateToIntDate d =-    fromMaybe (error $ "JWT.numericDate on " ++ show d)-      $ JWT.numericDate-      $ toPOSIX d--  iss' (Iss ms) = stringToStringOrURI <$> ms--  sub' (Sub ms) = stringToStringOrURI <$> ms--  aud' (Aud []) = Nothing-  aud' (Aud xs) = Just $ Right $ map stringToStringOrURI xs--  exp' (Exp mnd) = numericDateToIntDate <$> mnd--  nbf' (Nbf mnd) = numericDateToIntDate <$> mnd--  iat' (Iat mnd) = numericDateToIntDate <$> mnd--  jti' (Jti muid) = stringToStringOrURI . UUID.toString <$> muid--+  alg' HS256 = Just JWT.HS256+  alg' RS256 = Just JWT.RS256+  alg' _     = Nothing
test/Interop/JWTHelpers.hs view
@@ -1,33 +1,54 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE OverloadedStrings #-}+ module Interop.JWTHelpers   ( mkSigner-  , mkJWTAlgorithm+  , SomeAlgorithm(..)+  , hmac256+  , none+  , rsa256+  , toHeaderAlg   ) where -import           Web.Libjwt                     ( Alg(..)-                                                , RsaKeyPair(..)+import           Web.Libjwt                     ( Algorithm(..)+                                                , SigningKey(..)                                                 , Secret(..)                                                 )+import           Libjwt.Algorithms              ( toHeaderAlg ) +import qualified Env                           as E+ import           Data.Maybe                     ( fromMaybe )  import qualified Web.JWT                       as JWT -mkSigner :: Alg -> Maybe JWT.Signer-mkSigner (HS256 secret) = Just $ JWT.HMACSecret $ reveal secret-mkSigner (RS256 pem   ) = Just $ mkRSAPrivateKey pem-mkSigner None           = Nothing-mkSigner _              = error "Unsupported alg"+data SomeAlgorithm = forall k . SigningKey k => SomeAlgorithm (Algorithm k) -mkRSAPrivateKey :: RsaKeyPair -> JWT.Signer-mkRSAPrivateKey pem =-  JWT.RSAPrivateKey-    $ fromMaybe (error $ "JWT.readRsaSecret on\n" ++ show pem)-    $ JWT.readRsaSecret-    $ privKey pem+hmac256 :: SomeAlgorithm+hmac256 =+  SomeAlgorithm+    $ HMAC256+        "MWNmYzExODA5OWFjOGM3NDNmMmM5Zjg5ZDc0YTM3M2VhMGNkMzA2MDY3ZjFhZDk5N2I3OTc5Yjdm\+        \NDg3NDBkMiAgLQo" -mkJWTAlgorithm :: Alg -> Maybe JWT.Algorithm-mkJWTAlgorithm None      = Nothing-mkJWTAlgorithm (HS256 _) = Just JWT.HS256-mkJWTAlgorithm (RS256 _) = Just JWT.RS256-mkJWTAlgorithm _         = error "Unsupported algorithm"+none :: SomeAlgorithm+none = SomeAlgorithm AlgNone++rsa256 :: SomeAlgorithm+rsa256 = SomeAlgorithm $ RSA256 E.testRsa2048KeyPair++mkSigner :: SomeAlgorithm -> Maybe JWT.Signer+mkSigner (SomeAlgorithm (HMAC256 secret)) =+  Just $ JWT.HMACSecret $ reveal secret+mkSigner (SomeAlgorithm (RSA256 pem)) = Just $ mkRSAPrivateKey pem+mkSigner (SomeAlgorithm AlgNone     ) = Nothing+mkSigner _                            = error "Unsupported alg"++mkRSAPrivateKey :: SigningKey k => k -> JWT.Signer+mkRSAPrivateKey k =+  let rsaSecret = getSigningKey k+  in  JWT.RSAPrivateKey+        $ fromMaybe (error $ "JWT.readRsaSecret on\n" ++ show rsaSecret)+        $ JWT.readRsaSecret rsaSecret
test/Properties.hs view
@@ -74,39 +74,40 @@  prop_encode_decode_roundtrip_poly   :: (Decode (PrivateClaims pc ns), Encode (PrivateClaims pc ns), Show a, Eq a)-  => (Jwt pc ns -> a)-  -> (a -> Jwt pc ns)+  => (Payload pc ns -> a)+  -> (a -> Payload pc ns)   -> a   -> Property prop_encode_decode_roundtrip_poly project embed a =-  let jwt = embed a-  in  left-          displayException-          (   decodeByteString (alg $ header jwt) (getToken $ signJwt jwt)-          <&> project-          .   getDecoded-          )-        === pure a+  forAll genAlgorithm $ \(SomeAlgorithm algorithm) ->+    left+        displayException+        (   decodeByteString algorithm (getToken $ sign algorithm $ embed a)+        <&> project+        .   payload+        .   getDecoded+        )+      === pure a -prop_encode_decode_roundtrip_reg_only :: Jwt Empty 'NoNs -> Property+prop_encode_decode_roundtrip_reg_only :: Payload Empty 'NoNs -> Property prop_encode_decode_roundtrip_reg_only = prop_encode_decode_roundtrip_poly id id  prop_encode_decode_roundtrip-  :: Jwt+  :: Payload        '["intField" ->> Int, "dateField" ->> UTCTime, "textField" ->> JwtText, "optField" ->> Maybe JwtString, "arrayField" ->> [JwtText], "nonEmptyField" ->> NonEmpty ASCII]        'NoNs   -> Property prop_encode_decode_roundtrip = prop_encode_decode_roundtrip_poly id id  prop_encode_decode_roundtrip_ns-  :: Jwt+  :: Payload        '["dayField" ->> Day, "arrayField" ->> [Int], "stringField" ->> JwtString]        ( 'SomeNs "https://example.com")   -> Property prop_encode_decode_roundtrip_ns = prop_encode_decode_roundtrip_poly id id  prop_encode_decode_rountrip_comp-  :: Jwt+  :: Payload        '["user_name" ->> JwtText, "is_root" ->> Bool, "type" ->> Flag AccountType, "client_id" ->> UUID, "created" ->> UTCTime, "accounts" ->> NonEmpty (UUID, JwtText)]        'NoNs   -> Property@@ -138,15 +139,9 @@       <*> shrink createdAt  prop_from_to_generic :: ClaimObj -> Property-prop_from_to_generic claimObj = forAllBlind genHeader $ \header ->-  prop_encode_decode_roundtrip_poly-    (fromPrivateClaims . privateClaims . payload)-    (\claimObj' -> Jwt-      { header-      , payload = def { privateClaims = toPrivateClaims claimObj' }-      }-    )-    claimObj+prop_from_to_generic = prop_encode_decode_roundtrip_poly+  (fromPrivateClaims . privateClaims)+  (\claimObj' -> def { privateClaims = toPrivateClaims claimObj' })  data Account = MkAccount { account_name :: JwtText, account_id :: UUID }   deriving stock (Show, Eq, Generic)@@ -166,7 +161,7 @@   shrink (MkAccount name id) = MkAccount <$> shrink name <*> shrink id  prop_encode_decode_roundtrip_aeson-  :: Jwt '["user_id" ->> Int, "accounts" ->> NonEmpty Account] 'NoNs+  :: Payload '["user_id" ->> Int, "accounts" ->> NonEmpty Account] 'NoNs   -> Property prop_encode_decode_roundtrip_aeson = prop_encode_decode_roundtrip_poly id id