Add test implementation for Digest authentication

It can be used for testing of all available combinations of qop (auth/auth-int/not specified) and algorithms (MD5, SHA-256).
This commit is contained in:
Felix Palta
2016-12-03 23:05:24 +03:00
parent cc2fc5ea14
commit f198dab947
+83 -18
View File
@@ -7,7 +7,7 @@ import contextlib
import six
import json
from werkzeug.http import parse_dict_header
from hashlib import md5
from hashlib import md5, sha256
from six import BytesIO
import httpbin
@@ -36,6 +36,74 @@ def _string_to_base64(string):
utf8_encoded = string.encode('utf-8')
return base64.urlsafe_b64encode(utf8_encoded)
def _hash(data, algorithm):
"""Encode binary data according to specified algorithm"""
if algorithm == 'SHA-256':
return sha256(data).hexdigest()
else:
return md5(data).hexdigest()
def _make_digest_auth_header(username, password, method, uri, nonce,
realm=None, opaque=None, algorithm=None,
qop=None, cnonce=None, nc=None, body=''):
"""Compile a digest authentication header string
Arguments:
- `nonce': nonce string, received within "WWW-Authenticate" header
- `realm`: realm string, received within "WWW-Authenticate" header
- `opaque`: opaque string, received within "WWW-Authenticate" header
- `algorithm`: type of hashing algorithm, used by the client
- `qop`: type of quality-of-protection, used by the client
- `cnonce`: client nonce, required if qop is "auth" or "auth-int"
- `nc`: client nonce count, required if qop is "auth" or "auth-int"
- `body`: body of the outgoing request
"""
assert username
assert password
assert nonce
assert method
assert uri
assert algorithm in ('MD5', 'SHA-256', None)
a1 = b':'.join([username.encode('utf-8'),
(realm or '').encode('utf-8'),
password.encode('utf-8')])
ha1 = _hash(a1, algorithm)
a2 = b':'.join([method.encode('utf-8'), uri.encode('utf-8')])
if qop == 'auth-int':
a2 = b':'.join(a2, _hash(body, algorithm))
ha2 = _hash(a2, algorithm)
a3 = b':'.join([ha1, nonce.encode('utf-8')])
if qop in ('auth', 'auth-int'):
assert cnonce
assert nc
a3 = b':'.join([a3, nc.encode('utf-8'), cnonce.encode('utf-8'), qop.encode('utf-8')])
a3 = b':'.join([a3, ha2])
auth_response = _hash(a3, algorithm)
auth_header = \
'Digest username="{0}", response="{1}", uri="{2}", nonce="{3}"'\
.format(username, auth_response, uri, nonce)
if realm != None:
auth_header += ', realm="{0}"'.format(realm)
if opaque != None:
auth_header += ', opaque="{0}"'.format(opaque)
if algorithm:
auth_header += ', algorithm="{0}"'.format(algorithm)
if cnonce:
auth_header += ', cnonce="{0}"'.format(cnonce)
if nc:
auth_header += ', nc={0}'.format(nc)
if qop:
auth_header += ', qop={0}'.format(qop)
return auth_header
class HttpbinTestCase(unittest.TestCase):
"""Httpbin tests"""
@@ -180,8 +248,9 @@ class HttpbinTestCase(unittest.TestCase):
def test_digest_auth(self):
# make first request
uri = '/digest-auth/auth/user/passwd/MD5'
unauthorized_response = self.app.get(
'/digest-auth/auth/user/passwd/MD5',
uri,
environ_base={
# digest auth uses the remote addr to build the nonce
'REMOTE_ADDR': '127.0.0.1',
@@ -191,27 +260,23 @@ class HttpbinTestCase(unittest.TestCase):
self.assertEqual(unauthorized_response.status_code, 401)
header = unauthorized_response.headers.get('WWW-Authenticate')
auth_type, auth_info = header.split(None, 1)
self.assertEqual(auth_type, 'Digest')
# Begin crappy digest-auth implementation
d = parse_dict_header(auth_info)
a1 = b'user:' + d['realm'].encode('utf-8') + b':passwd'
ha1 = md5(a1).hexdigest().encode('utf-8')
a2 = b'GET:/digest-auth/auth/user/passwd/MD5'
ha2 = md5(a2).hexdigest().encode('utf-8')
a3 = ha1 + b':' + d['nonce'].encode('utf-8') + b':' + ha2
auth_response = md5(a3).hexdigest()
auth_header = 'Digest username="user",realm="' + \
d['realm'] + \
'",nonce="' + \
d['nonce'] + \
'",uri="/digest-auth/auth/user/passwd/MD5",response="' + \
auth_response + \
'",opaque="' + \
d['opaque'] + '"'
username = 'user'
password = 'passwd'
method = 'GET'
nonce = d['nonce']
realm = d['realm']
opaque = d['opaque']
auth_header = _make_digest_auth_header(
username, password, method, uri, nonce, realm, opaque)
# make second request
authorized_response = self.app.get(
'/digest-auth/auth/user/passwd/MD5',
uri,
environ_base={
# httpbin's digest auth implementation uses the remote addr to
# build the nonce