I am trying to implement HMAC-SHA256 authentication into my Python RESTful API project. I am using Python Eve (built on top of Flask), started with an simplified HMAC-SHA1 example.
My application is very simple:
##
# application.py
##
from eve import Eve
from hmac import HMACAuth
SETTINGS = {
'DEBUG': True,
'MONGO_HOST': '127.0.0.1',
'MONGO_PORT': 27017,
'MONGO_DBNAME': 'testing',
'DOMAIN': {'test': {}},
}
app = Eve(auth=HMACAuth, settings=SETTINGS)
if __name__ == '__main__':
app.run(use_reloader=True)
and the HMACAuth
class:
##
# hmac.py
##
import time
import hmac
from eve.auth import HMACAuth
from flask import current_app as app
from hashlib import sha256
class HMACAuth(HMACAuth):
def check_auth(self, userid, hmac_hash, headers, data, allowed_roles, resource, method):
# get user from database
accounts = app.data.driver.db['accounts']
user = accounts.find_one({'userid': userid})
if user:
# user found, we have its secret_key and we can re-create the signature user sent us
check_sig = hmac.new(bytes(user['secret_key'], 'utf8'), b'', sha256)
check_sig.update(bytes(headers['TIMESTAMP'], 'utf-8'))
check_sig.update(data)
check_signature = check_sig.hexdigest()
# try to use python's hmac.compare_digest() to compare user's signature
# and the one we re-created
if hmac.compare_digest(check_signature, hmac_hash):
# signature seems fine, we have to check if the request was sent in past 30 seconds
# we are also checking for negative time because we have a test case with timestamp
# in the future, so time_diff ends up with a negative number
time_diff = int(time.time()) - int(headers['TIMESTAMP'])
if 0 <= time_diff <= 30:
# everything seems superfine!
return True
else:
# time_diff was either negative or more than 30
print('TIME ERROR ({}).'.format(time_diff))
else:
# hmac.compare_digest() failed!
print('WRONG HASH!')
else:
# user doesn't even exist in our db
print('USER DOES NOT EXIST.')
# probably a mongodb related problem, should be properly wrapped in try/except block
print('WAT?')
return False
I tried to write some tests for this app and I ended up with all of them passing:
##
# tests.py
##
import json
import unittest
import arrow
import base64
import hmac
from hashlib import sha256
from application import app
def prepare_test_request(
test_user='testuser',
test_user_secret='xxwMXEqOGiY2TssVZ9hvOB4x6EVW3RW75hjAKEai4UBlxG0ts8Js8dsWOzDvAVq4',
seconds=0
):
timestamp = arrow.utcnow().replace(seconds=+seconds)
payload = {'testing': 'data'}
payload_json = json.dumps(payload)
payload_bytes = bytes(payload_json, 'utf-8')
# is there any reason to send encoded payload to server or is json-formatted string just fine?
payload_encoded = base64.urlsafe_b64encode(payload_bytes) # not used at the moment
sig = hmac.new(bytes(test_user_secret, 'utf-8'), b'', sha256)
sig.update(bytes(str(timestamp.timestamp), 'utf-8'))
sig.update(payload_bytes)
signature = sig.hexdigest()
headers = {
'Authorization': '{}:{}'.format(test_user, signature),
'timestamp': str(timestamp.timestamp),
}
return payload_bytes, headers
class BaseTest(unittest.TestCase):
def setUp(self):
app.config['TESTING'] = True
self.app = app.test_client()
def tearDown(self):
pass
def test_hmac(self):
data, headers = prepare_test_request() # plain request should return 200
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 200
def test_hmac_30(self):
data, headers = prepare_test_request(seconds=-30) # 30 seconds ago should return 200
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 200
def test_hmac_31(self):
data, headers = prepare_test_request(seconds=-31) # 31 seconds ago should return 401
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 401
def test_hmac_future(self):
data, headers = prepare_test_request(seconds=10) # 10 seconds in the future should return 401
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 401
def test_hmac_nonexisting_user(self):
data, headers = prepare_test_request(test_user='nonexisting')
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 401
def test_hmac_wrong_key(self):
data, headers = prepare_test_request(test_user_secret='wrong!')
req = self.app.get('http://127.0.0.1:5000/', data=data, headers=headers)
assert req.status_code == 401
My questions about this are:
- is this a correct way at all to authenticate user with HMAC-SHA256 authentication? I am aware that usually there are
request_method
andrequest_path
sent in headers (and accordingly re-created on server side), but for the sake of simplicity I includedtimestamp
only. - as stated in
tests.py
, is there any need to base64-encodepayload
and send it that way to server? It seems like another layer of "protection", but then again the encoded string can be easily decoded so I don't see the exact purpose (might be result of my limited knowledge about the topic :)) - I tried to test few cases which came to my mind initially, but is there anything else I should test to be sure authenticating really really really works?