Code Review Stack Exchange is a question and answer site for peer programmer code reviews. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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 and request_path sent in headers (and accordingly re-created on server side), but for the sake of simplicity I included timestamp only.
  • as stated in tests.py, is there any need to base64-encode payload 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?
share|improve this question

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Browse other questions tagged or ask your own question.