February 5, 2019

Verifying Slack Slash Commands in Google Cloud Functions

Verifying Slack Slash Commands in Google Cloud Functions

Recently Slack moved from using "Validation Tokens" to validate that Slash Command requests were actually from Slack to signing the requests with a "Client Signing Secret".  This seems like a simple change but turned out to be a huge pain in the butt to figure out.

This problem stumped me for WAY longer than it should have. This similar article for AWS Lambdas helped a lot but for whatever reason, the nuance of GCF made this tricky.

The crux of the matter was sorting out that when using the HTTP Trigger in GCF, the passed in object is a flask.Response object.  Just learning that took me a while and subsequently finding the right documentation took even longer.  Then, it took me forever to figure out that the intuitive request.data attribute would not work for getting the request body of the POST request. You have to use request.get_data() which returns the string in bytes that have to be converted to test. This was all discovered after a few hours on this Stack Overflow question (last answer).

Following Slacks guide for verifying requests, I was finally able to get this thing working.

import hashlib
import hmac
import os

os.environ.get('SLACK_SIGNING_SECRET')

# Verify that the Slack request is authentic
def verify(request):
    slack_signing_secret = bytes(os.environ.get('SLACK_SIGNING_SECRET'), 'utf-8')
    timestamp = request.headers['X-Slack-Request-Timestamp']
    request_data = request.get_data().decode('utf-8')
    basestring = f"v0:{timestamp}:{request_data}".encode('utf-8')

    # Create our own signature to compare to the one sent
    my_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest()
    
    if hmac.compare_digest(my_signature, request.headers['X-Slack-Signature']):
        return True
    else:
        return False
    
def main(request):
    if verify(request):
        return 'Hello, world!'
    else:
        return ('Request failed verification.', 401)

Now that this is done, I can finally move on to actually making this thing functional.