Skip to content

Commit

Permalink
Support anonymous users in verify_password callback
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Mar 2, 2015
1 parent fc34cc5 commit 5c5396b
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 12 deletions.
6 changes: 4 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,13 @@ API Documentation
return False
return passlib.hash.sha256_crypt.verify(password, user.password_hash)

Note that when a `verify_password` callback is provided the `get_password` and `hash_password` callbacks are not used.
If this callback is defined, it is also invoked when the request does not have the ``Authorization`` header with user credentials, and in this case both the ``username`` and ``password`` arguments are set to empty strings. The client can opt to return ``True`` and that will allow anonymous users access to the route. The callback function can indicate that the user is anonymous by writing a state variable to ``flask.g``, which the route can then check to generate an appropriate response.

Note that when a ``verify_password`` callback is provided the ``get_password`` and ``hash_password`` callbacks are not used.

.. method:: error_handler(error_callback)

If defined, this callback function will be called by the framework when it is necessary to send an authentication error back to the client. The return value from this function can be the body of the response as a string or it can also be a response object created with `make_response`. If this callback isn't provided a default error response is generated. Example::
If defined, this callback function will be called by the framework when it is necessary to send an authentication error back to the client. The return value from this function can be the body of the response as a string or it can also be a response object created with ``make_response``. If this callback isn't provided a default error response is generated. Example::
@auth.error_handler
def auth_error():
Expand Down
24 changes: 17 additions & 7 deletions flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,18 @@ def decorated(*args, **kwargs):
# 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 not auth:
return self.auth_error_callback()
password = self.get_password_callback(auth.username)
if auth:
password = self.get_password_callback(auth.username)
else:
password = None
if not self.authenticate(auth, password):
return self.auth_error_callback()
return f(*args, **kwargs)
return decorated

def username(self):
if not request.authorization:
return ""
return request.authorization.username


Expand All @@ -82,14 +85,21 @@ def authenticate_header(self):
return 'Basic realm="{0}"'.format(self.realm)

def authenticate(self, auth, stored_password):
client_password = auth.password
if auth:
username = auth.username
client_password = auth.password
else:
username = ""
client_password = ""
if self.verify_password_callback:
return self.verify_password_callback(auth.username, client_password)
return self.verify_password_callback(username, client_password)
if not auth:
return False
if self.hash_password_callback:
try:
client_password = self.hash_password_callback(client_password)
except TypeError:
client_password = self.hash_password_callback(auth.username,
client_password = self.hash_password_callback(username,
client_password)
return client_password == stored_password

Expand All @@ -113,7 +123,7 @@ def authenticate_header(self):
self.realm, session["auth_nonce"], session["auth_opaque"])

def authenticate(self, auth, password):
if not auth.username or not auth.realm or not auth.uri \
if not auth or not auth.username or not auth.realm or not auth.uri \
or not auth.nonce or not auth.response or not password:
return False
if auth.nonce != session.get("auth_nonce") or \
Expand Down
16 changes: 13 additions & 3 deletions test_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import base64
import re
from hashlib import md5 as basic_md5
from flask import Flask
from flask import Flask, g
from flask.ext.httpauth import HTTPBasicAuth, HTTPDigestAuth
from werkzeug.http import parse_dict_header

Expand Down Expand Up @@ -68,10 +68,14 @@ def basic_custom_auth_hash_password(password):

@basic_verify_auth.verify_password
def basic_verify_auth_verify_password(username, password):
g.anon = False
if username == 'john':
return password == 'hello'
elif username == 'susan':
return password == 'bye'
elif username == '':
g.anon = True
return True
return False

@digest_auth.get_password
Expand Down Expand Up @@ -114,7 +118,8 @@ def basic_custom_auth_route():
@app.route('/basic-verify')
@basic_verify_auth.login_required
def basic_verify_auth_route():
return 'basic_verify_auth:' + basic_verify_auth.username()
return 'basic_verify_auth:' + basic_verify_auth.username() + \
' anon:' + str(g.anon)

@app.route('/digest')
@digest_auth.login_required
Expand Down Expand Up @@ -204,7 +209,12 @@ def test_verify_auth_login_valid(self):
creds = base64.b64encode(b'susan:bye').decode('utf-8')
response = self.client.get(
'/basic-verify', headers={'Authorization': 'Basic ' + creds})
self.assertEqual(response.data, b'basic_verify_auth:susan')
self.assertEqual(response.data, b'basic_verify_auth:susan anon:False')

def test_verify_auth_login_empty(self):
creds = base64.b64encode(b'susan:bye').decode('utf-8')
response = self.client.get('/basic-verify')
self.assertEqual(response.data, b'basic_verify_auth: anon:True')

def test_verify_auth_login_invalid(self):
creds = base64.b64encode(b'john:bye').decode('utf-8')
Expand Down

0 comments on commit 5c5396b

Please sign in to comment.