----------------------------------------------------------------------------- -- | -- Module : Network.Wai.Middleware.SlackVerify -- Description : WAI Slack request verification Middleware -- Copyright : (c) 2018 Brandon Hamilton -- License : MIT -- Maintainer : Brandon Hamilton -- -- Middleware for WAI that uses signed secrets to verify Slack requests. -- See -- module Network.Wai.Middleware.SlackVerify ( verifySlackRequest , verifySlackRequest' , VerificationFailure(..) , FailureResponse , SigningSecret ) where import Control.Error.Util import Crypto.Hash import Crypto.Hash.Algorithms (SHA256) import Crypto.MAC.HMAC (HMAC(..), hmac) import Data.ByteArray.Encoding (convertToBase, Base(Base16)) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BC import Data.IORef (newIORef, atomicModifyIORef) import Network.HTTP.Types (status403) import Network.Wai -- Create a copy of the request body -- Based on technique from -- https://github.com/yesodweb/wai/blob/master/wai-extra/Network/Wai/Middleware/RequestLogger.hs getRequestBody :: Request -> IO (Request, BC.ByteString) getRequestBody req = do let loop front = do bs <- requestBody req if BC.null bs then return $ front [] else loop $ front . (bs :) body <- loop id ichunks <- newIORef body let rbody = atomicModifyIORef ichunks $ \chunks -> case chunks of [] -> ([], BC.empty) x : y -> (y, x) let req' = req { requestBody = rbody } return (req', BC.concat body) -- | Verification Failure reasons data VerificationFailure = NoSignature -- ^ The request does not contain the relevant -- signature headers | SignatureMismatch -- ^ Signature of the request does not match -- the server generated signature deriving Show type FailureResponse = VerificationFailure -> Application type SigningSecret = ByteString defaultFailureResponse :: FailureResponse defaultFailureResponse f _ res = res $ responseLBS status403 [] "Invalid Signature" -- | Middleware that will verify an incoming slack request signature -- using the provided signing secret. verifySlackRequest :: SigningSecret -> Middleware verifySlackRequest secret app = verifySlackRequest' secret defaultFailureResponse app -- | Middleware that will verify an incoming slack request signature -- using the provided signing secret. The failure response handler -- will be called upon verification errors. verifySlackRequest' :: SigningSecret -> FailureResponse -> Middleware verifySlackRequest' secret onFailure app req res = do (req', payload) <- getRequestBody req case checkSignature payload of Left e -> onFailure e req' res Right _ -> app req' res where headers = requestHeaders req timestamp = note NoSignature (lookup "X-Slack-Request-Timestamp" headers) signature = note NoSignature (lookup "X-Slack-Signature" headers) checkSignature p = do t <- timestamp s <- signature let HMAC h = hmac secret $ (BS.concat ["v0", ":", t, ":", p]) :: HMAC SHA256 if s == BS.append "v0=" (convertToBase Base16 h) then Right () else Left SignatureMismatch