diff --git a/docs/index.rst b/docs/index.rst index 9b1d851..fc34861 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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. diff --git a/flask_httpauth.py b/flask_httpauth.py index a9b12cb..bf497c7 100644 --- a/flask_httpauth.py +++ b/flask_httpauth.py @@ -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 @@ -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"]) @@ -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 diff --git a/test_httpauth.py b/test_httpauth.py index 747b274..1208e45 100644 --- a/test_httpauth.py +++ b/test_httpauth.py @@ -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)