packages feed

jwt-0.11.0: tests/src/Web/JWTTests.hs

{-# LANGUAGE BangPatterns, OverloadedStrings, ScopedTypeVariables, TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Web.JWTTests
  (
    main
  , defaultTestGroup
) where

import           Test.Tasty
import           Test.Tasty.TH
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck
import qualified Test.QuickCheck       as QC
import qualified Data.Map              as Map
import qualified Data.Text             as T
import qualified Data.Text.Lazy        as TL
import qualified Data.ByteString       as BS
import           Data.Aeson.Types
import           Data.Maybe
import           Data.String (fromString)
import           Data.Time
import           Web.JWT
import qualified Crypto.PubKey.RSA as RSA
import qualified Crypto.Random.Types as CT
import qualified Data.ByteArray as BA

defaultTestGroup :: TestTree
defaultTestGroup = $(testGroupGenerator)

main :: IO ()
main = defaultMain defaultTestGroup

case_stringOrURIString = do
    let str = "foo bar baz 2312j!@&^#^*!(*@"
        sou = stringOrURI str
    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

case_numericDateDeriveOrd = do
    let i1 = numericDate 1231231231 -- Tue  6 Jan 2009 19:40:31 AEDT
        i2 = numericDate 1231232231 -- Tue  6 Jan 2009 19:57:11 AEDT
    LT @=? i1 `compare` i2

case_decodeJWT = do
    -- Generated with ruby-jwt
    let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"
        mJwt = decode input
    True @=? isJust mJwt
    True @=? isJust (fmap signature mJwt)
    let (Just unverified) = mJwt
    Just HS256 @=? alg (header unverified)
    Just "payload" @=? Map.lookup "some" (unClaimsMap $ unregisteredClaims $ claims unverified)

case_verify = do
    -- Generated with ruby-jwt
    let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"
        mVerified = verify (toVerify . hmacSecret $ "secret") =<< decode input
    True @=? isJust mVerified

case_decodeAndVerifyJWT = do
    -- Generated with ruby-jwt
    let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U"
        mJwt = decodeAndVerifySignature (toVerify . hmacSecret $ "secret") input
    True @=? isJust mJwt
    let (Just verified) = mJwt
    Just HS256 @=? alg (header verified)
    Just "payload" @=? Map.lookup "some" (unClaimsMap $ unregisteredClaims $ claims verified)

-- It must be impossible to get a VerifiedJWT if alg is "none"
case_decodeAndVerifyJWTAlgoNone = do
    {-
    - Header:
            {
              "alg": "none",
              "typ": "JWT"
            }
      Payload:
            {
              "iss": "https://jwt-idp.example.com",
              "sub": "mailto:mike@example.com",
              "nbf": 1425980755,
              "exp": 1425984355,
              "iat": 1425980755,
              "jti": "id123456",
              "typ": "https://example.com/register"
            }
    -}
    let input = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTQyNTk4MDc1NSwiZXhwIjoxNDI1OTg0MzU1LCJpYXQiOjE0MjU5ODA3NTUsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9."
        mJwt = decodeAndVerifySignature (toVerify . hmacSecret $ "secretkey") input
    False @=? isJust mJwt

case_decodeAndVerifyJWTFailing = do
    -- Generated with ruby-jwt, modified to be invalid
    let input = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2u"
        mJwt = decodeAndVerifySignature (toVerify . hmacSecret $ "secret") input
    False @=? isJust mJwt

case_decodeInvalidInput = do
    let inputs = ["", "a.", "a.b"]
        result = map decode inputs
    True @=? all isNothing result

case_decodeAndVerifySignatureInvalidInput = do
    let inputs = ["", "a.", "a.b"]
        result = map (decodeAndVerifySignature (toVerify . hmacSecret $ "secret")) inputs
    True @=? all isNothing result

case_encodeJWTNoMac = do
    let cs = mempty {
        iss = stringOrURI "Foo"
      , unregisteredClaims = ClaimsMap $ Map.fromList [("http://example.com/is_root", Bool True)]
    }
        jwt = encodeUnsigned cs mempty
    -- Verify the shape of the JWT, ensure the shape of the triple of
    -- <header>.<claims>.<signature>
    let (h:c:s:_) = T.splitOn "." jwt
    False @=? T.null h
    False @=? T.null c
    True  @=? T.null s


case_encodeDecodeJWTNoMac = do
    let cs = mempty {
        iss = stringOrURI "Foo"
      , unregisteredClaims = ClaimsMap $ Map.fromList [("http://example.com/is_root", Bool True)]
    }
        mJwt = decode $ encodeUnsigned cs mempty
    True @=? isJust mJwt
    let (Just unverified) = mJwt
    cs @=? claims unverified

case_encodeDecodeJWT = do
    let now = 1394573404
        cs = mempty {
        iss = stringOrURI "Foo"
      , iat = numericDate now
      , unregisteredClaims = ClaimsMap $ Map.fromList [("http://example.com/is_root", Bool True)]
    }
        key = hmacSecret "secret-key"
        mJwt = decode $ encodeSigned key mempty cs
    let (Just claims') = fmap claims mJwt
    cs @=? claims'
    Just now @=? fmap secondsSinceEpoch (iat claims')

case_tokenIssuer = do
    let iss' = stringOrURI "Foo"
        cs = mempty {
        iss = iss'
      , unregisteredClaims = ClaimsMap $ Map.fromList [("http://example.com/is_root", Bool True)]
    }
        key = hmacSecret "secret-key"
        t   = encodeSigned key mempty cs
    iss' @=? tokenIssuer t

case_encodeDecodeJWTClaimsSetCustomClaims = do
    let now = 1234
        cs = mempty {
        iss = stringOrURI "Foo"
      , iat = numericDate now
      , unregisteredClaims = ClaimsMap $ Map.fromList [("http://example.com/is_root", Bool True)]
    }
    let secret' =  hmacSecret "secret"
        jwt = decodeAndVerifySignature (toVerify secret') $ encodeSigned secret' mempty cs
    Just cs @=? fmap claims jwt

case_encodeDecodeJWTClaimsSetWithSingleAud = do
    let now = 1234
        cs = mempty {
            iss = stringOrURI "Foo"
          , aud = Left <$> stringOrURI "single-audience"
          , iat = numericDate now
        }
    let secret' = hmacSecret "secret"
        jwt = decodeAndVerifySignature (toVerify secret') $ encodeSigned secret' mempty cs
    Just cs @=? fmap claims jwt

case_encodeDecodeJWTClaimsSetWithMultipleAud = do
    let now = 1234
        cs = mempty {
            iss = stringOrURI "Foo"
          , aud = Right <$> (:[]) <$> stringOrURI "audience"
          , iat = numericDate now
        }
    let secret' = hmacSecret "secret"
        jwt = decodeAndVerifySignature (toVerify secret') $ encodeSigned secret' mempty cs
    Just cs @=? fmap claims jwt

case_encodeDecodeJWTClaimsSetBinarySecret = do
    let now = 1234
        cs = mempty {
            iss = stringOrURI "Foo"
          , iat = numericDate now
        }
    secretKey <- BS.readFile "tests/jwt.secret.1"
    let secret' = EncodeHMACSecret secretKey
        jwt = decodeAndVerifySignature (toVerify secret') $ encodeSigned secret' mempty cs
    Just cs @=? fmap claims jwt

prop_stringOrURIProp = f
    where f :: StringOrURI -> Bool
          f sou = let s = stringOrURI $ T.pack $ show sou
                  in Just sou == s

prop_stringOrURIToText= f
    where f :: T.Text -> Bool
          f t = let mSou = stringOrURI t
                in case mSou of
                       Just sou -> stringOrURIToText sou == t
                       Nothing  -> True

prop_encode_decode = f
    where f :: T.Text -> JWTClaimsSet -> Bool
          f key claims' = let Just unverified = (decode $ encodeSigned (hmacSecret key) mempty claims')
                          in claims unverified == claims'

prop_encode_decode_binary_secret = f
    where f :: BS.ByteString -> JWTClaimsSet -> Bool
          f binary claims' = let Just unverified = (decode $ encodeSigned (EncodeHMACSecret binary) mempty claims')
                          in claims unverified == claims'

prop_encode_decode_verify_signature = f
    where f :: T.Text -> JWTClaimsSet -> Bool
          f key' claims' = let key = hmacSecret key'
                               Just verified = (decodeAndVerifySignature (toVerify key) $ encodeSigned key mempty claims')
                           in claims verified == claims'

-- Generating a keypair takes over a second. Let's only do this a few times.
prop_rsa_verify_with_public_key = withMaxSuccess 20 f
    where f :: Keypair -> JWTClaimsSet -> Bool
          f kp claims' = let encodeSigner = EncodeRSAPrivateKey . kpPrivate $ kp
                             verifySigner = VerifyRSAPublicKey . kpPublic $ kp
                             signedToken = encodeSigned encodeSigner mempty claims'
                         in isJust $ decodeAndVerifySignature verifySigner signedToken

data Keypair = Keypair
    { kpPrivate :: RSA.PrivateKey
    , kpPublic :: RSA.PublicKey
    } deriving (Show)

instance Arbitrary (Keypair) where
    arbitrary = do
        (pubKey, privateKey) <- RSA.generate 256 3
        return $ Keypair { kpPrivate = privateKey, kpPublic = pubKey }

instance CT.MonadRandom Gen where
    getRandomBytes size = do
        bytes <- vector size
        return . BA.pack $ bytes

instance Arbitrary JWTClaimsSet where
    arbitrary = JWTClaimsSet <$> arbitrary
                             <*> arbitrary
                             <*> arbitrary
                             <*> arbitrary
                             <*> arbitrary
                             <*> arbitrary
                             <*> arbitrary
                             <*> arbitrary

instance Arbitrary ClaimsMap where
    arbitrary = return $ ClaimsMap Map.empty

instance Arbitrary NumericDate where
    arbitrary = fmap (f . numericDate) (arbitrary :: QC.Gen NominalDiffTime)
        where f = fromMaybe (fromJust $ numericDate 1)

instance Arbitrary NominalDiffTime where
    arbitrary = arbitrarySizedFractional
    shrink    = shrinkRealFrac

instance Arbitrary StringOrURI where
    arbitrary = fmap (f . stringOrURI) (arbitrary :: QC.Gen T.Text)
        where
            f = fromMaybe (fromJust $ stringOrURI "http://example.com")

instance Arbitrary BS.ByteString where
    arbitrary = BS.pack <$> arbitrary
    shrink xs = BS.pack <$> shrink (BS.unpack xs)

instance Arbitrary T.Text where
    arbitrary = fromString <$> (arbitrary :: QC.Gen String)

instance Arbitrary TL.Text where
    arbitrary = fromString <$> (arbitrary :: QC.Gen String)