Permalink
Browse files

Merge pull request #308 from felixpalta/Runscope-GH-307-fix-digest-au…

…th-int


GH-307 Fix error 500 on digest auth with qop='auth-int'
  • Loading branch information...
2 parents ce32861 + e7e9163 commit cff242d46f8068f3f1eacaf48ba0f7bb1cab20e4 @sigmavirus24 sigmavirus24 committed on GitHub Dec 4, 2016
Showing with 100 additions and 24 deletions.
  1. +4 −3 httpbin/helpers.py
  2. +96 −21 test_httpbin.py
View
@@ -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
View
@@ -7,7 +7,7 @@
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 @@ def test_digest_auth_with_wrong_password(self):
'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,35 +271,30 @@ def test_digest_auth(self):
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
'REMOTE_ADDR': '127.0.0.1',
},
headers={
'Authorization': auth_header,
- }
+ },
+ data=body
)
# done!

0 comments on commit cff242d

Please sign in to comment.