wai-middleware-slack-verify (empty) → 0.1.0.0
raw patch · 8 files changed
+321/−0 lines, 8 filesdep +HUnitdep +basedep +bytestringsetup-changed
Dependencies added: HUnit, base, bytestring, cryptonite, errors, hspec, http-types, linklater, memory, text, time, wai, wai-extra, wai-middleware-slack-verify, warp
Files
- LICENSE +19/−0
- README.md +5/−0
- Setup.hs +2/−0
- examples/Bot.hs +21/−0
- src/Network/Wai/Middleware/SlackVerify.hs +92/−0
- test/Spec.hs +1/−0
- test/WaiMiddlewareSlackVerifySpec.hs +80/−0
- wai-middleware-slack-verify.cabal +101/−0
+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 208 Brandon Hamilton++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,5 @@+# wai-middleware-slack-verify++Middleware for WAI that uses [signed secrets to verify Slack requests](https://api.slack.com/docs/verifying-requests-from-slack).++Documentation on [Hackage](https://hackage.haskell.org/package/wai-middleware-slack-verify)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/Bot.hs view
@@ -0,0 +1,21 @@+-- | Example slack bot responding that will respond to slash commands+{-# LANGUAGE LambdaCase #-}++import Data.ByteString.Char8 (pack)+import Network.Linklater (slashSimple)+import Network.Wai+import Network.Wai.Handler.Warp (run)+import Network.Wai.Middleware.SlackVerify (verifySlackRequest)+import System.Environment (lookupEnv)++main :: IO ()+main = lookupEnv "SLACK_SIGNING_SECRET" >>= \case+ Just secret -> do+ let port = 8080+ putStrLn $ "Running on port " ++ show port+ run port $ verifySlackRequest (pack secret) $ slashSimple $ \_ ->+ pure "👋 "+ Nothing ->+ putStrLn+ $ "Expected 'SLACK_SIGNING_SECRET' environment variable.\n"+ ++ "See https://api.slack.com/docs/verifying-requests-from-slack"
+ src/Network/Wai/Middleware/SlackVerify.hs view
@@ -0,0 +1,92 @@+-----------------------------------------------------------------------------+-- |+-- Module : Network.Wai.Middleware.SlackVerify+-- Description : WAI Slack request verification Middleware+-- Copyright : (c) 2018 Brandon Hamilton+-- License : MIT+-- Maintainer : Brandon Hamilton <brandon.hamilton@gmail.com>+--+-- Middleware for WAI that uses signed secrets to verify Slack requests.+-- See <https://api.slack.com/docs/verifying-requests-from-slack>+--++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
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/WaiMiddlewareSlackVerifySpec.hs view
@@ -0,0 +1,80 @@+module WaiMiddlewareSlackVerifySpec+ ( spec+ ) where++import Crypto.Hash+import Crypto.Hash.Algorithms+import Crypto.MAC.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.Time.Clock.POSIX+import Test.Hspec+import Test.HUnit hiding (Test)+import Network.Wai+import Network.Wai.Test+import Network.Wai.Middleware.SlackVerify+import Network.HTTP.Types++spec :: Spec+spec = describe "Network.Wai.Middleware.SlackVerify" $ do+ it "authenticates valid signatures" caseValidSignature+ it "rejects missing signature" caseMissingSignature+ it "rejects signature mismatch" caseSignatureMismatch++testApp :: ByteString -> FailureResponse -> Application+testApp secret h =+ verifySlackRequest' secret h $ \_ f -> f $ responseLBS status200 [] ""++testSecret = "<RANDOM SECRET>"++testHandler e _ res = res $ responseLBS status403 [] msg+ where+ msg = case e of+ NoSignature -> "No signature"+ SignatureMismatch -> "Signature Mismatch"++createHeaders :: ByteString -> ByteString -> [(HeaderName, ByteString)]+createHeaders sig timestamp =+ [ ("Content-Type" , "application/json")+ , ("X-Slack-Request-Timestamp", timestamp)+ , ("X-Slack-Signature" , sig)+ ]++getTimestamp :: IO ByteString+getTimestamp = BC.pack . show . floor <$> getPOSIXTime++caseValidSignature :: Assertion+caseValidSignature = do+ timestamp <- getTimestamp+ flip runSession (testApp testSecret testHandler) $ do+ res <- request defaultRequest+ { requestHeaders = createHeaders (generateSignature timestamp)+ timestamp+ }+ assertStatus 200 res+ assertBody "" res+ where+ generateSignature t =+ let+ HMAC h =+ hmac testSecret $ (BS.concat ["v0", ":", t, ":", ""]) :: HMAC SHA256+ in BS.append "v0=" (convertToBase Base16 h)++caseMissingSignature :: Assertion+caseMissingSignature = do+ flip runSession (testApp testSecret testHandler) $ do+ res <- request defaultRequest+ assertStatus 403 res+ assertBody "No signature" res++caseSignatureMismatch :: Assertion+caseSignatureMismatch = do+ timestamp <- getTimestamp+ flip runSession (testApp testSecret testHandler) $ do+ res <- request defaultRequest+ { requestHeaders = createHeaders "<invalid>" timestamp+ }+ assertStatus 403 res+ assertBody "Signature Mismatch" res
+ wai-middleware-slack-verify.cabal view
@@ -0,0 +1,101 @@+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 66172e90e1e8bd7a72cdad63d36e2f7721ade3fd4716ca57b7e6bbaf18145423++name: wai-middleware-slack-verify+version: 0.1.0.0+synopsis: WAI Slack request verification middleware+description: Middleware for WAI that uses signed secrets to verify Slack requests.\n + See <https://api.slack.com/docs/verifying-requests-from-slack>+category: Web+homepage: https://github.com/brandonhamilton/wai-middleware-slack-verify#readme+bug-reports: https://github.com/brandonhamilton/wai-middleware-slack-verify/issues+author: Brandon Hamilton+maintainer: brandon.hamilton@gmail.com+copyright: Copyright (c) 2018 Brandon Hamilton+license: MIT+license-file: LICENSE+build-type: Simple+cabal-version: >= 1.10+extra-source-files:+ README.md++source-repository head+ type: git+ location: https://github.com/brandonhamilton/wai-middleware-slack-verify++flag build-example+ description: Build example executable.+ manual: True+ default: False++library+ exposed-modules:+ Network.Wai.Middleware.SlackVerify+ other-modules:+ Paths_wai_middleware_slack_verify+ hs-source-dirs:+ src+ default-extensions: OverloadedStrings+ build-depends:+ base >=4.7 && <5+ , bytestring+ , cryptonite+ , errors+ , http-types >=0.8+ , memory+ , wai >=3.0+ default-language: Haskell2010++executable bot-example+ main-is: Bot.hs+ other-modules:+ Paths_wai_middleware_slack_verify+ hs-source-dirs:+ examples+ default-extensions: OverloadedStrings+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ base >=4.7 && <5+ , bytestring+ , cryptonite+ , errors+ , http-types >=0.8+ , memory+ , wai >=3.0+ if flag(build-example)+ build-depends:+ linklater+ , text+ , wai-middleware-slack-verify+ , warp+ else+ buildable: False+ default-language: Haskell2010++test-suite wai-middleware-slack-verify-test+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ other-modules:+ WaiMiddlewareSlackVerifySpec+ Paths_wai_middleware_slack_verify+ hs-source-dirs:+ test+ default-extensions: OverloadedStrings+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ build-depends:+ HUnit+ , base >=4.7 && <5+ , bytestring+ , cryptonite+ , errors+ , hspec >=1.3+ , http-types >=0.8+ , memory+ , time+ , wai >=3.0+ , wai-extra+ , wai-middleware-slack-verify+ default-language: Haskell2010