diff --git a/src/flask_httpauth.py b/src/flask_httpauth.py index f33f6c8..eca596f 100644 --- a/src/flask_httpauth.py +++ b/src/flask_httpauth.py @@ -254,13 +254,20 @@ def authenticate(self, auth, stored_password): class HTTPDigestAuth(HTTPAuth): - def __init__(self, scheme=None, realm=None, use_ha1_pw=False, qop='auth'): + def __init__(self, scheme=None, realm=None, use_ha1_pw=False, qop='auth', + algorithm='MD5'): super(HTTPDigestAuth, self).__init__(scheme or 'Digest', realm) self.use_ha1_pw = use_ha1_pw if isinstance(qop, str): self.qop = [v.strip() for v in qop.split(',')] else: self.qop = qop + if algorithm.lower() == 'md5': + self.algorithm = 'MD5' + elif algorithm.lower() == 'md5-sess': + self.algorithm = 'MD5-Sess' + else: + raise ValueError(f'Algorithm {algorithm} is not supported') self.random = SystemRandom() try: self.random.random() @@ -331,9 +338,10 @@ def authenticate_header(self): nonce = self.get_nonce() opaque = self.get_opaque() if self.qop: - return '{0} realm="{1}",nonce="{2}",opaque="{3}",qop="{4}"'.format( + return ('{0} realm="{1}",nonce="{2}",opaque="{3}",algorithm="{4}"' + ',qop="{5}"').format( self.scheme, self.realm, nonce, - opaque, ','.join(self.qop)) + opaque, self.algorithm, ','.join(self.qop)) else: return '{0} realm="{1}",nonce="{2}",opaque="{3}"'.format( self.scheme, self.realm, nonce, @@ -355,6 +363,9 @@ def authenticate(self, auth, stored_password_or_ha1): a1 = auth.username + ":" + auth.realm + ":" + \ stored_password_or_ha1 ha1 = md5(a1.encode('utf-8')).hexdigest() + if self.algorithm == 'MD5-Sess': + ha1 = md5((ha1 + ':' + auth.nonce + ':' + auth.cnonce).encode( + 'utf-8')).hexdigest() a2 = request.method + ":" + auth.uri ha2 = md5(a2.encode('utf-8')).hexdigest() if auth.qop == 'auth': diff --git a/tests/test_digest_get_password.py b/tests/test_digest_get_password.py index 8f6b4bf..6a6491f 100644 --- a/tests/test_digest_get_password.py +++ b/tests/test_digest_get_password.py @@ -1,5 +1,6 @@ import unittest import re +import pytest from hashlib import md5 as basic_md5 from flask import Flask from flask_httpauth import HTTPDigestAuth @@ -46,13 +47,32 @@ def digest_auth_route(): self.digest_auth = digest_auth self.client = app.test_client() + def test_constructor(self): + d = HTTPDigestAuth() + assert d.qop == ['auth'] + assert d.algorithm == 'MD5' + d = HTTPDigestAuth(qop=None) + assert d.qop is None + d = HTTPDigestAuth(qop='auth') + assert d.qop == ['auth'] + d = HTTPDigestAuth(qop=['foo', 'bar']) + assert d.qop == ['foo', 'bar'] + d = HTTPDigestAuth(qop='foo,bar, baz') + assert d.qop == ['foo', 'bar', 'baz'] + d = HTTPDigestAuth(algorithm='md5') + assert d.algorithm == 'MD5' + d = HTTPDigestAuth(algorithm='md5-sess') + assert d.algorithm == 'MD5-Sess' + with pytest.raises(ValueError): + HTTPDigestAuth(algorithm='foo') + def test_digest_auth_prompt(self): response = self.client.get('/digest') self.assertEqual(response.status_code, 401) self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="Authentication Required",' r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",' - r'qop="auth"$', + r'algorithm="MD5",qop="auth"$', response.headers['WWW-Authenticate'])) def test_digest_auth_ignore_options(self): @@ -85,6 +105,34 @@ def test_digest_auth_login_valid(self): d['opaque'])}) self.assertEqual(response.data, b'digest_auth:john') + def test_digest_auth_md5_sess_login_valid(self): + self.digest_auth.algorithm = 'MD5-Sess' + + response = self.client.get('/digest') + self.assertTrue(response.status_code == 401) + header = response.headers.get('WWW-Authenticate') + auth_type, auth_info = header.split(None, 1) + d = parse_dict_header(auth_info) + + a1 = 'john:' + d['realm'] + ':bye' + ha1 = md5( + md5(a1).hexdigest() + ':' + d['nonce'] + ':foobar').hexdigest() + a2 = 'GET:/digest' + ha2 = md5(a2).hexdigest() + a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2 + auth_response = md5(a3).hexdigest() + + response = self.client.get( + '/digest', headers={ + 'Authorization': 'Digest username="john",realm="{0}",' + 'nonce="{1}",uri="/digest",qop=auth,' + 'nc=00000001,cnonce="foobar",response="{2}",' + 'opaque="{3}"'.format(d['realm'], + d['nonce'], + auth_response, + d['opaque'])}) + self.assertEqual(response.data, b'digest_auth:john') + def test_digest_auth_login_bad_realm(self): response = self.client.get('/digest') self.assertTrue(response.status_code == 401) @@ -112,7 +160,7 @@ def test_digest_auth_login_bad_realm(self): self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="Authentication Required",' r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",' - r'qop="auth"$', + r'algorithm="MD5",qop="auth"$', response.headers['WWW-Authenticate'])) def test_digest_auth_login_invalid2(self): @@ -142,7 +190,7 @@ def test_digest_auth_login_invalid2(self): self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="Authentication Required",' r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",' - r'qop="auth"$', + r'algorithm="MD5",qop="auth"$', response.headers['WWW-Authenticate'])) def test_digest_generate_ha1(self):