Skip to content

Commit

Permalink
token auth support, plus test reorg
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Mar 7, 2016
1 parent a6f50e7 commit aac866d
Show file tree
Hide file tree
Showing 14 changed files with 789 additions and 469 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: python
python:
- "3.5"
- "3.4"
- "3.3"
- "2.7"
Expand Down
78 changes: 59 additions & 19 deletions flask_httpauth.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,22 @@
from hashlib import md5
from random import Random, SystemRandom
from flask import request, make_response, session
from werkzeug.datastructures import Authorization


class HTTPAuth(object):
def __init__(self, scheme=None, realm=None):
self.scheme = scheme
self.realm = realm or "Authentication Required"
self.get_password_callback = None
self.auth_error_callback = None

def default_get_password(username):
return None

def default_auth_error():
return "Unauthorized Access"

self.scheme = scheme
self.realm = realm or "Authentication Required"
self.get_password(default_get_password)
self.error_handler(default_auth_error)

Expand All @@ -35,25 +39,38 @@ def error_handler(self, f):
@wraps(f)
def decorated(*args, **kwargs):
res = f(*args, **kwargs)
if type(res) == str:
res = make_response(res)
res = make_response(res)
if res.status_code == 200:
# if user didn't set status code, use 401
res.status_code = 401
if 'WWW-Authenticate' not in res.headers.keys():
res.headers['WWW-Authenticate'] = self.authenticate_header()
return res
self.auth_error_callback = decorated
return decorated

def authenticate_header(self):
return '{0} realm="{1}"'.format(self.scheme, self.realm)

def login_required(self, f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
# We need to ignore authentication headers for OPTIONS to avoid
# unwanted interactions with CORS.
# Chrome and Firefox issue a preflight OPTIONS request to check
# Access-Control-* headers, and will fail if it returns 401.
if request.method != 'OPTIONS':
if auth:
if auth is None and 'Authorization' in request.headers:
# Flask/Werkzeug do not recognize any authentication types
# other than Basic or Digest, so here we parse the header by
# hand
auth_type, token = request.headers['Authorization'].split(
None, 1)
auth = Authorization(auth_type, {'token': token})
if auth is not None and auth.type.lower() != self.scheme.lower():
return self.auth_error_callback()
# Flask normally handles OPTIONS requests on its own, but in the
# case it is configured to forward those to the application, we
# need to ignore authentication headers and let the request through
# to avoid unwanted interactions with CORS.
if request.method != 'OPTIONS': # pragma: no cover
if auth and auth.username:
password = self.get_password_callback(auth.username)
else:
password = None
Expand All @@ -70,9 +87,10 @@ def username(self):

class HTTPBasicAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None):
super(HTTPBasicAuth, self).__init__(scheme, realm)
self.hash_password(None)
self.verify_password(None)
super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm)

self.hash_password_callback = None
self.verify_password_callback = None

def hash_password(self, f):
self.hash_password_callback = f
Expand All @@ -82,9 +100,6 @@ def verify_password(self, f):
self.verify_password_callback = f
return f

def authenticate_header(self):
return '{0} realm="{1}"'.format(self.scheme or 'Basic', self.realm)

def authenticate(self, auth, stored_password):
if auth:
username = auth.username
Expand All @@ -107,14 +122,19 @@ def authenticate(self, auth, stored_password):

class HTTPDigestAuth(HTTPAuth):
def __init__(self, scheme=None, realm=None, use_ha1_pw=False):
super(HTTPDigestAuth, self).__init__(scheme, realm)
super(HTTPDigestAuth, self).__init__(scheme or 'Digest', realm)
self.use_ha1_pw = use_ha1_pw
self.random = SystemRandom()
try:
self.random.random()
except NotImplementedError:
except NotImplementedError: # pragma: no cover
self.random = Random()

self.generate_nonce_callback = None
self.verify_nonce_callback = None
self.generate_opaque_callback = None
self.verify_opaque_callback = None

def _generate_random():
return md5(str(self.random.random()).encode('utf-8')).hexdigest()

Expand Down Expand Up @@ -168,7 +188,7 @@ def authenticate_header(self):
nonce = self.get_nonce()
opaque = self.get_opaque()
return '{0} realm="{1}",nonce="{2}",opaque="{3}"'.format(
self.scheme or 'Digest', self.realm, nonce,
self.scheme, self.realm, nonce,
opaque)

def authenticate(self, auth, stored_password_or_ha1):
Expand All @@ -190,3 +210,23 @@ def authenticate(self, auth, stored_password_or_ha1):
a3 = ha1 + ":" + auth.nonce + ":" + ha2
response = md5(a3.encode('utf-8')).hexdigest()
return response == auth.response


class HTTPTokenAuth(HTTPAuth):
def __init__(self, scheme='Bearer', realm=None):
super(HTTPTokenAuth, self).__init__(scheme, realm)

self.verify_token_callback = None

def verify_token(self, f):
self.verify_token_callback = f
return f

def authenticate(self, auth, stored_password):
if auth:
token = auth['token']
else:
token = ""
if self.verify_token_callback:
return self.verify_token_callback(token)
return False
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
install_requires=[
'Flask'
],
test_suite="test_httpauth",
test_suite="tests",
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
Expand Down
Loading

0 comments on commit aac866d

Please sign in to comment.