Verifying HMAC from Microsoft Teams bot in Python Flask - python

I am trying to build a Microsoft Teams chat bot using Flask, following the instructions on how to build custom bots. However I am unable to verify the HMAC auth which I really want for security.
Based on guides and documentation I've found I am using the following minimial testing app trying to calculate a HMAC for the incoming request. (Bot name and description DevBot and the key/security_token below for testing).
#!/usr/bin/python
# coding=utf-8
from flask import Flask, request, jsonify
import hmac, hashlib, base64, json
app = Flask(__name__)
#app.route('/', methods=['GET', 'POST'])
def webhook():
if request.method == 'POST':
# Authenticate
security_token = b"O5XHU8OSzwx8w9YiM0URkR/Ij4TZZiZUwz7Swc+1hZE="
request_data = request.get_data()
digest = hmac.new(security_token, msg=request_data, digestmod=hashlib.sha256).digest()
signature = base64.b64encode(digest).decode()
# TODO: Verify signature = Authorization header HMAC here
return jsonify({
'type' : 'message',
'text' : "Auth header: {0} <br>Calculated HMAC: {1}".format(request.headers.get('Authorization'), signature),
})
elif request.method == 'GET':
return "Hello World"
if __name__ == '__main__':
app.run(debug=True)
Upon sending the message #DevBot test I get the following hashes back in the reply from the bot, but they aren't matching as expected:
Auth header: HMAC LuDmz97y/Z2KWLIZ1WZASz3HlOEtDCwk5/lL/fK8GqM=
Calculated HMAC: eaxTdJSLuU3Z4l94bxFiWvsBhjNG9SPxwq/UHeR7KcA=
Any ideas or pointers? I've been trying all sorts of stuff with encoding but I have a feeling that Flask might be doing something that modifies the request body or something?
edit 1: small clarification
edit 2: full Flask app example
edit 3: sample bot details, input and output examples

After lots of trial and error and trying to reproduce the C# code example from MS I managed to solve it myself. Here's the solution:
#!/usr/bin/python
# coding=utf-8
from flask import Flask, request, jsonify
import hmac, hashlib, base64, json
app = Flask(__name__)
#app.route('/', methods=['GET', 'POST'])
def webhook():
if request.method == 'POST':
# Reply
data = request.get_json()
channel = data['channelId']
message_type = data['type']
sender = data['from']['name']
message_format = data['textFormat']
message = data['text']
# Authenticate
security_token = b"O5XHU8OSzwx8w9YiM0URkR/Ij4TZZiZUwz7Swc+1hZE="
request_data = request.get_data()
digest = hmac.new(base64.b64decode(security_token), msg=request_data, digestmod=hashlib.sha256).digest()
signature = base64.b64encode(digest).decode()
# TODO: verify that HMAC header == signature
return jsonify({
'type' : 'message',
'text' : "auth header: {0} <br>hmac: {1}".format(request.headers.get('Authorization').split(' ')[1], signature),
})
elif request.method == 'GET':
return "Hello World"
if __name__ == '__main__':
app.run(debug=True)

Another option rather than interfacing directly with Microsoft Teams may be to use the Microsoft Bot Connector API.
https://docs.botframework.com/en-us/restapi/connector/
I have a bot working with Microsoft Teams using https://github.com/Grungnie/microsoftbotframework which is validating the JWT that is sent from Microsoft.

Related

Access Token From Spotify API is the Same One For Each User

So during testing, I was able to sign in with 1 spotify account at the login page and receive a correct access code and token. However, when I run and close the app, and run it again, I went on an incognito tab (with no cookies) and was able to successfully sign into a new Spotify account through the same login page (which sends you to authenticate through spotify). But, for some reason, the spotify API sent me the same access code and token as the first user.
Thanks for your help!
import spotipy
from spotipy.oauth2 import SpotifyOAuth
from flask import Flask, request, url_for, session, redirect, render_template
import time
from os import urandom
# App Initialization
app = Flask(__name__)
# Setup Cookies
app.secret_key = urandom(64)
app.config['SESSION_COOKIE_NAME'] = 'A Session Cookie'
TOKEN_INFO = "token_info"
#app.route('/login')
def login():
sp_oauth = create_spotify_oauth()
auth_url = sp_oauth.get_authorize_url()
return redirect(auth_url)
#app.route('/redirect')
def redirectPage():
sp_oauth = create_spotify_oauth()
session.clear()
code = request.args.get('code')
token_info = sp_oauth.get_access_token(code)
# print(token_info)
session[TOKEN_INFO] = token_info
return redirect(url_for('profile', _external=True))
# Universal Functions
def create_spotify_oauth():
return SpotifyOAuth(
client_id = 'INSERT CLIENT ID',
client_secret = 'INSERT CLIENT SECRET',
redirect_uri=url_for('redirectPage', _external=True),
scope='user-library-read user-read-recently-played user-read-playback-position playlist-read-collaborative user-read-playback-state user-top-read playlist-modify-public user-read-currently-playing user-library-read playlist-read-private playlist-modify-private',
)
def get_token():
token_info = session.get(TOKEN_INFO, None)
if not token_info:
raise "exception"
now = int(time.time())
is_expired = token_info['expires_at'] - now < 60
if is_expired:
sp_oauth = create_spotify_oauth()
token_info = sp_oauth.refresh_access_token(token_info['refresh_token'])
return token_info
app.run()
Essentially, the function "get_access_token()" returns an access token if it can find any on your system through environment variables, so this isn't even an issue for my application. It's working as intended.

