Skip to content

Commit

Permalink
initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
Philip Martin committed Jun 28, 2014
1 parent 069ec94 commit 9254bcf
Show file tree
Hide file tree
Showing 5 changed files with 491 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,4 @@ coverage.xml
# Sphinx documentation
docs/_build/

.idea
78 changes: 76 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,76 @@
flask-hmacauth
==============
#Flask-Hmacauth
A module to simplify HMAC-style authentication for RESTful APIs in Flask, which also builds in a simple RBAC concept and anti-replay via a timestamp. For GET requests, the path section and all parameters are hashed. For POST requests, the request body is added as well. By default, the module expects authentication via an X-Auth-Signature header and ACCOUNT_ID and TIMESTAMP parameters (holding the obvious values) to be present in the query string or request body. TIMESTAMP can be in any format datetime.fromtimestamp() can parse. ACCOUNT_ID will be used to lookup a given account's secret and roles via an AccountBroker. All of that can be changed, however.

The concept of an AccountBroker is used to separate this module from any actual user/role management logic. 2 trivial AccountBroker implementations have been provided.

#Example
##Server

from flask import Flask
from flask.ext.hmacauth import hmac_auth, DictAccountBroker, HmacManager

app = Flask(__name__)
accountmgr = DictAccountBroker(
accounts={
"admin": {"secret": ";hi^897t7utf", "rights": ["create", "edit", "delete", "view"]},
"editor": {"secret": "afstr5afewr", "rights": ["create", "edit", "view"]},
"guest": {"secret": "ASDFjoiu%i", "rights": ["view"]}
})
hmacmgr = HmacManager(app, accountmgr)
...
@app.route('/api/v1/create')
@hmac_auth("create")
def create_thing():
...

##Client

import requests
import time

path_and_query = "/api/v1/create?TIMESTAMP="+str(int(time.time()))+"&ACCOUNT_ID=admin&foo=bar"
host = "https://example.com"
sig=hmac.new(";hi^897t7utf", msg=path_and_query).hexdigest()
req = requests.get(host+path_and_query, headers={'X-Auth-Signature': sig})

#AccountBroker
An AccountBroker is an object that intermediates between the HMAC authentication and your user/account store. It does this by exposing the following methods:

* get_secret(account_id) - returns a string secret given an account ID. If the account does not exist, returns None
* has_rights(account_id, rights) - returns True if account_id has all of the rights in the list rights, otherwise returns False. Returns False if the account does not exist.
* is_active(account_id) - returns True if account_id is active (for whatever definition you want to define for active), otherwise returns False.

Flask-Hmacauth ships with 2 trivial AccountBroker implementations, a Dict-based AccountBroker (DictAccountBroker) and a static AccountBroker (StaticAccountBroker).

##DictAccountBroker
Takes a dict of format:

{
"accountID": {
secret: "blahblah",
rights: ["right1", "right2", "right3", ...]
}
...
}

it also exposes the add_accounts and del_accounts methods to modify accounts on the fly.

##StaticAccountBroker
Essentially disables all of the user and role management, and sets a static key for use in HMAC. NOTE, if you use this class you need to pass StaticAccountBroker.GET_ACCOUNT to HmacManager as the account_id parameter OR supply a dummy value for ACCOUNT_ID in the query string

##Write your own
A very common case for larger applications will be user management via a database. In that case, your AuthenticationBroker class just needs to perform the requisite SQL queries to satisfy the the methods above and you're good to go.

#HmacManager
This is the meat of the module. This object contains the is_authorized method, which actually does the HMAC verification and role checks.

In the simple case, you just need to pass this object's constructor the flask application object and an AccountBroker object. In more complex cases, where you want to change defaults, you have the following options:

* app - this is the Flask application container
* account_broker - this is the ApplicationBroker object
* account_id - this is a callable, which when fed a request object will return the request's account ID. The default value for this is lambda x: x.values.get('ACCOUNT_ID')
* signature - this is a callable, which when fed a request object will return the request's signature. The default value for this is GET_SIGNATURE = lambda x: x.headers.get('X-Auth-Signature').
* timestamp - this is a callable, which when fed a request object will return the request's timestamp. The default value for this is lambda x: x.values.get('TIMESTAMP')
* valid_time - number of seconds that a signed request is valid (based on the signed timestamp). defaults to 5
* digest - digest type, defaults to hashlib.sha1

