Merge pull request #308 from felixpalta/Runscope-GH-307-fix-digest-auth-int

GH-307 Fix error 500 on digest auth with qop='auth-int'
This commit is contained in:
Ian Cordasco
2016-12-04 14:38:48 -06:00
committed by GitHub
2 changed files with 100 additions and 24 deletions
+4 -3
View File
@@ -292,9 +292,10 @@ def HA2(credentails, request, algorithm):
for k in 'method', 'uri', 'body':
if k not in request:
raise ValueError("%s required" % k)
return H("%s:%s:%s" % (request['method'],
request['uri'],
H(request['body'])), algorithm)
A2 = b":".join([request['method'].encode('utf-8'),
request['uri'].encode('utf-8'),
H(request['body'], algorithm).encode('utf-8')])
return H(A2, algorithm)
raise ValueError
+96 -21
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,73 @@ 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, use MD5 by default"""
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=None):
"""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 (bytes), used if qop is "auth-int"
"""
assert username
assert password
assert nonce
assert method
assert uri
assert algorithm in ('MD5', 'SHA-256', None)
a1 = ':'.join([username, realm or '', password])
ha1 = _hash(a1.encode('utf-8'), algorithm)
a2 = ':'.join([method, uri])
if qop == 'auth-int':
a2 = ':'.join([a2, _hash(body or b'', algorithm)])
ha2 = _hash(a2.encode('utf-8'), algorithm)
a3 = ':'.join([ha1, nonce])
if qop in ('auth', 'auth-int'):
assert cnonce
assert nc
a3 = ':'.join([a3, nc, cnonce, qop])
a3 = ':'.join([a3, ha2])
auth_response = _hash(a3.encode('utf-8'), algorithm)
auth_header = \
'Digest username="{0}", response="{1}", uri="{2}", nonce="{3}"'\
.format(username, auth_response, uri, nonce)
# 'realm' and 'opaque' should be returned unchanged, even if empty
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"""
@@ -176,12 +243,25 @@ class HttpbinTestCase(unittest.TestCase):
'Authorization': auth_header,
}
)
assert 'Digest' in response.headers.get('WWW-Authenticate')
self.assertTrue('Digest' in response.headers.get('WWW-Authenticate'))
self.assertEqual(response.status_code, 401)
def test_digest_auth(self):
# make first request
"""Test different combinations of digest auth parameters"""
username = 'user'
password = 'passwd'
for qop in None, 'auth', 'auth-int',:
for algorithm in None, 'MD5', 'SHA-256':
for body in None, b'', b'request payload':
self._test_digest_auth(username, password, qop, algorithm, body)
def _test_digest_auth(self, username, password, qop=None, algorithm=None, body=None):
uri = '/digest-auth/{0}/{1}/{2}'.format(qop or 'wrong-qop', username, password)
if algorithm:
uri += '/' + algorithm
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 +271,21 @@ 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'] + '"'
nonce = d['nonce']
realm = d['realm']
opaque = d['opaque']
cnonce, nc = (_hash(os.urandom(10), "MD5"), '00000001') if qop in ('auth', 'auth-int') else (None, None)
auth_header = _make_digest_auth_header(
username, password, 'GET', uri, nonce, realm, opaque, algorithm, qop, cnonce, nc, body)
# 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
@@ -219,7 +293,8 @@ class HttpbinTestCase(unittest.TestCase):
},
headers={
'Authorization': auth_header,
}
},
data=body
)
# done!