mirror of
https://github.com/kennethreitz/httpbin.git
synced 2026-06-05 14:50:17 +00:00
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:
+83
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user