jwt 0.2.0 → 0.2.1
raw patch · 5 files changed
+88/−95 lines, 5 files
Files
- changelog +11/−2
- jwt.cabal +1/−1
- src/Web/JWT.hs +51/−69
- tests/src/Web/Base64Tests.hs +2/−2
- tests/src/Web/JWTTests.hs +23/−21
changelog view
@@ -1,14 +1,23 @@+2014-03-10 0.2.1+=================++ * Add Decoding/Encoding sections+ * Make the examples runnable by doctest+ * Fix hlint warnings+ * Add 'secondsSinceEpoch' to extract the seconds from epoch from an IntDate++ 2014-03-10 0.2.0 ================= * Export the IntDate and StringOrURI types #5a1137b -2014-03-03 0.1.1+2014-03-03 0.1.1 ================= * Verify that invalid input to the decode* functions fails as expected -2014-03-03 0.1.0+2014-03-03 0.1.0 ================= * Initial release
jwt.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: jwt-version: 0.2.0+version: 0.2.1 synopsis: JSON Web Token (JWT) decoding and encoding license: MIT license-file: LICENSE
src/Web/JWT.hs view
@@ -27,8 +27,10 @@ module Web.JWT ( -- * Encoding & Decoding JWTs+ -- ** Decoding decode , decodeAndVerifySignature+ -- ** Encoding , encodeSigned , encodeUnsigned @@ -43,6 +45,7 @@ -- ** JWT claims set , intDate , stringOrURI+ , secondsSinceEpoch -- ** JWT header , typ , cty@@ -122,9 +125,12 @@ signature (Verified _ _ s) = Just s -- | A JSON numeric value representing the number of seconds from--- 1970-01-01T0:0:0Z UTC until the specified UTC date/time.+-- 1970-01-01T0:0:0Z UTC until the specified UTC date/time. newtype IntDate = IntDate Integer deriving (Show, Eq) +-- | Return the seconds since 1970-01-01T0:0:0Z UTC for the given 'IntDate'+secondsSinceEpoch :: IntDate -> NominalDiffTime+secondsSinceEpoch (IntDate s) = fromInteger s -- | A JSON string value, with the additional requirement that while -- arbitrary string values MAY be used, any value containing a ":"@@ -197,23 +203,20 @@ def = JWTClaimsSet Nothing Nothing Nothing Nothing Nothing Nothing Nothing Map.empty ++ -- | Encode a claims set using the given secret ----- > {-# LANGUAGE OverloadedStrings #-}--- > import Data.Aeson--- > import qualified Data.Map as Map--- >--- > let cs = def { -- def returns a default JWTClaimsSet--- > iss = stringOrURI "Foo"--- > , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]--- > }--- > key = secret "secret-key"--- > jwt = encodeSigned HS256 key cs------ This yields:------ > >>> jwt--- > "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiRm9vIn0.vHQHuG3ujbnBUmEp-fSUtYxk27rLiP2hrNhxpyWhb2E"+-- >>> :{+-- let+-- cs = def { -- def returns a default JWTClaimsSet+-- iss = stringOrURI "Foo"+-- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+-- }+-- key = secret "secret-key"+-- in encodeSigned HS256 key cs+-- :}+-- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiRm9vIn0.vHQHuG3ujbnBUmEp-fSUtYxk27rLiP2hrNhxpyWhb2E" encodeSigned :: Algorithm -> Secret -> JWTClaimsSet -> JSON encodeSigned algo secret claims = dotted [header, claim, signature] where claim = encodeJWT claims@@ -225,20 +228,16 @@ -- | Encode a claims set without signing it ----- > {-# LANGUAGE OverloadedStrings #-}--- > import Data.Aeson--- > import qualified Data.Map as Map--- >--- > let cs = def { -- def returns a default JWTClaimsSet--- > iss = stringOrURI "Foo"--- > , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]--- > }--- > jwt = encodeUnsigned cs------ This yields:------ > >>> jwt--- > "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vZXhhbXBsZS5jb20vaXNfcm9vdCI6dHJ1ZSwiaXNzIjoiRm9vIn0."+-- >>> :{+-- let+-- cs = def { -- def returns a default JWTClaimsSet+-- iss = stringOrURI "Foo"+-- , iat = intDate 1394700934+-- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+-- }+-- in encodeUnsigned cs+-- :}+-- "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjEzOTQ3MDA5MzQsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJGb28ifQ." encodeUnsigned :: JWTClaimsSet -> JSON encodeUnsigned claims = dotted [header, claim, ""] where claim = encodeJWT claims@@ -253,29 +252,23 @@ -- (e.g. the secret needs to be retrieved based on unverified information -- from the claims set). ----- > import qualified Data.Text as T--- > let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text--- > mJwt = decode input--- > mHeader = fmap header mJwt--- > mClaims = fmap claims mJwt--- > mSignature = join $ fmap signature mJwt------ This yields:------ > >>> mHeader--- > Just (JWTHeader {typ = Just "JWT", cty = Nothing, alg = Just HS256})------ and------ > >>> mClaims--- > Just (JWTClaimsSet {iss = Nothing, sub = Nothing, aud = Nothing,--- > exp = Nothing, nbf = Nothing, iat = Nothing, jti = Nothing,--- > unregisteredClaims = fromList [("some",String "payload")]})+-- >>> :{+-- let+-- input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text+-- mJwt = decode input+-- in fmap header mJwt+-- :}+-- Just (JWTHeader {typ = Just "JWT", cty = Nothing, alg = Just HS256}) -- -- and ----- > >>> mSignature--- > Nothing+-- >>> :{+-- let+-- input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text+-- mJwt = decode input+-- in fmap claims mJwt+-- :}+-- Just (JWTClaimsSet {iss = Nothing, sub = Nothing, aud = Nothing, exp = Nothing, nbf = Nothing, iat = Nothing, jti = Nothing, unregisteredClaims = fromList [("some",String "payload")]}) decode :: JSON -> Maybe (JWT UnverifiedJWT) decode input = do (h,c) <- extractElems $ T.splitOn "." input@@ -293,24 +286,13 @@ -- This will return a VerifiedJWT if and only if the signature can be verified -- using the given secret. ----- > import qualified Data.Text as T--- > let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text--- > mJwt = decodeAndVerifySignature (secret "secret") input--- > mSignature = join $ fmap signature mJwt------ This yields:------ > >>> mJwt--- > Just (Verified (JWTHeader {typ = Just "JWT", cty = Nothing, alg = Just HS256})--- > (JWTClaimsSet {iss = Nothing, sub = Nothing, aud = Nothing, exp = Nothing,--- > nbf = Nothing, iat = Nothing, jti = Nothing,--- > unregisteredClaims = fromList [("some",String "payload")]})--- > (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"))------ and------ > >>> mSignature--- > Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U")+-- >>> :{+-- let+-- input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" :: T.Text+-- mJwt = decodeAndVerifySignature (secret "secret") input+-- in join $ fmap signature mJwt+-- :}+-- Just (Signature "Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U") decodeAndVerifySignature :: Secret -> T.Text -> Maybe (JWT VerifiedJWT) decodeAndVerifySignature secret' input = do (h,c,s) <- extractElems $ T.splitOn "." input@@ -347,7 +329,7 @@ -- String cannot be converted (e.g. if the String contains a ':' but is -- *not* a valid URI). stringOrURI :: T.Text -> Maybe StringOrURI-stringOrURI t | URI.isURI $ T.unpack t = U <$> (URI.parseURI $ T.unpack t)+stringOrURI t | URI.isURI $ T.unpack t = U <$> URI.parseURI (T.unpack t) stringOrURI t = Just (S t) -- =================================================================================
tests/src/Web/Base64Tests.hs view
@@ -38,7 +38,7 @@ case_base64EncodeDecodeStringNoPadding = do let header = "sdjkfhaks jdhfak sjldhfa lkjsdf"- header @=? (base64Decode $ base64Encode header)+ header @=? base64Decode (base64Encode header) case_base64DecodeString = do let str = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9"@@ -46,7 +46,7 @@ prop_base64_encode_decode = f where f :: T.Text -> Bool- f input = (base64Decode $ base64Encode input) == input+ f input = base64Decode (base64Encode input) == input instance Arbitrary T.Text where
tests/src/Web/JWTTests.hs view
@@ -16,11 +16,11 @@ import qualified Data.Text as T import qualified Data.Text.Lazy as TL import Data.Aeson.Types-import qualified Data.Aeson as JSON import Data.Maybe import Data.String (fromString, IsString) import Data.Time + import Web.JWT defaultTestGroup :: TestTree@@ -34,22 +34,22 @@ case_stringOrURIString = do let str = "foo bar baz 2312j!@&^#^*!(*@" sou = stringOrURI str- (Just str) @=? (fmap (T.pack . show) sou)+ Just str @=? fmap (T.pack . show) sou case_stringOrURI= do let str = "http://user@example.com:8900/foo/bar?baz=t;" sou = stringOrURI str- (Just str) @=? (fmap (T.pack . show) sou)+ Just str @=? fmap (T.pack . show) sou case_decodeJWT = do -- Generated with ruby-jwt let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U" mJwt = decode input True @=? isJust mJwt- True @=? (isJust $ fmap signature mJwt)+ True @=? isJust (fmap signature mJwt) let (Just unverified) = mJwt- (Just HS256) @=? (alg $ header unverified)- (Just "payload") @=? (Map.lookup "some" $ unregisteredClaims $ claims unverified)+ Just HS256 @=? alg (header unverified)+ Just "payload" @=? Map.lookup "some" (unregisteredClaims $ claims unverified) case_decodeAndVerifyJWT = do -- Generated with ruby-jwt@@ -57,8 +57,8 @@ mJwt = decodeAndVerifySignature (secret "secret") input True @=? isJust mJwt let (Just verified) = mJwt- (Just HS256) @=? (alg $ header verified)- (Just "payload") @=? (Map.lookup "some" $ unregisteredClaims $ claims verified)+ Just HS256 @=? alg (header verified)+ Just "payload" @=? Map.lookup "some" (unregisteredClaims $ claims verified) case_decodeAndVerifyJWTFailing = do -- Generated with ruby-jwt, modified to be invalid@@ -69,17 +69,17 @@ case_decodeInvalidInput = do let inputs = ["", "a.", "a.b"] result = map decode inputs- True @=? (all isNothing result)+ True @=? all isNothing result case_decodeAndVerifySignatureInvalidInput = do let inputs = ["", "a.", "a.b"] result = map (decodeAndVerifySignature (secret "secret")) inputs- True @=? (all isNothing result)+ True @=? all isNothing result case_encodeJWTNoMac = do let cs = def { iss = stringOrURI "Foo"- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+ , unregisteredClaims = Map.fromList [("http://example.com/is_root", Bool True)] } jwt = encodeUnsigned cs -- Verified using https://py-jwt-decoder.appspot.com/@@ -88,29 +88,31 @@ case_encodeDecodeJWTNoMac = do let cs = def { iss = stringOrURI "Foo"- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+ , unregisteredClaims = Map.fromList [("http://example.com/is_root", Bool True)] } mJwt = decode $ encodeUnsigned cs- True @=? (isJust mJwt)+ True @=? isJust mJwt let (Just unverified) = mJwt cs @=? claims unverified case_encodeDecodeJWT = do- let cs = def {+ let now = 1394573404+ cs = def { iss = stringOrURI "Foo"- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+ , iat = intDate now+ , unregisteredClaims = Map.fromList [("http://example.com/is_root", Bool True)] } key = secret "secret-key" mJwt = decode $ encodeSigned HS256 key cs- True @=? (isJust mJwt) let (Just unverified) = mJwt cs @=? claims unverified+ Just now @=? fmap secondsSinceEpoch (iat (claims unverified)) case_tokenIssuer = do let iss' = stringOrURI "Foo" cs = def { iss = iss'- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+ , unregisteredClaims = Map.fromList [("http://example.com/is_root", Bool True)] } key = secret "secret-key" t = encodeSigned HS256 key cs@@ -129,7 +131,7 @@ cs = def { iss = stringOrURI "Foo" , iat = intDate now- , unregisteredClaims = Map.fromList [("http://example.com/is_root", (Bool True))]+ , unregisteredClaims = Map.fromList [("http://example.com/is_root", Bool True)] } -- The expected string can be decoded using the ruby-jwt library "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjEyMzQsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlLCJpc3MiOiJGb28ifQ.F3VCSxBBnY2caX4AH4GvIHyTVUhOnJF9Av_G_N4m710" @=? encodeSigned HS256 (secret "secret") cs@@ -138,7 +140,7 @@ prop_stringOrURIProp = f where f :: StringOrURI -> Bool f sou = let s = stringOrURI $ T.pack $ show sou- in (Just sou) == s+ in Just sou == s prop_encode_decode_prop = f where f :: JWTClaimsSet -> Bool@@ -168,7 +170,7 @@ instance Arbitrary IntDate where arbitrary = fmap (f . intDate) (arbitrary :: QC.Gen NominalDiffTime)- where f mIntDate = fromMaybe (fromJust $ intDate 1) mIntDate+ where f = fromMaybe (fromJust $ intDate 1) instance Arbitrary NominalDiffTime where arbitrary = arbitrarySizedFractional@@ -177,7 +179,7 @@ instance Arbitrary StringOrURI where arbitrary = fmap (f . stringOrURI) (arbitrary :: QC.Gen T.Text) where- f mSou = fromMaybe (fromJust $ stringOrURI "http://example.com") mSou+ f = fromMaybe (fromJust $ stringOrURI "http://example.com") instance Arbitrary T.Text where arbitrary = fromString <$> (arbitrary :: QC.Gen String)