Slack webhook events verified in Redwood functions

Hello :wave:

First time poster and first time using RedwoodJS. :dancer:

I’ve been working on a Slack integration application and need to receive their events API. I couldn’t find another example of the Slack Events API in the Redwood Webhook docs so I thought I’d share what I did because there are a few quirks with the Slack signature you want to be aware of.

I’d love to hear any improvements anyone would like to share. Otherwise I simply wanted to put this out there to hopefully accelerate someone else’s development time.

Slack uses the same SHA256 Verifier as GitHub and Discourse but you need a signature transformer create the expected format for the signature verifier function because Slack doesn’t include the algorithm.

import type { APIGatewayEvent } from 'aws-lambda'

import {
  verifyEvent,
  VerifyOptions,
  WebhookVerificationError,
} from '@redwoodjs/api/dist/webhooks'

import { logger as webhookLogger } from 'src/lib/logger'

export const handler = async (event: APIGatewayEvent) => {
  const logger = webhookLogger.child({ webhook: 'slack' })
  logger.info('Invoked slackEvents function')

  try {
    const slackTimestamp = event.headers['x-slack-request-timestamp'] || ''

    const options = {
      signatureHeader: 'x-slack-signature',
      // the timestamp comes in seconds format so convert to miliseconds
      eventTimestamp: Number.parseInt(slackTimestamp, 10) * 1000,
      signatureTransformer: (signature: string) => {
        // Slack passes a signature that starts with a version
        // example: v0=hash
        const [version, signatureHash] = signature.split('=')
        // Slack only supports version v0 currently
        if (version === 'v0') {
          // verifyEvent expects the signature to include the algorithm
          return `sha256=${signatureHash}`
        }
      },
    } as VerifyOptions

    const [signatureVersion] = (event.headers['x-slack-signature'] || '').split(
      '='
    )
    if (signatureVersion !== 'v0') {
      throw new WebhookVerificationError(
        `${signatureVersion}: unknown signature version`
      )
    }

    verifyEvent('sha256Verifier', {
      event,
      secret: process.env.SLACK_SIGNING_SECRET,
      payload: `${signatureVersion}:${slackTimestamp}:${event.body}`,
      options,
    })
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      logger.warn('Unauthorized')

      return {
        statusCode: 401,
      }
    } else {
      logger.error({ error }, error.message)

      return {
        headers: {
          'Content-Type': 'application/json',
        },
        statusCode: 500,
        body: JSON.stringify({
          error: error.message,
        }),
      }
    }
  }
}

3 Likes

@clarkbw this is excellent.

I have some thoughts about how a) we could add a Slack verifier and/or 2) make the verifiers more customizable so either take a string or a function to build aka transform values to get the signature or any other value the verifiers need.

I’ll add some thoughts to a GitHub issue — or if you’d like the start one I’ll add on with “ideas to improve webhook verifiers to support Slack”

Glad you got it working but I know we can incorporate these ideas into a release to help others out who need to use webhooks.

Thanks so much!

1 Like

This is great @dthyresson!

I created a GitHub issue and added some additional notes about the webhook mock function that would need some updates as well.

Thanks!

1 Like

@clarkbw Thanks! I’ll assign to myself and include some thoughts — and maybe it’s something you’d be interested in working on?

Happy to help if I’m useful :smile: