Skip to content

Commit

Permalink
Allow for custom nonce/opaque generation
Browse files Browse the repository at this point in the history
  • Loading branch information
iffy committed Jun 30, 2015
1 parent 5e85b27 commit ddaa3b6
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 4 deletions.
32 changes: 32 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,38 @@ API Documentation

Generate the HA1 hash that can be stored in the user database when ``use_ha1_pw`` is set to True in the constructor.

.. method:: generate_nonce(nonce_making_callback)

If defined, this callback function will be called by the framework to
generate a nonce. If this is defined, ``verify_nonce`` should
also be defined.

This can be used to use a state storage mechanism other than the session.

.. method:: verify_nonce(nonce_verify_callback)

If defined, this callback function will be called by the framework to
verify that a nonce is valid. It will be called with a single argument:
the nonce to be verified.

This can be used to use a state storage mechanism other than the session.

.. method:: generate_opaque(opaque_making_callback)

If defined, this callback function will be called by the framework to
generate an opaque value. If this is defined, ``verify_opaque`` should
also be defined.

This can be used to use a state storage mechanism other than the session.

.. method:: verify_opaque(opaque_verify_callback)

If defined, this callback function will be called by the framework to
verify that an opaque value is valid. It will be called with a single
argument: the opaque value to be verified.

This can be used to use a state storage mechanism other than the session.

.. method:: get_password(password_callback)

See basic authentication for documentation and examples.
Expand Down
49 changes: 45 additions & 4 deletions flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,49 @@ def __init__(self, use_ha1_pw = False):
except NotImplementedError:
self.random = Random()

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

def default_generate_nonce():
session["auth_nonce"] = _generate_random()
return session["auth_nonce"]

def default_verify_nonce(nonce):
return nonce == session.get("auth_nonce")

def default_generate_opaque():
session["auth_opaque"] = _generate_random()
return session["auth_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)
self.verify_opaque(default_verify_opaque)

def generate_nonce(self, f):
self.generate_nonce_callback = f
return f

def verify_nonce(self, f):
self.verify_nonce_callback = f
return f

def generate_opaque(self, f):
self.generate_opaque_callback = f
return f

def verify_opaque(self, f):
self.verify_opaque_callback = f
return f

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

def get_opaque(self):
return self.generate_opaque_callback()

def generate_ha1(self, username, password):
a1 = username + ":" + self.realm + ":" + password
Expand All @@ -124,7 +165,7 @@ def generate_ha1(self, username, password):

def authenticate_header(self):
session["auth_nonce"] = self.get_nonce()
session["auth_opaque"] = 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"])

Expand All @@ -133,8 +174,8 @@ def authenticate(self, auth, stored_password_or_ha1):
or not auth.nonce or not auth.response \
or not stored_password_or_ha1:
return False
if auth.nonce != session.get("auth_nonce") or \
auth.opaque != session.get("auth_opaque"):
if not(self.verify_nonce_callback(auth.nonce)) or \
not(self.verify_opaque_callback(auth.opaque)):
return False
if self.use_ha1_pw:
ha1 = stored_password_or_ha1
Expand Down
51 changes: 51 additions & 0 deletions test_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,57 @@ def test_digest_generate_ha1(self):
ha1_expected = get_ha1('pawel', 'test', self.digest_auth.realm)
self.assertEqual(ha1, ha1_expected)

def test_digest_custom_nonce_checker(self):
@self.digest_auth.generate_nonce
def noncemaker():
return 'not a good nonce'

@self.digest_auth.generate_opaque
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)
return True

response = self.client.get('/digest')
self.assertEqual(response.status_code, 401)
header = response.headers.get('WWW-Authenticate')
auth_type, auth_info = header.split(None, 1)
d = parse_dict_header(auth_info)

self.assertEqual(d['nonce'], 'not a good nonce')
self.assertEqual(d['opaque'], 'some opaque')

a1 = 'john:' + d['realm'] + ':bye'
ha1 = md5(a1).hexdigest()
a2 = 'GET:/digest'
ha2 = md5(a2).hexdigest()
a3 = ha1 + ':' + d['nonce'] + ':' + ha2
auth_response = md5(a3).hexdigest()

response = self.client.get(
'/digest', headers={
'Authorization': 'Digest username="john",realm="{0}",'
'nonce="{1}",uri="/digest",response="{2}",'
'opaque="{3}"'.format(d['realm'],
d['nonce'],
auth_response,
d['opaque'])})
self.assertEqual(response.data, b'digest_auth:john')
self.assertEqual(verify_nonce_called, ['not a good nonce'],
"Should have verified the nonce.")
self.assertEqual(verify_opaque_called, ['some opaque'],
"Should have verified the opaque.")


def suite():
return unittest.makeSuite(HTTPAuthTestCase)
Expand Down

0 comments on commit ddaa3b6

Please sign in to comment.