207 changes: 207 additions & 0 deletions flask_hmacauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""
flask.ext.hmacauth
---------------
This module provides HMAC-based authentication and authorization for
Flask. It lets you work with reuests in a database-independent manner.
initiate the HmacManager with a app and set account ID, signature and timestamp
"""

from flask import current_app, request, abort
from functools import update_wrapper
import hmac
import hashlib
import datetime
import urlparse

#simple macros where x is a request object
GET_TIMESTAMP = lambda x: x.values.get('TIMESTAMP')
GET_ACCOUNT = lambda x: x.values.get('ACCOUNT_ID')
GET_SIGNATURE = lambda x: x.headers.get('X-Auth-Signature')


class HmacManager(object):
"""
This object is used to hold the settings for authenticating requests. Instances of
:class:`HmacManager` are not bound to specific apps, so you can create one in the
main body of your code and then bind it to your app in a factory function.
"""
def __init__(self, app, account_broker, account_id=GET_ACCOUNT, signature=GET_SIGNATURE,
timestamp=GET_TIMESTAMP, valid_time=5, digest=hashlib.sha1):
"""
:param app Flask application container
:param account_broker AccountBroker object
:param account_id :type callable that takes a request object and :returns the Account ID (default
ACCOUNT_ID parameter in the query string or POST body)
:param signature :type callable that takes a request object and :returns the signature value (default
X-Auth-Signature header)
:param timestamp :type callable that takes a request object and :returns the timestamp (default
TIMESTAMP parameter in the query string or POST body)
:param valid_time :type integer, number of seconds a timestamp remains valid (default 20)
:param digest hashlib hash :type to be used in the signature (default sha1)
"""

self._account_id = account_id
self._signature = signature
self._timestamp = timestamp
self._account_broker = account_broker
self._valid_time = valid_time
self._digest = digest

app.hmac_manager = self

def is_authorized(self, request_obj, required_rights):

try:
timestamp = self._timestamp(request_obj)
assert timestamp is not None
except:
#TODO: add logging
return False

ts = datetime.datetime.fromtimestamp(float(timestamp))

#is the timestamp valid?
if ts < datetime.datetime.now()-datetime.timedelta(seconds=self._valid_time) \
or ts > datetime.datetime.now():
#TODO: add logging
return False

#do we have an account ID in the request?
try:
account_id = self._account_id(request_obj)
except:
#TODO: add logging
return False

#do we have a secret and rights for this account?
#implicitly, does this account exist?
secret = self._account_broker.get_secret(account_id)
if secret is None:
#TODO: add logging
return False

#Is the account active, valid, etc?
if not self._account_broker.is_active(account_id):
#TODO: add logging
return False

#hash the request URL and Body
hasher = hmac.new(secret, digestmod=self._digest)
#TODO: do we need encode() here?
url = urlparse.urlparse(request.url.encode())
hasher.update(url.path + "?" + url.query)
if request.method == "POST":
hasher.update(request.body)
calculated_hash = hasher.hexdigest()

try:
sent_hash = self._signature(request_obj)
except:
#TODO: add logging
return False

#compare to what we got as the sig
if not calculated_hash == sent_hash:
#TODO: add logging
return False

#ensure this account has the required rights
#TODO: add logging
if required_rights is not None:
if isinstance(required_rights, list):
return self._account_broker.has_rights(account_id, required_rights)
else:
return self._account_broker.has_rights(account_id, [required_rights])

return True


class DictAccountBroker(object):
"""
Default minimal implementation of an AccountBroker. This implementation maintains
a dict in memory with structure:
{
account_id:
{
secret: "some secret string",
rights: ["someright", "someotherright"],
},
...
}
Your implementation can use whatever backing store you like as long as you provide
the following methods:
get_secret(account_id) - returns a string secret given an account ID. If the account does not exist, returns None
has_rights(account_id, rights) - returns True if account_id has all of the rights in the list
rights, otherwise returns False. Returns False if the account does not exist.
is_active(account_id) - returns True if account_id is active (for whatever definition you want
to define for active), otherwise returns False.
"""
def __init__(self, accounts=None):
if accounts is None:
self.accounts = {}
else:
self.accounts = accounts

def add_accounts(self, accounts):
self.accounts.update(accounts)

def del_accounts(self, accounts):
if isinstance(accounts, list):
for i in accounts:
del self.accounts[i]
else:
del self.accounts[accounts]

def get_secret(self, account):
try:
secret = self.accounts[account]["secret"]
except KeyError:
return None
return secret

def has_rights(self, account, rights):
try:
account_rights = self.accounts[account]["rights"]
except KeyError:
return False
if set(rights).issubset(account_rights):
return True
return False

def is_active(self, account):
if account in self.accounts:
return True
return False


class StaticAccountBroker(object):

GET_ACCOUNT = lambda x: "dummy"

def __init__(self, secret=None):
if secret is None:
raise ValueError("you must provide a value for 'secret'")
self._secret = secret

def is_active(self, account):
return True

def get_secret(self, account):
return self._secret

def has_rights(self, account, rights):
return True


def hmac_auth(rights=None):
def decorator(f):
def wrapped_function(*args, **kwargs):
if current_app.hmac_manager.is_authorized(request, rights):
return f(*args, **kwargs)
else:
abort(403)
return update_wrapper(wrapped_function, f)
return decorator
Loading

0 comments on commit 9254bcf

Please sign in to comment.