Shopify Webhooks Hmac Python verification fails

I am trying to verify the webhook received from Shopify but the Hmac verification fails.
def verify_webhook(data, hmac_header):
digest = hmac.new(SECRET.encode('utf-8'), data, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))
#app.route('/productCreation', methods=['POST'])
def productCreation():
data = request.data
verified = verify_webhook(
data, request.headers.get('X-Shopify-Hmac-SHA256'))
if(verified):
return ("Success", 200)
return("Integrity error", 401)
Getting error as
hash = hmac.new(SECRET.encode('utf-8'), body.encode('utf-8'), hashlib.sha256)
AttributeError: 'bytes' object has no attribute 'encode'
Can anyone help with this? I am developing a Flask app for this.
You´re getting a attribute error, means your body object is not a string (encode() is working for string like objects) as mentioned in the error message it is a 'bytes' like object. Remove the
.encode('utf-8') from body.
Update March 2022
It seems like Shopify has updated their Flask example with the same changes I suggested below.
Original answer
I got the same error when using Shopify's Flask example
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
SECRET = 'hush'
def verify_webhook(data, hmac_header):
digest = hmac.new(SECRET, data.encode('utf-8'), hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))
#app.route('/webhook', methods=['POST'])
def handle_webhook():
data = request.get_data()
verified = verify_webhook(data, request.headers.get('X-Shopify-Hmac-SHA256'))
if not verified:
abort(401)
# process webhook payload
# ...
return ('', 200)
To get it to work I had to modify verify_webhook by:
Encoding SECRET since hmac.new() expects the key in bytes and not as a string.
Not encoding data since Flask's response.get_data already returns an encoded bytestring.
The final code
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
SECRET = 'hush'
def verify_webhook(data, hmac_header):
digest = hmac.new(SECRET.encode('utf-8'), data, hashlib.sha256).digest()
computed_hmac = base64.b64encode(digest)
return hmac.compare_digest(computed_hmac, hmac_header.encode('utf-8'))
#app.route('/webhook', methods=['POST'])
def handle_webhook():
data = request.get_data()
verified = verify_webhook(data, request.headers.get('X-Shopify-Hmac-SHA256'))
if not verified:
abort(401)
# process webhook payload
# ...
return ('', 200)

Shopify Webhook HMAC Validation With Flask

I'm trying to verify that the Webhook received is coming from Shopify. They have this doc, but it doesn't work (getting type errors).
Here's what I have so far. It produces no errors, but the verify_webhook function always returns false.
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
SECRET = '...'
def verify_webhook(data, hmac_header):
digest = hmac.new(SECRET.encode('utf-8'), data, hashlib.sha256).digest()
genHmac = base64.b64encode(digest)
return hmac.compare_digest(genHmac, hmac_header.encode('utf-8'))
#app.route('/', methods=['POST'])
def hello_world(request):
print('Received Webhook...')
data = request.get_data()
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
verified = verify_webhook(data, hmac_header)
if not verified:
return 'Integrity of request compromised...', 401
print('Verified request...')
if __name__ == '__main__':
app.run()
What am I doing wrong?
Answer:
from flask import Flask, request, abort
import hmac
import hashlib
import base64
app = Flask(__name__)
SECRET = '...'
def verify_webhook(data, hmac_header):
digest = hmac.new(SECRET.encode('utf-8'), data, hashlib.sha256).digest()
genHmac = base64.b64encode(digest)
return hmac.compare_digest(genHmac, hmac_header.encode('utf-8'))
#app.route('/', methods=['POST'])
def hello_world(request):
print('Received Webhook...')
data = request.data # NOT request.get_data() !!!!!
hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')
verified = verify_webhook(data, hmac_header)
if not verified:
return 'Integrity of request compromised...', 401
print('Verified request...')
if __name__ == '__main__':
app.run()
Issue was in the data = request.get_data() line.

Python Flask: Send file and variable

