packages feed

wai-hmac-auth-1.0.0: tests/tests.hs

module Main where

import Control.Applicative
import Data.Byteable (toBytes)
import Crypto.Hash
import Test.Hspec
import Network.Wai 
import Network.Wai.Test (setPath)
import qualified Network.HTTP.Types as H
import qualified Data.ByteString as BS
import qualified Data.Sequence as S
import Network.Wai.Auth.HMAC
import Data.Maybe
import Data.Either
import qualified Data.ByteString.Base64.URL as B64

infixl 1 &
(&) :: a -> (a -> b) -> b
a & f = f a
{-# INLINE (&) #-}

setRequestMethod :: Request -> H.Method -> Request
setRequestMethod req method = req { requestMethod = method }

setRequestHeaders :: Request -> H.RequestHeaders -> Request
setRequestHeaders req headers = req { requestHeaders = headers }

removeRequestHeader :: H.HeaderName -> Request -> Request
removeRequestHeader targ req = setRequestHeaders req hdrs'
    where
    hdrs' = filter (not . (== targ) . fst) (requestHeaders req)

changeRequestHeader :: H.HeaderName -> BS.ByteString -> Request -> Request
changeRequestHeader targ newval req = setRequestHeaders req hdrs'
    where
    hdrs' = replacer <$> requestHeaders req
    replacer (n, v) = if n == targ then (n, newval) else (n, v)


mkRequest :: H.Method -> BS.ByteString -> H.RequestHeaders -> S.Seq BS.ByteString -> IO Request
mkRequest method path headers bodyChunks = defaultRequest `setRequestMethod` method `setRequestHeaders` headers `setPath` path `rerunRequestBody` bodyChunks

shouldBeLeft :: (Eq e, Show e, Show a) => Either e a -> e -> Expectation
shouldBeLeft e v = either (`shouldBe` v) (\r -> expectationFailure $ "expected: Left (" ++ show v ++ ")\n but got: " ++ show r) e

shouldJustSatisfy :: Show a => Maybe a -> (a -> Bool) -> Expectation
shouldJustSatisfy Nothing _ = expectationFailure "expected: Just a value\nbut got: Nothing"
shouldJustSatisfy (Just a) p = a `shouldSatisfy` p

failLeftOr :: Show e => Either e a -> (a -> Expectation) -> Expectation
failLeftOr e f = either (\l -> expectationFailure $ "expected: a Right value\nbut got: Left (" ++ show l ++ ")") f e

failNothing :: Maybe a -> (a -> Expectation) -> Expectation
failNothing m f = maybe (expectationFailure "expected: Just a value\nbut got: Nothing") f m


withSignedRequest :: HashAlgorithm alg => RequestConfig alg -> SecretKey -> IO Request -> (Request -> Expectation) -> Expectation
withSignedRequest conf k actReq f = do
    req <- actReq
    signRes <- signRequest conf req k
    failLeftOr signRes f

main :: IO ()
main = hspec $ do
    testGetApiKey
    testSignRequest
    testAuthenticate

testGetApiKey :: Spec
testGetApiKey = describe "getApiKey" $ do
    context "when using query param spec" $ do
        let spec = QueryParamKey "apiKey"
            getKey = getApiKey spec

        it "fails when no key is added" $ do
            req <- mkRequest H.methodGet "/" [] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "won't get key in header" $ do
            req <- mkRequest H.methodGet "/" [("apiKey", "somekey")] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "won't get key in different query param" $ do
            req <- mkRequest H.methodGet "/?passkey=somepass" [] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "is case sensitive" $ do
            req <- mkRequest H.methodGet "/?apikey=somepass" [] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "will get key in specified query param" $ do
            req <- mkRequest H.methodGet "/?apiKey=somepass" [] $ S.singleton "chunk"
            getKey req `shouldBe` Just (ApiKey "somepass")

    context "when using header spec" $ do
        let spec = HeaderKey "x-auth-apikey"
            getKey = getApiKey spec

        it "fails when no key is present" $ do
            req <- mkRequest H.methodGet "/" [] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "won't get key in query param" $ do
            req <- mkRequest H.methodGet "/?x-auth-apikey=somekey" [] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "won't get key in different header" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-passkey", "somekey")] $ S.singleton "chunk"
            getKey req `shouldBe` Nothing

        it "will get key in specified header" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-apikey", "somekey")] $ S.singleton "chunk"
            getKey req `shouldBe` Just (ApiKey "somekey")

        it "is NOT case sensitive" $ do
            req <- mkRequest H.methodGet "/" [("X-AUTH-APIKEY", "somekey")] $ S.singleton "chunk"
            getKey req `shouldBe` Just (ApiKey "somekey")

testSignRequest :: Spec
testSignRequest = describe "sign request" $ do
    context "when using a header api key" $ do
        let spec = HeaderKey "x-auth-apikey"
            conf = RequestConfig spec "x-auth-timestamp" "x-auth-signature" SHA256
            secretKey = SecretKey "test-key"
            confSign req = signRequest conf req secretKey

        it "demands the api key" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-timestamp", "2012-12-21T00:00:00Z")] $ S.singleton "chunk"
            signRes <- confSign req
            signRes `shouldBeLeft` MissingApiKey spec

        it "demands the timestamp" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-apikey", "somekey")] $ S.singleton "chunk"
            signRes <- confSign req
            signRes `shouldBeLeft` MissingTimestampHeader "x-auth-timestamp"

        it "produces a signed request" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-apikey", "somekey"), ("x-auth-timestamp", "2012-12-21T00:00:00Z")] $ S.singleton "chunk" S.|> "1 chunk2"
            signRes <- confSign req
            failLeftOr signRes $ \res -> 
                lookup "x-auth-signature" (requestHeaders res) `shouldSatisfy` isJust

        it "creates a base64-url encoded signature" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-apikey", "somekey"), ("x-auth-timestamp", "2012-12-21T00:00:00Z")] $ S.singleton "chunk" S.|> "1 chunk2"
            signRes <- confSign req
            failLeftOr signRes $ \res -> 
                failNothing (lookup "x-auth-signature" (requestHeaders res)) $ \hdr -> 
                    B64.decode hdr `shouldSatisfy` isRight

    context "when using a query param api key" $ do
        let spec = QueryParamKey "apiKey"
            conf = RequestConfig spec "x-auth-timestamp" "x-auth-signature" SHA256
            secretKey = SecretKey "test-key"
            confSign req = signRequest conf req secretKey

        it "demands the api key" $ do
            req <- mkRequest H.methodGet "/" [("x-auth-timestamp", "2012-12-21T00:00:00Z")] $ S.singleton "chunk"
            signRes <- confSign req
            signRes `shouldBeLeft` MissingApiKey spec

        it "demands the timestamp" $ do
            req <- mkRequest H.methodGet "/path?apiKey=somekey" [] $ S.singleton "chunk"
            signRes <- confSign req
            signRes `shouldBeLeft` MissingTimestampHeader "x-auth-timestamp"

        it "produces a signed request" $ do
            req <- mkRequest H.methodGet "/path?apiKey=somekey" [("x-auth-timestamp", "2012-12-21T00:00:00Z")] $ S.singleton "chunk"
            signRes <- confSign req
            failLeftOr signRes $ \res -> 
                lookup "x-auth-signature" (requestHeaders res) `shouldSatisfy` isJust

testAuthenticate :: Spec
testAuthenticate = describe "authenticate" $ do
    context "when using a query param api key" $ do
        let spec = QueryParamKey "apiKey"
            conf = RequestConfig spec "x-auth-timestamp" "x-auth-signature" SHA256
            secretKey = SecretKey "test-key"
            withSigned = withSignedRequest conf secretKey
            confAuthenticate req = authenticate conf req secretKey

        let simpleReq = mkRequest H.methodGet "/loc?apiKey=somekey" [("x-auth-timestamp", "2012-12-21T00:00:00Z")] (S.singleton "chunk" S.|> "1 chunk2")
        it "works with a simple signed request" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate sreq
            ares `shouldSatisfy` isRight

        it "complains about missing api key" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ setPath sreq "/loc"
            ares `shouldBeLeft` MissingApiKey spec

        it "complains about missing timestamp" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ removeRequestHeader "x-auth-timestamp" sreq
            ares `shouldBeLeft` MissingTimestampHeader "x-auth-timestamp"

        it "complains about missing signature" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ removeRequestHeader "x-auth-signature" sreq
            ares `shouldBeLeft` MissingSignatureHeader "x-auth-signature"

        it "complains about incorrectly-encoded signature" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ changeRequestHeader "x-auth-signature" "fake.it" sreq
            putStrLn (show ares)
            ares `shouldBeLeft` SignatureBase64DecodeFailed "invalid padding"
            ares2 <- confAuthenticate $ changeRequestHeader "x-auth-signature" (B64.encode "invalid") sreq
            ares2 `shouldBeLeft` SignatureToDigestFailed

        it "complains if the signature does not match" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ changeRequestHeader "x-auth-signature" (B64.encode $ toBytes $ (hash "what" :: Digest SHA256)) sreq
            ares `shouldBeLeft` HashMismatch

        context "when a request is already signed" $ do
            it "fails if the api key param changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ setPath sreq "/loc?apiKey=otherkey"
                ares `shouldBeLeft` HashMismatch

    context "when using a header api key" $ do
        let spec = HeaderKey "x-auth-apikey"
            conf = RequestConfig spec "x-auth-timestamp" "x-auth-signature" SHA256
            secretKey = SecretKey "test-key"
            withSigned = withSignedRequest conf secretKey
            confAuthenticate req = authenticate conf req secretKey

        let simpleReq = mkRequest H.methodGet "/loc" [("x-auth-apikey", "somekey"), ("x-auth-timestamp", "2012-12-21T00:00:00Z")] (S.singleton "chunk" S.|> "1 chunk2")
        it "works with a simple signed request" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate sreq
            ares `shouldSatisfy` isRight

        it "complains about missing api key header" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ removeRequestHeader "x-auth-apikey" sreq
            ares `shouldBeLeft` MissingApiKey spec

        it "complains about missing timestamp" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ removeRequestHeader "x-auth-timestamp" sreq
            ares `shouldBeLeft` MissingTimestampHeader "x-auth-timestamp"

        it "complains about missing signature" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ removeRequestHeader "x-auth-signature" sreq
            ares `shouldBeLeft` MissingSignatureHeader "x-auth-signature"

        it "complains about incorrectly-encoded signature" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ changeRequestHeader "x-auth-signature" "fake.it" sreq
            putStrLn (show ares)
            ares `shouldBeLeft` SignatureBase64DecodeFailed "invalid padding"
            ares2 <- confAuthenticate $ changeRequestHeader "x-auth-signature" (B64.encode "invalid") sreq
            ares2 `shouldBeLeft` SignatureToDigestFailed

        it "complains if the signature does not match" $ withSigned simpleReq $ \sreq -> do
            ares <- confAuthenticate $ changeRequestHeader "x-auth-signature" (B64.encode $ toBytes $ (hash "what" :: Digest SHA256)) sreq
            ares `shouldBeLeft` HashMismatch

        context "when a request is already signed" $ do
            it "fails if the method changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ setRequestMethod sreq "PUT" 
                ares `shouldBeLeft` HashMismatch

            it "fails if the path changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ setPath sreq "/pathnew"
                ares `shouldBeLeft` HashMismatch

            it "fails if the query string changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ setPath sreq "/loc?query=value"
                ares `shouldBeLeft` HashMismatch

            it "fails if the timestamp changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ changeRequestHeader "x-auth-timestamp" "faketime" sreq
                ares `shouldBeLeft` HashMismatch

            it "fails if the api key header changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate $ changeRequestHeader "x-auth-apikey" "otherkey" sreq
                ares `shouldBeLeft` HashMismatch

            it "fails if the body changes" $ withSigned simpleReq $ \sreq -> do
                ares <- confAuthenticate =<< rerunRequestBody sreq (S.singleton "chunk0 ")
                ares `shouldBeLeft` HashMismatch