'How to validate Slack API request?

I've a slack app that is sending to a service written in typescript that is forwarding the message to my python script where I'm trying to validate the request. However, for some reason, the validation always fails. The typescript relevant code:

const rp = require('request-promise');
var qs = require('querystring')


export const handler = async (event: any, context: Context, callback: Callback): Promise<any> => {
    const options = {
        method: method,
        uri: some_url,
        body: qs.parse(event.body),
        headers: {
            signature: event.headers['X-Slack-Signature'],
            timestamp: event.headers['X-Slack-Request-Timestamp']
        },
        json: true
    };

    return rp(options);

The python code (based on this article) :

  def authenticate_message(self, request: Request) -> bool:
        slack_signing_secret = bytes(SLACK_SIGNING_SECRET, 'utf-8')

        slack_signature = request.headers['signature']
        slack_timestamp = request.headers['timestamp']

        request_body = json.loads(request.body)['payload']

        basestring = f"v0:{slack_timestamp}:{request_body}".encode('utf-8')
        my_signature = 'v0=' + hmac.new(slack_signing_secret, basestring, hashlib.sha256).hexdigest()

        return hmac.compare_digest(my_signature, slack_signature))

I'm pretty sure the issue is the way I'm taking the body but tried several options and still no luck.

Any ideas?

Thanks, Nir.



Solution 1:[1]

I had the same issue. My solution was to parse the payload to replace '/' by %2F and ':' by %3A. It's not explicit in the Slack doc but if you see the example, that's how it's shown:

'v0:1531420618:token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c'

You see command and response_url are parsed.

I managed to get this working in Python. I see you ask in Typescript, but I hope this python script helps:

@app.route('/slack-validation', methods=['GET', 'POST']) 
def slack_secutiry():
    headers = request.headers
    timestamp = request.headers['X-Slack-Request-Timestamp'] 

    slack_payload = request.form
    dict_slack = slack_payload.to_dict()

### This is the key that solved the issue for me, where urllib.parse.quote(val, safe='')] ###
    payload= "&".join(['='.join([key, urllib.parse.quote(val, safe='')]) for key, val in dict_slack.items()])  

    ### compose the message:
    sig_basestring = 'v0:' + timestamp + ':' + payload

    sig_basestring = sig_basestring.encode('utf-8')

    ### secret
    signing_secret = slack_signing_secret.encode('utf-8') # I had an env variable declared with slack_signing_secret
    
    my_signature = 'v0=' + hmac.new(signing_secret, sig_basestring, hashlib.sha256).hexdigest()
    print('my signature: ')
    print(my_signature)
    
    return '', 200

Solution 2:[2]

It might be useful for you to check how the request validation feature is implemented in the Bolt framework:

https://github.com/slackapi/bolt-python/blob/4e0709f0578080833f9aeab984a778be81a30178/slack_bolt/middleware/request_verification/request_verification.py

Note that it is implemented as a middleware, enabled by default when you instantiate the app (see attribute request_verification_enabled).

You can inspect this behaviour and/or change it if you want to validate the requests manually:

app = App(
    token=SLACK_BOT_TOKEN,
    signing_secret=SLACK_SIGNING_SECRET,
    request_verification_enabled=False
)

Solution 3:[3]

The following solution solves the problem of verification of signing secret of slack

#!/usr/bin/env python3
import hashlib
import hmac
import base64


def verify_slack_request(event: dict, slack_signing_secret: str) -> bool:
    """Verify slack requests.
    Borrowed from https://janikarhunen.fi/verify-slack-requests-in-aws-lambda-and-python.html
    - Removed optional args
    - Checks isBase64Encoded
    :param event: standard event handler
    :param slack_signing_secret: slack secret for the slash command
    :return: True if verification worked
    """
    slack_signature = event['headers']['x-slack-signature']
    slack_time = event['headers']['x-slack-request-timestamp']
    body = event['body']
    if event['isBase64Encoded']:
        body = base64.b64decode(body).decode("utf-8")

    """ Form the basestring as stated in the Slack API docs. We need to make a bytestring"""
    base_string = f'v0:{slack_time}:{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, base_string, 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).'''
    result = hmac.compare_digest(my_signature, slack_signature)
    if not result:
        logger.error('Verification failed. my_signature: ')
        logger.error(f'{my_signature} != {slack_signature}')

    return result


if __name__ == '__main__':
    # add correct params here
    print(verify_slack_request({}, None))

Borrowed From: https://gist.github.com/nitrocode/288bb104893698011720d108e9841b1f

Credits: https://gist.github.com/nitrocode

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 nahusznaj
Solution 2 Dharman
Solution 3 Akash Kumar Singhal