Skip to content

Commit

Permalink
Support custom authentication scheme and realm
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Sep 19, 2015
1 parent 4f41d3d commit bf12f95
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 32 deletions.
18 changes: 14 additions & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ The following example is similar to the previous one, but HTTP Digest authentica
Security Concerns with Digest Authentication
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The digest authentication algorightm requires a *challenge* to be sent to the client for use in encrypting the password for transmission. This challenge needs to be used again when the password is decoded at the server, so the challenge information needs to be stored so that it can be recalled later.
The digest authentication algorihtm requires a *challenge* to be sent to the client for use in encrypting the password for transmission. This challenge needs to be used again when the password is decoded at the server, so the challenge information needs to be stored so that it can be recalled later.

By default, Flask-HTTPAuth stores the challenge data in the Flask session. To make the authentication flow secure when using session storage, it is required that server-side sessions are used instead of the default Flask cookie based sessions, as this ensures that the challenge data is not at risk of being captured as it moves in a cookie between server and client. The Flask-Session and Flask-KVSession extensions are both very good options to implement server-side sessions.

Expand Down Expand Up @@ -138,10 +138,14 @@ API Documentation

This class that handles HTTP Basic authentication for Flask routes.

.. method:: __init__()
.. method:: __init__(scheme=None, realm=None)

Create a basic authentication object.

If the optional ``scheme`` argument is provided, it will be used instead of the standard "Basic" scheme in the ``WWW-Authenticate`` response. A fairly common practice is to use a custom scheme to prevent browsers from prompting the user to login.

The ``realm`` argument can be used to provide an application defined realm with the ``WWW-Authenticate`` header.

.. method:: get_password(password_callback)

This callback function will be called by the framework to obtain the password for a given user. Example::
Expand Down Expand Up @@ -210,9 +214,15 @@ API Documentation

This class that handles HTTP Digest authentication for Flask routes. The ``SECRET_KEY`` configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies. To support clients that are not web browsers or that cannot handle cookies a `session interface <http://flask.pocoo.org/docs/api/#flask.Flask.session_interface>`_ that writes sessions in the server must be used.

.. method:: __init__(self, use_ha1_pw=False)
.. method:: __init__(self, scheme=None, realm=None, use_ha1_pw=False)

Create a digest authentication object.

If the optional ``scheme`` argument is provided, it will be used instead of the "Digest" scheme in the ``WWW-Authenticate`` response. A fairly common practice is to use a custom scheme to prevent browsers from prompting the user to login.

The ``realm`` argument can be used to provide an application defined realm with the ``WWW-Authenticate`` header.

Create a digest authentication object. If ``use_ha1_pw`` is False, then the ``get_password`` callback needs to return the plain text password for the given user. If ``use_ha1_pw`` is True, the ``get_password`` callback needs to return the HA1 value for the given user. The advantage of setting ``use_ha1_pw`` to ``True`` is that it allows the application to store the HA1 hash of the password in the user database.
If ``use_ha1_pw`` is False, then the ``get_password`` callback needs to return the plain text password for the given user. If ``use_ha1_pw`` is True, the ``get_password`` callback needs to return the HA1 value for the given user. The advantage of setting ``use_ha1_pw`` to ``True`` is that it allows the application to store the HA1 hash of the password in the user database.

.. method:: generate_ha1(username, password)

Expand Down
22 changes: 12 additions & 10 deletions flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@


class HTTPAuth(object):
def __init__(self):
def __init__(self, scheme=None, realm=None):
def default_get_password(username):
return None

def default_auth_error():
return "Unauthorized Access"

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

Expand Down Expand Up @@ -68,8 +69,8 @@ def username(self):


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

Expand All @@ -82,7 +83,7 @@ def verify_password(self, f):
return f

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

def authenticate(self, auth, stored_password):
if auth:
Expand All @@ -105,8 +106,8 @@ def authenticate(self, auth, stored_password):


class HTTPDigestAuth(HTTPAuth):
def __init__(self, use_ha1_pw = False):
super(HTTPDigestAuth, self).__init__()
def __init__(self, scheme=None, realm=None, use_ha1_pw=False):
super(HTTPDigestAuth, self).__init__(scheme, realm)
self.use_ha1_pw = use_ha1_pw
self.random = SystemRandom()
try:
Expand All @@ -130,7 +131,7 @@ def default_generate_opaque():

def default_verify_opaque(opaque):
return opaque == session.get("auth_opaque")

self.generate_nonce(default_generate_nonce)
self.generate_opaque(default_generate_opaque)
self.verify_nonce(default_verify_nonce)
Expand Down Expand Up @@ -166,8 +167,9 @@ def generate_ha1(self, username, password):
def authenticate_header(self):
session["auth_nonce"] = self.get_nonce()
session["auth_opaque"] = self.get_opaque()
return 'Digest realm="{0}",nonce="{1}",opaque="{2}"'.format(
self.realm, session["auth_nonce"], session["auth_opaque"])
return '{0} realm="{1}",nonce="{2}",opaque="{3}"'.format(
self.scheme or 'Digest', self.realm, session["auth_nonce"],
session["auth_opaque"])

def authenticate(self, auth, stored_password_or_ha1):
if not auth or not auth.username or not auth.realm or not auth.uri \
Expand Down
41 changes: 23 additions & 18 deletions test_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,26 @@ def md5(str):
str = str.encode('utf-8')
return basic_md5(str)


