slack-web-2.0.1.0: src/Web/Slack/Experimental/RequestVerification.hs
module Web.Slack.Experimental.RequestVerification (
SlackSigningSecret (..),
SlackSignature (..),
SlackRequestTimestamp (..),
SlackVerificationFailed (..),
validateRequest,
validateRequest',
) where
import Crypto.Hash (SHA256, digestFromByteString)
import Crypto.MAC.HMAC
import Data.Aeson (eitherDecodeStrict)
import Data.ByteString.Base16 qualified as B16
import Data.ByteString.Char8 (readInt)
import Data.Either.Combinators (mapLeft, maybeToRight)
import Data.Time (NominalDiffTime)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Web.HttpApiData (FromHttpApiData (..))
import Web.Slack.Prelude
-- | Slack generated Signing Secret placed into configuration.
-- See https://api.slack.com/authentication/verifying-requests-from-slack#signing_secrets_admin_page
newtype SlackSigningSecret
= SlackSigningSecret ByteString
deriving stock (Eq)
instance Show SlackSigningSecret where
show _ = "<SlackSigningSecret>"
newtype SlackSignature = SlackSignature ByteString
deriving newtype (Eq, Show)
newtype SlackRequestTimestamp = SlackRequestTimestamp ByteString
deriving newtype (Eq, Show)
instance FromHttpApiData SlackRequestTimestamp where
parseQueryParam _ = error "SlackRequestTimestamp should not be in a query param"
parseUrlPiece _ = error "SlackRequestTimestamp should not be in a url piece"
parseHeader = Right . SlackRequestTimestamp
instance FromHttpApiData SlackSignature where
parseQueryParam _ = error "SlackSignature should not be in a query param"
parseUrlPiece _ = error "SlackSignature should not be in a url piece"
parseHeader = Right . SlackSignature
data SlackVerificationFailed
= VerificationMissingTimestamp
| VerificationMalformedTimestamp ByteString
| VerificationTimestampOutOfRange Int
| VerificationMissingSignature
| VerificationUnknownSignatureVersion ByteString
| VerificationMalformedSignature String
| VerificationUndecodableSignature ByteString
| VerificationSignatureMismatch
| VerificationCannotParse Text
deriving stock (Show, Eq)
instance Exception SlackVerificationFailed
validateRequest ::
(MonadIO m, FromJSON a) =>
SlackSigningSecret ->
SlackSignature ->
SlackRequestTimestamp ->
ByteString ->
m (Either SlackVerificationFailed a)
validateRequest secret sig reqTs body =
liftIO getPOSIXTime >>= \time -> pure $ validateRequest' time secret sig reqTs body
-- | Pure version of 'validateRequest'. Probably only useful for tests.
validateRequest' ::
(FromJSON a) =>
NominalDiffTime ->
SlackSigningSecret ->
SlackSignature ->
SlackRequestTimestamp ->
ByteString ->
Either SlackVerificationFailed a
validateRequest' now (SlackSigningSecret secret) (SlackSignature sigHeader) (SlackRequestTimestamp timestampString) body = do
let fiveMinutes = 5 * 60
-- timestamp must be an Int for proper basestring construction below
timestamp <-
maybeToRight (VerificationMalformedTimestamp timestampString)
$ fst
<$> readInt timestampString
if abs (now - fromIntegral timestamp) > fiveMinutes
then Left $ VerificationTimestampOutOfRange timestamp
else Right ()
sigHeaderStripped <-
maybeToRight (VerificationUnknownSignatureVersion sigHeader)
$ stripPrefix "v0=" sigHeader
sigDecoded <-
mapLeft VerificationMalformedSignature
$ B16.decode sigHeaderStripped
sig :: HMAC SHA256 <-
maybeToRight (VerificationUndecodableSignature sigDecoded)
$ HMAC
<$> digestFromByteString sigDecoded
let basestring = encodeUtf8 ("v0:" <> tshow timestamp <> ":") <> body
when (hmac secret basestring /= sig)
$ Left VerificationSignatureMismatch
mapLeft (VerificationCannotParse . pack) $ eitherDecodeStrict body