-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Philip Martin
committed
Jun 28, 2014
1 parent
069ec94
commit 9254bcf
Showing
5 changed files
with
491 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -52,3 +52,4 @@ coverage.xml | |
# Sphinx documentation | ||
docs/_build/ | ||
|
||
.idea |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.