I have two servers where one is trying to get a file from the other. I am using Flask get requests to send simple data back and forth (strings, lists, JSON objects, etc.).
I also know how to send just a file, but I need to send an error code with my data.
I'm using something along the following lines:
Server 1:
req = requests.post('https://www.otherserver.com/_download_file', data = {'filename':filename})
Server 2:
#app.route('/_download_file', methods = ['POST'])
def download_file():
filename = requests.form.get('filename')
file_data = codecs.open(filename, 'rb').read()
return file_data
Server 1:
with codecs.open('new_file.xyz', 'w') as f:
f.write(req.content)
...all of which works fine. However, I want to send an error code variable along with file_data so that Server 1 knows the status (and not the HTTP status, but an internal status code).
Any help is appreciated.
One solution that comes to my mind is to use a custom HTTP header.
Here is an example server and client implementation.
Of course, you are free to change the name and the value of the custom header as you need.
server
from flask import Flask, send_from_directory
app = Flask(__name__)
#app.route('/', methods=['POST'])
def index():
response = send_from_directory(directory='your-directory', filename='your-file-name')
response.headers['my-custom-header'] = 'my-custom-status-0'
return response
if __name__ == '__main__':
app.run(debug=True)
client
import requests
r = requests.post(url)
status = r.headers['my-custom-header']
# do what you want with status
UPDATE
Here is another version of the server based on your implementation
import codecs
from flask import Flask, request, make_response
app = Flask(__name__)
#app.route('/', methods=['POST'])
def index():
filename = request.form.get('filename')
file_data = codecs.open(filename, 'rb').read()
response = make_response()
response.headers['my-custom-header'] = 'my-custom-status-0'
response.data = file_data
return response
if __name__ == '__main__':
app.run(debug=True)

Force Content-Type or expose request.data in Flask for known content-type

I am recreating a service in Python/Flask and am running into an issue with the way the existing clients authenticate. I have to match the existing clients scheme for compatibility reasons.
The existing clients take the username, password and base64 encode it. This is not HTTP Basic Authentication, despite sounding similar. Below is some sample code that would create this login request.
credentials = {
'username': 'test#example.com',
'password': 'password'
}
data = b64encode(urlencode(credentials))
request = urllib2.Request(loginURL)
request.add_data(data)
# request.add_header('Content-Type', 'application/gooblygop')
# 'application/x-www-form-urlencoded' seems to be a default Content-Type
login = urllib2.urlopen(request)
On the server side, I take the POST data and base64 decode it to get the username and password information again.
flask server:
#app.route('/login', methods=['POST'])
def login():
error = None
if request.method == 'POST':
# post data: cGFzc3dvcmQ9ZGVmYXVsdCZlbWFpbD10ZXN0JTQwZXhhbXBsZS5jb20=
data = b64decode(request.data)
# decoded data: password=default&email=test%40example.com
return('ok')
The problem is the Content Type. If I specify an unknown Content-Type in the client (application/gooblygop), Flask exposes the POST data to request.data and I can decode the base64 string. If I leave the Content-Type as default (application/x-www-form-urlencoded), the raw data is not exposed to request.data and I don't know how to retrieve the base64 encoded string and make use of it.
The existing client software all pretty much defaults to x-www-form-urlencoded, but I can't rely on that always being the case.
Essentially, I need a reliable, server-side method for accessing that encoded string no matter what Content-Type the client program states.
Other notes: I am very new to Python, coming from a PHP background. So I am very open to suggestions. Also, this project is primarily for personal use.
You want to look at the request.form object when dealing with urlencoded posts with normal mimetypes. In this case you have an unusual form, but here is a way to do it:
# mkreq.py
from urllib import urlencode
import urllib2
from base64 import b64encode
credentials = {
'username': 'test#example.com',
'password': 'password'
}
data = b64encode(urlencode(credentials))
request = urllib2.Request("http://localhost:5000/login")
request.add_data(data)
request.add_header('Content-Type', 'application/gooblygop')
# 'application/x-www-form-urlencoded' seems to be a default Content-Type
login1 = urllib2.urlopen(request).read()
print(login1)
request2 = urllib2.Request("http://localhost:5000/login")
request2.add_data(data)
login2 = urllib2.urlopen(request2).read()
print(login2)
You probably want to modify the login bit to check the mimetype, here is a version with minimal changes to your current setup:
#app.route('/login', methods=['POST'])
def login():
error = None
if request.method == 'POST':
# post data: cGFzc3dvcmQ9ZGVmYXVsdCZlbWFpbD10ZXN0JTQwZXhhbXBsZS5jb20=
data = b64decode(request.data)
# decoded data: password=default&email=test%40example.com
if not data:
data = b64decode(request.form.keys()[0])
special_mimetype = request.mimetype
return(special_mimetype + '\n' + data)
This is the output of the first code sample, with two requests:
bvm$ python mkreq.py
application/gooblygop
username=test%40example.com&password=password
application/x-www-form-urlencoded
username=test%40example.com&password=password
Have you thought about using json to pass your data in the POST? Flask has built in support for passing json data. In addition, if you set the Content-Type in the headers to application/json then flask will automatically dejson the POST data for you and put it in request.json
Here is the requesting application
import urllib2
import json
if __name__ == "__main__":
headers = {'Content-Type':'application/json'}
post_data = {"user":"test_user"}
print "Posting request"
req = urllib2.Request("http://localhost:5000/login", json.dumps(post_data), headers)
resp = urllib2.urlopen(req)
print "Response was %s" % resp.read()
This is the Flask view
from flask import request
#app.route('/login', methods=['POST'])
def login():
user = request.json['user']
return user
I suggest you test using curl as well if you are using the linux terminal. Here is an example
curl -X POST -H "Content-Type:application/json" -s -d '{"user":"This is the username"}' 'localhost:5000/login'
This is the username

Categories

Resources