def get_ha1(user, pw, realm):
a1 = user + ":" + realm + ":" + pw
return md5(a1).hexdigest()


class HTTPAuthTestCase(unittest.TestCase):
def setUp(self):
app = Flask(__name__)
app.config['SECRET_KEY'] = 'my secret'

basic_auth = HTTPBasicAuth()
basic_auth_my_realm = HTTPBasicAuth()
basic_auth_my_realm.realm = 'My Realm'
basic_auth_my_realm = HTTPBasicAuth(scheme='CustomBasic',
realm='My Realm')
basic_custom_auth = HTTPBasicAuth()
basic_verify_auth = HTTPBasicAuth()
digest_auth = HTTPDigestAuth()
digest_auth_my_realm = HTTPDigestAuth()
digest_auth_my_realm.realm = 'My Realm'
digest_auth_ha1_pw = HTTPDigestAuth(use_ha1_pw = True)
digest_auth_my_realm = HTTPDigestAuth(scheme='CustomDigest',
realm='My Realm')
digest_auth_ha1_pw = HTTPDigestAuth(use_ha1_pw=True)

@digest_auth_ha1_pw.get_password
def get_digest_password(username):
Expand Down Expand Up @@ -92,7 +94,7 @@ def basic_verify_auth_verify_password(username, password):
return False

@digest_auth.get_password
def get_digest_password(username):
def get_digest_password_2(username):
if username == 'susan':
return 'hello'
elif username == 'john':
Expand All @@ -101,7 +103,7 @@ def get_digest_password(username):
return None

@digest_auth_my_realm.get_password
def get_digest_password_2(username):
def get_digest_password_3(username):
if username == 'susan':
return 'hello'
elif username == 'john':
Expand Down Expand Up @@ -178,7 +180,7 @@ def test_basic_auth_prompt_with_custom_realm(self):
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Basic realm="My Realm"')
'CustomBasic realm="My Realm"')
self.assertEqual(response.data.decode('utf-8'), 'custom error')

def test_basic_auth_login_valid(self):
Expand Down Expand Up @@ -208,7 +210,7 @@ def test_basic_auth_login_invalid(self):
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertEqual(response.headers['WWW-Authenticate'],
'Basic realm="My Realm"')
'CustomBasic realm="My Realm"')

def test_basic_custom_auth_login_valid(self):
creds = base64.b64encode(b'john:hello').decode('utf-8')
Expand All @@ -230,7 +232,6 @@ def test_verify_auth_login_valid(self):
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')

Expand Down Expand Up @@ -258,8 +259,8 @@ def test_digest_auth_prompt_with_custom_realm(self):
response = self.client.get('/digest-with-realm')
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertTrue(re.match(r'^Digest realm="My Realm",nonce="[0-9a-f]+",'
r'opaque="[0-9a-f]+"$',
self.assertTrue(re.match(r'^CustomDigest realm="My Realm",'
'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
response.headers['WWW-Authenticate']))

def test_digest_auth_login_valid(self):
Expand Down Expand Up @@ -303,7 +304,8 @@ def test_digest_ha1_pw_auth_login_valid(self):
response = self.client.get(
'/digest', headers={
'Authorization': 'Digest username="john",realm="{0}",'
'nonce="{1}",uri="/digest_ha1_pw",response="{2}",'
'nonce="{1}",uri="/digest_ha1_pw",'
'response="{2}",'
'opaque="{3}"'.format(d['realm'],
d['nonce'],
auth_response,
Expand Down Expand Up @@ -341,15 +343,16 @@ def test_digest_auth_login_bad_realm(self):
def test_digest_auth_login_invalid(self):
response = self.client.get(
'/digest-with-realm', headers={
"Authorization": 'Digest username="susan",realm="My Realm",'
"Authorization": 'CustomDigest username="susan",'
'realm="My Realm",'
'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",'
'uri="/digest-with-realm",'
'response="ca306c361a9055b968810067a37fb8cb",'
'opaque="5ccc069c403ebaf9f0171e9517f40e41"'})
self.assertEqual(response.status_code, 401)
self.assertTrue('WWW-Authenticate' in response.headers)
self.assertTrue(re.match(r'^Digest realm="My Realm",nonce="[0-9a-f]+",'
r'opaque="[0-9a-f]+"$',
self.assertTrue(re.match(r'^CustomDigest realm="My Realm",'
r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
response.headers['WWW-Authenticate']))

def test_digest_auth_login_invalid2(self):
Expand Down Expand Up @@ -395,12 +398,14 @@ def opaquemaker():
return 'some opaque'

verify_nonce_called = []

@self.digest_auth.verify_nonce
def verify_nonce(provided_nonce):
verify_nonce_called.append(provided_nonce)
return True

verify_opaque_called = []

@self.digest_auth.verify_opaque
def verify_opaque(provided_opaque):
verify_opaque_called.append(provided_opaque)
Expand Down Expand Up @@ -432,9 +437,9 @@ def verify_opaque(provided_opaque):
d['opaque'])})
self.assertEqual(response.data, b'digest_auth:john')
self.assertEqual(verify_nonce_called, ['not a good nonce'],
"Should have verified the nonce.")
"Should have verified the nonce.")
self.assertEqual(verify_opaque_called, ['some opaque'],
"Should have verified the opaque.")
"Should have verified the opaque.")


def suite():
Expand Down

0 comments on commit bf12f95

Please sign in to comment.