Verify Slack requests in AWS Lambda and Python

Posted on Sat, 30 June 2018 in development (Last modified on Fri, 13 July 2018)

Verification flow of a Slack POST request in Python and AWS Lambda

Slack announced yesterday a new way to verify the HTTP requests for e.g. Slash commands and Events API it sends to web server. In this article I will show how to implement this in Python and AWS Lambda.

Setting the stage

I have implemented Slash commands in Slack. My HTTP REST API which receives the POST requests from Slack is implemented as a serverless solution utilizing AWS API Gateway and Lambda function. The Lambda is hooked up to API Gateway as a proxy integration, so the Lambda receives a “raw request” from the client. Lambda function is implemented in Python 3.6.

As Slack sends requests, there is no real authentication when invoking my API endpoint. Some day it might be cool, if Slack enabled real auth to be used. Until now, the way I could verify that the requests are coming from Slack instead of some malicious user, used to be a simple shared secret, called Verification Token, which Slack sends in the HTTP body of the request in a key called token. I stored the token in Systems Manager Parameter Store as an encrypted parameter, which I then read from my Lambda function, and simply compared the two strings.

On June 29th Slack announced a new way to verify the requests, and the deprecation of the old way. The new and significantly more secure way is described in the Slack API docs. I will walk through the implementation in the following chapter.

Verify request in Lambda

This is by no means the complete implementation of my Slash command backend, just the part regarding the verification. The rest of the code and microservices are omitted.

First of all, there is a new shared secret, called Signing Secret, which I need to store in SSM Parameter Store, just as before.

Then, the flow goes as follows (quote from the Slack API docs):

Here’s an overview of the process to validate a signed request from Slack:

  1. Retrieve the X-Slack-Request-Timestamp header on the HTTP request, and the body of the request. Important: The HTTP request ‘body’ does not include the HTTP status line, query parameters, or HTTP headers. Evaluate only the raw HTTP request body when computing signatures.
  2. Concatenate the version number, the timestamp, and the body of the request to form a basestring. Use a colon as the delimiter between the three elements. For example, v0:123456789:command=/weather&text=94070. The version number right now is always v0.
  3. With the help of SHA256 implemented in your favorite programming, hash the above basestring, using the Slack Signing Secret as the key.
  4. Compare this computed signature to the `X-Slack-Signature header on the request.

Implementation in Python 3.6 as a AWS Lambda function:

import hashlib
import hmac
import logging
import boto3

''' Further imports and code omitted. '''

logger = logging.getLogger()
logger.setLevel(logging.WARNING)

''' Code omitted. '''

''' Get the encrypted Signing Secret from SSM.
    Lambda needs IAM permissions to SSM and to the decryption key.
'''
slack_signing_secret = boto3.client('ssm'). \
    get_parameter(
        Name='slack_signing_secret',
        WithDecryption=True)['Parameter']['Value']

''' Code omitted. '''


''' Verify the POST request. '''
def verify_slack_request(slack_signature=None, slack_request_timestamp=None, request_body=None):
    ''' Form the basestring as stated in the Slack API docs. We need to make a bytestring. '''
    basestring = f"v0:{slack_request_timestamp}:{request_body}".encode('utf-8')

    ''' Make the Signing Secret a bytestring too. '''
    slack_signing_secret = bytes(slack_signing_secret, 'utf-8')

    ''' Create a new HMAC "signature", and return the string presentation. '''
    my_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest()

    ''' Compare the the Slack provided signature to ours.
    If they are equal, the request should be verified successfully.
    Log the unsuccessful requests for further analysis
    (along with another relevant info about the request). '''
    if hmac.compare_digest(my_signature, slack_signature):
        return True
    else:
        logger.warning(f"Verification failed. my_signature: {my_signature}")
        return False


''' Process the POST request from API Gateway proxy integration. '''
def post(event, context):
    try:
        ''' Incoming data from Slack is application/x-www-form-urlencoded and UTF-8. '''

        ''' Capture the necessary data. '''
        slack_signature = event['headers']['X-Slack-Signature']
        slack_request_timestamp = event['headers']['X-Slack-Request-Timestamp']

        ''' Verify the request. '''
        if not verify_slack_request(slack_signature, slack_request_timestamp, event['body']):
            logger.info('Bad request.')
            response = {
                "statusCode": 400,
                "body": ''
            }
            return response

        ''' Keep processing the actual request, and respond to Slack in a timely fashion. '''

        ''' Code omitted. '''

    except Exception as e:
        ''' Just a stub. Please make this better in real use :) '''
        logger.error(f"ERROR: {e}")
        response = {
            "statusCode": 200,
            "body": ''
        }
        return response

The verification flow is still quite simple to implement, yet significantly more secure than before. Now, back to features!