jose-jwt 0.4.2 → 0.5
raw patch · 6 files changed
+62/−41 lines, 6 filesdep ~errorsPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: errors
API changes (from Hackage documentation)
+ Jose.Jwt: JweEncoding :: JweAlg -> Enc -> JwtEncoding
+ Jose.Jwt: JwsEncoding :: JwsAlg -> JwtEncoding
+ Jose.Jwt: data JwtEncoding
- Jose.Jwt: decode :: CPRG g => g -> [Jwk] -> ByteString -> (Either JwtError JwtContent, g)
+ Jose.Jwt: decode :: CPRG g => g -> [Jwk] -> Maybe JwtEncoding -> ByteString -> (Either JwtError JwtContent, g)
- Jose.Jwt: encode :: CPRG g => g -> [Jwk] -> Alg -> Maybe Enc -> Payload -> (Either JwtError Jwt, g)
+ Jose.Jwt: encode :: CPRG g => g -> [Jwk] -> JwtEncoding -> Payload -> (Either JwtError Jwt, g)
Files
- CHANGELOG.md +5/−0
- Jose/Jwt.hs +34/−29
- Jose/Types.hs +9/−0
- jose-jwt.cabal +1/−1
- tests/Tests/JweSpec.hs +4/−5
- tests/Tests/JwsSpec.hs +9/−6
CHANGELOG.md view
@@ -1,3 +1,8 @@+0.5+---++* Add JwtEncoding type. Changes API of `Jwt.encode` and `Jwt.decode`.+ 0.4.2 -----
Jose/Jwt.hs view
@@ -14,8 +14,8 @@ -- >>> g <- makeSystem -- >>> let jsonJwk = "{\"kty\":\"RSA\", \"kid\":\"mykey\", \"n\":\"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ\", \"e\":\"AQAB\", \"d\":\"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ\"}" :: ByteString -- >>> let Just jwk = decodeStrict jsonJwk :: Maybe Jwk--- >>> let (Right (Jwt jwtEncoded), g') = encode g [jwk] (Signed RS256) Nothing (Claims "public claims")--- >>> let (Right jwtDecoded, g'') = Jose.Jwt.decode g' [jwk] jwtEncoded+-- >>> let (Right (Jwt jwtEncoded), g') = encode g [jwk] (JwsEncoding RS256) (Claims "public claims")+-- >>> let (Right jwtDecoded, g'') = Jose.Jwt.decode g' [jwk] (Just (JwsEncoding RS256)) jwtEncoded -- >>> jwtDecoded -- Jws (JwsHeader {jwsAlg = RS256, jwsTyp = Nothing, jwsCty = Nothing, jwsKid = Just "mykey"},"public claims") @@ -49,45 +49,45 @@ -- | Use the supplied JWKs to create a JWT. -- The list of keys will be searched to locate one which is--- consistent with the chosen algorithm.+-- consistent with the chosen encoding algorithms. -- encode :: (CPRG g)- => g -- ^ Random number generator.+ => g -- ^ Random number generator -> [Jwk] -- ^ The key or keys. At least one must be consistent with the chosen algorithm- -> Alg -- ^ The JWS or JWE algorithm- -> Maybe Enc -- ^ The payload encryption algorithm (if applicable)+ -> JwtEncoding -- ^ The encoding algorithm(s) used to encode the payload -> Payload -- ^ The payload (claims) -> (Either JwtError Jwt, g) -- ^ The encoded JWT, if successful-encode rng jwks alg enc msg = flip runState rng $ runEitherT $ case alg of- Signed None -> do- unless (isNothing enc) $ left (BadAlgorithm "Enc cannot be set for an unsecured JWT")- case msg of- Claims p -> return $ Jwt $ BC.intercalate "." [unsecuredHdr, B64.encode p]- Nested _ -> left BadClaims- Signed a -> do- unless (isNothing enc) $ left (BadAlgorithm "Enc cannot be set for a JWS")- case filter (canEncodeJws a) jwks of- [] -> left (KeyError "No matching key found for JWS algorithm")- (k:_) -> hoistEither =<< state (\g -> Jws.jwkEncode g a k msg)- Encrypted a -> do- e <- hoistEither $ note (BadAlgorithm "Enc must be supplied for a JWE") enc- case filter (canEncodeJwe a) jwks of- [] -> left (KeyError "No matching key found for JWE algorithm")- (k:_) -> hoistEither =<< state (\g -> Jwe.jwkEncode g a e k msg)+encode rng jwks encoding msg = flip runState rng $ runEitherT $ case encoding of+ JwsEncoding None -> case msg of+ Claims p -> return $ Jwt $ BC.intercalate "." [unsecuredHdr, B64.encode p]+ Nested _ -> left BadClaims+ JwsEncoding a -> case filter (canEncodeJws a) jwks of+ [] -> left (KeyError "No matching key found for JWS algorithm")+ (k:_) -> hoistEither =<< state (\g -> Jws.jwkEncode g a k msg)+ JweEncoding a e -> case filter (canEncodeJwe a) jwks of+ [] -> left (KeyError "No matching key found for JWE algorithm")+ (k:_) -> hoistEither =<< state (\g -> Jwe.jwkEncode g a e k msg) where unsecuredHdr = B64.encode "{\"alg\":\"none\"}" -- | Uses the supplied keys to decode a JWT. -- Locates a matching key by header @kid@ value where possible--- or by suitable key type.--- The JWK @use@ and @alg@ options are currently ignored.+-- or by suitable key type for the encoding algorithm.+--+-- The algorithm(s) used can be optionally be supplied for validation+-- by setting the @JwtEncoding@ parameter, in which case an error will+-- be returned if they don't match.+--+-- For unsecured tokens (with algorithm "none"), the expected algorithm+-- must be set to @Just (JwsEncoding None)@ or an error will be returned. decode :: CPRG g => g -- ^ Random number generator. Only used for RSA blinding -> [Jwk] -- ^ The keys to use for decoding+ -> Maybe JwtEncoding -- ^ The expected encoding information -> ByteString -- ^ The encoded JWT -> (Either JwtError JwtContent, g) -- ^ The decoded JWT payload, if successful-decode rng keySet jwt = flip runState rng $ runEitherT $ do+decode rng keySet encoding jwt = flip runState rng $ runEitherT $ do let components = BC.split '.' jwt when (length components < 3) $ left $ BadDots 2 hdr <- B64.decode (head components) >>= hoistEither . parseHeader@@ -95,9 +95,15 @@ -- Now we have one or more suitable keys (or none for the unsecured case). -- Try each in turn until successful decodings <- case hdr of- UnsecuredH -> B64.decode (components !! 1) >>= \p -> return [Just (Unsecured p)]- JwsH _ -> mapM decodeWithJws ks- JweH _ -> mapM decodeWithJwe ks+ UnsecuredH -> do+ unless (encoding == Just (JwsEncoding None)) $ left (BadAlgorithm "JWT is unsecured but expected 'alg' was not 'none'")+ B64.decode (components !! 1) >>= \p -> return [Just (Unsecured p)]+ JwsH h -> do+ unless (isNothing encoding || encoding == Just (JwsEncoding (jwsAlg h))) $ left (BadAlgorithm "Expected 'alg' doesn't match JWS header")+ mapM decodeWithJws ks+ JweH h -> do+ unless (isNothing encoding || encoding == Just (JweEncoding (jweAlg h) (jweEnc h))) $ left (BadAlgorithm "Expected encoding doesn't match JWE header")+ mapM decodeWithJwe ks maybe (left $ KeyError "None of the keys was able to decode the JWT") (return . fromJust) $ find isJust decodings where decodeWithJws :: CPRG g => Jwk -> EitherT JwtError (State g) (Maybe JwtContent)@@ -142,4 +148,3 @@ -- TODO Move checks to JWK and support better error messages checkKeys [] = left $ KeyError "No suitable key was found to decode the JWT" checkKeys ks = return ks-
Jose/Types.hs view
@@ -10,6 +10,7 @@ , JwsHeader (..) , JweHeader (..) , JwtContent (..)+ , JwtEncoding (..) , JwtError (..) , IntDate (..) , Payload (..)@@ -54,6 +55,14 @@ -- | A decoded JWT which can be either a JWE or a JWS, or an unsecured JWT. data JwtContent = Unsecured !ByteString | Jws !Jws | Jwe !Jwe deriving (Show, Eq)++-- | Defines the encoding information for a JWT.+--+-- Used for both encoding new JWTs and validating existing ones.+data JwtEncoding+ = JwsEncoding JwsAlg+ | JweEncoding JweAlg Enc+ deriving (Eq, Show) data JwtHeader = JweH JweHeader | JwsH JwsHeader
jose-jwt.cabal view
@@ -1,5 +1,5 @@ Name: jose-jwt-Version: 0.4.2+Version: 0.5 Synopsis: JSON Object Signing and Encryption Library Homepage: http://github.com/tekul/jose-jwt Bug-Reports: http://github.com/tekul/jose-jwt/issues
tests/Tests/JweSpec.hs view
@@ -41,7 +41,7 @@ generateCmkAndIV g A256GCM @?= (a1cek, a1iv, RNG "") it "generates the expected RSA-encrypted content key" $ do- let g = RNG $ a1oaepSeed+ let g = RNG a1oaepSeed rsaEncrypt g RSA_OAEP a1PubKey a1cek @?= (a1jweKey, RNG "") it "encrypts the payload to the expected ciphertext and authentication tag" $ do@@ -52,8 +52,8 @@ let g = RNG $ B.concat [a1cek, a1iv, a1oaepSeed] Jwe.rsaEncode g RSA_OAEP A256GCM a1PubKey a1Payload @?= (Jwt a1, RNG "") - it "decodes the JWT to the expected header and payload" $ do- (fst $ Jwe.rsaDecode blinderRNG a1PrivKey a1) @?= Right (a1Header, a1Payload)+ it "decodes the JWT to the expected header and payload" $+ fst (Jwe.rsaDecode blinderRNG a1PrivKey a1) @?= Right (a1Header, a1Payload) it "decodes the JWK to the correct RSA key values" $ do let Right (Jwk.RsaPrivateJwk (RSA.PrivateKey pubKey d 0 0 0 0 0) _ _ _) = eitherDecode a1jwk@@ -64,7 +64,7 @@ it "decodes the JWT using the JWK" $ do let Right k1 = eitherDecode a1jwk Just k2 = decodeStrict' a2jwk- fst (decode blinderRNG [k2, k1] a1) @?= (Right $ Jwe (a1Header, a1Payload))+ fst (decode blinderRNG [k2, k1] (Just $ JweEncoding RSA_OAEP A256GCM) a1) @?= (Right $ Jwe (a1Header, a1Payload)) context "when using JWE Appendix 2 data" $ do let a2Header = defJweHdr {jweAlg = RSA1_5, jweEnc = A128CBC_HS256}@@ -247,4 +247,3 @@ -- Decrypt, drop the 02 at the start and take the bytes up to the next 0 extractPKCS15Seed :: RSA.PrivateKey -> B.ByteString -> B.ByteString extractPKCS15Seed key ct = B.takeWhile (/= 0) . B.drop 2 $ dp Nothing key ct-
tests/Tests/JwsSpec.hs view
@@ -46,7 +46,7 @@ it "decodes the payload using the JWK" $ do let Just k11 = decodeStrict' a11jwk- fst (decode RNG [k11] a11) @?= fmap Jws a11decoded+ fst (decode RNG [k11] Nothing a11) @?= fmap Jws a11decoded it "encodes/decodes using HS512" $ hmacRoundTrip HS512 a11Payload@@ -60,7 +60,7 @@ it "decodes the JWT to the expected header and payload with the JWK" $ do let Just k21 = decodeStrict' a21jwk- fst (decode RNG [k21] a21) @?= (Right $ Jws (defJwsHdr {jwsAlg = RS256}, a21Payload))+ fst (decode RNG [k21] (Just (JwsEncoding RS256)) a21) @?= (Right $ Jws (defJwsHdr {jwsAlg = RS256}, a21Payload)) it "encodes the payload to the expected JWT" $ do let sign = either (error "Sign failed") id . RSAPKCS15.sign Nothing hashDescrSHA256 rsaPrivateKey@@ -79,11 +79,15 @@ let a31decoded = Right (defJwsHdr {jwsAlg = ES256}, a31Payload) it "decodes the JWT to the expected header and payload" $ do let Just k31 = decodeStrict' a31jwk- fst (decode RNG [k31] a31) @?= fmap Jws a31decoded+ fst (decode RNG [k31] Nothing a31) @?= fmap Jws a31decoded - context "when using an unsecured JWT" $+ context "when using an unsecured JWT" $ do+ it "returns an error if alg is unset" $+ fst (decode RNG [] Nothing jwt61) @?= Left (BadAlgorithm "JWT is unsecured but expected 'alg' was not 'none'")+ it "returns an error if alg is is not 'none'" $+ fst (decode RNG [] (Just (JwsEncoding RS256)) jwt61) @?= Left (BadAlgorithm "JWT is unsecured but expected 'alg' was not 'none'") it "decodes the JWT to the expected header and payload" $- fst (decode RNG [] jwt61) @?= Right (Unsecured jwt61Payload)+ fst (decode RNG [] (Just (JwsEncoding None)) jwt61) @?= Right (Unsecured jwt61Payload) signWithHeader sign hdr payload = B.intercalate "." [hdrPayload, B64.encode $ sign hdrPayload]@@ -152,4 +156,3 @@ } a11mac = hmac SHA256.hash 64 hmacKey-