jwt 0.2.1 → 0.3.0
raw patch · 4 files changed
+72/−24 lines, 4 filesdep −tasty-hspec
Dependencies removed: tasty-hspec
Files
- changelog +6/−0
- jwt.cabal +1/−2
- src/Web/JWT.hs +59/−22
- tests/src/Web/JWTTests.hs +6/−0
changelog view
@@ -1,3 +1,9 @@+2014-06-02 0.3.0+================++ * Add verify function (thanks to Robert Massaioli) to allow verifying an+ already decoded JWT token+ 2014-03-10 0.2.1 =================
jwt.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: jwt-version: 0.2.1+version: 0.3.0 synopsis: JSON Web Token (JWT) decoding and encoding license: MIT license-file: LICENSE@@ -62,7 +62,6 @@ build-depends: base < 5 && >= 4.4 , tasty >= 0.7 , tasty-th >= 0.1- , tasty-hspec >= 0.1 , tasty-hunit >= 0.4 , tasty-quickcheck >= 0.3 , HUnit
src/Web/JWT.hs view
@@ -28,7 +28,9 @@ ( -- * Encoding & Decoding JWTs -- ** Decoding+ -- $docDecoding decode+ , verify , decodeAndVerifySignature -- ** Encoding , encodeSigned@@ -104,24 +106,24 @@ -- | The JSON Web Token data JWT r where- Unverified :: JWTHeader -> JWTClaimsSet -> JWT UnverifiedJWT+ Unverified :: JWTHeader -> JWTClaimsSet -> Signature -> T.Text -> JWT UnverifiedJWT Verified :: JWTHeader -> JWTClaimsSet -> Signature -> JWT VerifiedJWT deriving instance Show (JWT r) -- | Extract the claims set from a JSON Web Token claims :: JWT r -> JWTClaimsSet-claims (Unverified _ c) = c+claims (Unverified _ c _ _) = c claims (Verified _ c _) = c -- | Extract the header from a JSON Web Token header :: JWT r -> JWTHeader-header (Unverified h _) = h+header (Unverified h _ _ _) = h header (Verified h _ _) = h -- | Extract the signature from a verified JSON Web Token signature :: JWT r -> Maybe Signature-signature (Unverified _ _) = Nothing+signature Unverified{} = Nothing signature (Verified _ _ s) = Just s -- | A JSON numeric value representing the number of seconds from@@ -271,14 +273,40 @@ -- Just (JWTClaimsSet {iss = Nothing, sub = Nothing, aud = Nothing, exp = Nothing, nbf = Nothing, iat = Nothing, jti = Nothing, unregisteredClaims = fromList [("some",String "payload")]}) decode :: JSON -> Maybe (JWT UnverifiedJWT) decode input = do- (h,c) <- extractElems $ T.splitOn "." input+ (h,c,s) <- extractElems $ T.splitOn "." input let header' = parseJWT h claims' = parseJWT c- Unverified <$> header' <*> claims'+ Unverified <$> header' <*> claims' <*> (pure . Signature $ s) <*> (pure . dotted $ [h,c]) where- extractElems (h:c:_) = Just (h,c)+ extractElems (h:c:s:_) = Just (h,c,s) extractElems _ = Nothing +-- | Using a known secret and a decoded claims set verify that the signature is correct+-- and return a verified JWT token as a result.+--+-- This will return a VerifiedJWT if and only if the signature can be verified using the+-- given secret.+--+-- The separation between decode and verify is very useful if you are communicating with+-- multiple different services with different secrets and it allows you to lookup the+-- correct secret for the unverified JWT before trying to verify it. If this is not an+-- isuse for you (there will only ever be one secret) then you should just use+-- 'decodeAndVerifySignature'.+--+-- >>> :{+-- let+-- input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text+-- mUnverifiedJwt = decode input+-- mVerifiedJwt = verify (secret "secret") =<< mUnverifiedJwt+-- in signature =<< mVerifiedJwt+-- :}+-- Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U")+verify :: Secret -> JWT UnverifiedJWT -> Maybe (JWT VerifiedJWT)+verify secret' (Unverified header' claims' unverifiedSignature originalClaim) = do+ algo <- alg header'+ let calculatedSignature = Signature $ calculateDigest algo secret' originalClaim+ guard (unverifiedSignature == calculatedSignature)+ pure $ Verified header' claims' calculatedSignature -- | Decode a claims set and verify that the signature matches by using the supplied secret. -- The algorithm is based on the supplied header value.@@ -290,22 +318,11 @@ -- let -- input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text -- mJwt = decodeAndVerifySignature (secret "secret") input--- in join $ fmap signature mJwt+-- in signature =<< mJwt -- :} -- Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U")-decodeAndVerifySignature :: Secret -> T.Text -> Maybe (JWT VerifiedJWT)-decodeAndVerifySignature secret' input = do- (h,c,s) <- extractElems $ T.splitOn "." input- header' <- parseJWT h- claims' <- parseJWT c- algo <- fmap alg header'- let sign = if Just s == calculateMessageDigest h c algo then pure $ Signature s else mzero- Verified <$> header' <*> claims' <*> sign- where- calculateMessageDigest header' claims' (Just algo') = Just $ calculateDigest algo' secret' (dotted [header', claims'])- calculateMessageDigest _ _ Nothing = Nothing- extractElems (h:c:s:_) = Just (h,c,s)- extractElems _ = Nothing+decodeAndVerifySignature :: Secret -> JSON -> Maybe (JWT VerifiedJWT)+decodeAndVerifySignature secret' input = verify secret' =<< decode input -- | Try to extract the value for the issue claim field 'iss' from the web token in JSON form tokenIssuer :: JSON -> Maybe StringOrURI@@ -424,4 +441,24 @@ parseJSON (String s) = return $ S s parseJSON _ = mzero -+-- $docDecoding+-- There are three use cases supported by the set of decoding/verification+-- functions:+--+-- (1) Plaintext JWTs (<http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-16#section-6>).+-- This is supported by the decode function 'decode'.+-- As a client you don't care about signing or encrypting so you only get back a 'JWT' 'UnverifiedJWT'.+-- I.e. the type makes it clear that no signature verification was attempted.+--+-- (2) Signed JWTs you want to verify using a known secret.+-- This is what 'decodeAndVerifySignature' supports, given a secret+-- and JSON it will return a 'JWT' 'VerifiedJWT' if the signature can be+-- verified.+--+-- (3) Signed JWTs that need to be verified using a secret that depends on+-- information contained in the JWT. E.g. the secret depends on+-- some claim, therefore the JWT needs to be decoded first and after+-- retrieving the appropriate secret value, verified in a subsequent step.+-- This is supported by using the `verify` function which given+-- a 'JWT' 'UnverifiedJWT' and a secret will return a 'JWT' 'VerifiedJWT' iff the+-- signature can be verified.
tests/src/Web/JWTTests.hs view
@@ -51,6 +51,12 @@ Just HS256 @=? alg (header unverified) Just "payload" @=? Map.lookup "some" (unregisteredClaims $ claims unverified) +case_verify = do+ -- Generated with ruby-jwt+ let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"+ mVerified = verify (secret "secret") =<< decode input+ True @=? isJust mVerified+ case_decodeAndVerifyJWT = do -- Generated with ruby-jwt let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"