4
votes

I'm getting a POST request inside a Flask app from Slack. The request is sent when a user presses on an interactive message button. According to Slack docs I must extract the body of the request to verify the signature. My computed signature doesn't match the one sent by Slack, though. In fact, the body of the request comes as some encoded string. The string is actually an encoded dictionary instead of a query str parameters, as expected.

Here's the beginning of my view:

@app.route('/register', methods=['POST'])
def register_visit():
    data = request.get_data()
    signature = request.headers.get('X-Slack-Signature', None)
    timestamp = request.headers.get('X-Slack-Request-Timestamp', None)
    signing_secret = b'aaaaaaaaaaaaaaaa'


    # old message, ignore
    if round(actual_time.time() - float(timestamp)) > 60 * 5:
        return
    concatenated = ("v0:%s:%s" % (timestamp, data)).encode('utf-8')
    computed_signature = 'v0=' + hmac.new(signing_secret, msg=concatenated, digestmod=hashlib.sha256).hexdigest()
    if hmac.compare_digest(computed_signature, signature):
        ...

I've tried to format the received data to make it look like:

token=fdjkgjl&user_id=1234... but I am not aware of all of the necessary parameters that have to be present in the data.

Any ideas are highly appreciated.

The body of the message is following - after being URL decoded (note I've modified possibly sensitive data):

b'payload={"type":"interactive_message","actions": [{"name":"yes_button","type":"button","value":"236"}],"callback_id":"visit_button","team":{"id":"fffff","domain":"ffff"},"channel":{"id":"ffff","name":"directmessage"},"user":{"id":"ffffff","name":"fffft"},"action_ts":"1540403943.419120","message_ts":"1541403949.000100","attachment_id":"1","token":"8LpjBuv13J7xAjhl2lEajoBU","is_app_unfurl":false,"original_message":{"text":"Test","bot_id":"DDDDDDDDD","attachments":[{"callback_id":"visit_button","text":"Register","id":1,"color":"3AA3E3","actions":[{"id":"1","name":"yes_button","text":"Yes","type":"button","value":"236","style":""}],"fallback":"Register"}],"type":"message","subtype":"bot_message","ts":"1540413949.000100"},"response_url":"https://hooks.slack.com/actions/ffffff/ffffff/tXJjx1XInaUhrikj6oEzK08e","trigger_id":"464662548327.425084163429.dda35a299eedb940ab98dbb9386b56f0"}'

2
Have a look at the official slack API client for python: github.com/slackapi/python-slackclient. - Fine
Looks to me a bit that you are confusing header and body of the request. The header will contain the Signature, and the body will contain everything else from the request (e.g. user ID). Note that the body is a POST form with the single property payload. That property contains the request data as JSON array. see here - Erik Kalkoken
@ErikKalkoken Where exactly do you see that I am confusing them? The body does contain the payload. That's exactly what I am getting. But check this out, the body comes as something like this: b'payload=%7B%22type%22%3A%22interactive_message%22%2C%22actions%22%3A%5B%7B%22name%22%3A%22kaynti...' - elena
@Fian Well, the functionality I'm trying to achieve shouldn't require any libraries. It's just a regular POST request, which I should be able to handle with Python/Flask built-in functionality. Have you used the library for interactive messages? - elena
maybe I misunderstood then. apologizes. your body is URL encoded. I am not super familiar with Python / Flask, but you should get it in plain text with request.form.get('payload'). Source - Erik Kalkoken

2 Answers

4
votes

The reason you are getting the "garbled" data is that you are using request.get_data(). That method will return the raw data of a request, but not do any decoding for you.

Much more convenient is to use request.form.get('payload'), which will directly give you the JSON string of the request object. You can then convert that into a dict object with json.loads() to process it further in your app.

Note that the format you received is the correct format for interactive messages. You will not get a query string (e.g. "token=abc;user_id?def...") as you suggested (like for slash command requests). Interactive message request will always contain the request as JSON string in a payload form property. See here for reference.

Here is a simple working example, which will reply a greeting to the user that pressed the button. It will work directly with Slack, but I recommend using Postman to test it.

#app.py

from flask import Flask, request #import main Flask class and request object
import json

app = Flask(__name__) #create the Flask app

@app.route('/register', methods=['POST'])
def register_visit():
    slack_req = json.loads(request.form.get('payload'))
    response = '{"text": "Hi, <@' + slack_req["user"]["id"] + '>"}'
    return response, 200, {'content-type': 'application/json'}

if __name__ == '__main__':
    app.run(debug=True, port=5000) #run app in debug mode on port 5000
3
votes

OK, the issue wasn't related to how Slack sends me the message. It was about misunderstanding which data comes as bytes and which data is unicode. The culprit was string formatting in my case - the line concatenated = ("v0:%s:%s" % (timestamp, data)).encode('utf-8') should have been concatenated = (b"v0:%b:%b" % (timestamp.encode("utf-8"), data)). Data is already bytes, timestamp meanwhile is unicode. Cannot believe I've banged my head on this for hours -_-

@app.route('/register', methods=['POST'])
def register_visit():
    data = request.get_data()
    signature = request.headers.get('X-Slack-Signature', None)
    timestamp = request.headers.get('X-Slack-Request-Timestamp', None)
    signing_secret = b'aaaaaaaaaaaaaaaa'
    # old message, ignore
    if round(actual_time.time() - float(timestamp)) > 60 * 5:
        return
    concatenated = (b"v0:%b:%b" % (timestamp.encode("utf-8"), data))
    computed_signature = 'v0=' + hmac.new(signing_secret, msg=concatenated, 
    digestmod=hashlib.sha256).hexdigest()
    if hmac.compare_digest(computed_signature, signature):
        ...