packages feed

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 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