diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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)
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/examples/Bot.hs b/examples/Bot.hs
new file mode 100644
--- /dev/null
+++ b/examples/Bot.hs
@@ -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"
diff --git a/src/Network/Wai/Middleware/SlackVerify.hs b/src/Network/Wai/Middleware/SlackVerify.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Wai/Middleware/SlackVerify.hs
@@ -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
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test/WaiMiddlewareSlackVerifySpec.hs b/test/WaiMiddlewareSlackVerifySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/WaiMiddlewareSlackVerifySpec.hs
@@ -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
diff --git a/wai-middleware-slack-verify.cabal b/wai-middleware-slack-verify.cabal
new file mode 100644
--- /dev/null
+++ b/wai-middleware-slack-verify.cabal
@@ -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
