Skip to content

Commit

Permalink
Add MD5-Sess algorithm for Digest auth
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Apr 20, 2022
1 parent ffeab17 commit 8a5d1eb
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 6 deletions.
17 changes: 14 additions & 3 deletions src/flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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':
Expand Down
54 changes: 51 additions & 3 deletions tests/test_digest_get_password.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit 8a5d1eb

Please sign in to comment.