diff --git a/httpbin/core.py b/httpbin/core.py index 12b816c..caa664c 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -405,9 +405,11 @@ def hidden_basic_auth(user='user', passwd='passwd'): return jsonify(authenticated=True, user=user) -@app.route('/digest-auth///') -def digest_auth(qop=None, user='user', passwd='passwd'): +@app.route('/digest-auth////') +def digest_auth(algorithm='MD5', qop=None, user='user', passwd='passwd'): """Prompts the user for authorization using HTTP Digest auth""" + if algorithm not in ('MD5', 'SHA-256'): + algorithm = 'MD5' if qop not in ('auth', 'auth-int'): qop = None if 'Authorization' not in request.headers or \ @@ -426,12 +428,12 @@ def digest_auth(qop=None, user='user', passwd='passwd'): str(time.time()).encode('ascii'), b':', os.urandom(10) - ])) - opaque = H(os.urandom(10)) + ]), "MD5") + opaque = H(os.urandom(10), "MD5") auth = WWWAuthenticate("digest") auth.set_digest('me@kennethreitz.com', nonce, opaque=opaque, - qop=('auth', 'auth-int') if qop is None else (qop, )) + qop=('auth', 'auth-int') if qop is None else (qop, ), algorithm=algorithm) response.headers['WWW-Authenticate'] = auth.to_header() response.headers['Set-Cookie'] = 'fake=fake_value' return response diff --git a/httpbin/helpers.py b/httpbin/helpers.py index 404077c..3913ca1 100644 --- a/httpbin/helpers.py +++ b/httpbin/helpers.py @@ -9,7 +9,7 @@ This module provides helper functions for httpbin. import json import base64 -from hashlib import md5 +from hashlib import md5, sha256 from werkzeug.http import parse_authorization_header from flask import request, make_response @@ -260,11 +260,14 @@ def check_basic_auth(user, passwd): # Digest auth helpers # qop is a quality of protection -def H(data): - return md5(data).hexdigest() +def H(data, algorithm): + if algorithm == 'SHA-256': + return sha256(data).hexdigest() + else: + return md5(data).hexdigest() -def HA1(realm, username, password): +def HA1(realm, username, password, algorithm): """Create HA1 hash by realm, username, password HA1 = md5(A1) = MD5(username:realm:password) @@ -273,10 +276,10 @@ def HA1(realm, username, password): realm = u'' return H(b":".join([username.encode('utf-8'), realm.encode('utf-8'), - password.encode('utf-8')])) + password.encode('utf-8')]), algorithm) -def HA2(credentails, request): +def HA2(credentails, request, algorithm): """Create HA2 md5 hash If the qop directive's value is "auth" or is unspecified, then HA2: @@ -285,14 +288,14 @@ def HA2(credentails, request): HA2 = md5(A2) = MD5(method:digestURI:MD5(entityBody)) """ if credentails.get("qop") == "auth" or credentails.get('qop') is None: - return H(b":".join([request['method'].encode('utf-8'), request['uri'].encode('utf-8')])) + return H(b":".join([request['method'].encode('utf-8'), request['uri'].encode('utf-8')]), algorithm) elif credentails.get("qop") == "auth-int": 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']))) + H(request['body'])), algorithm) raise ValueError @@ -310,18 +313,20 @@ def response(credentails, password, request): - `request`: request dict """ response = None + algorithm = credentails.get('algorithm') HA1_value = HA1( credentails.get('realm'), credentails.get('username'), - password + password, + algorithm ) - HA2_value = HA2(credentails, request) + HA2_value = HA2(credentails, request, algorithm) if credentails.get('qop') is None: response = H(b":".join([ HA1_value.encode('utf-8'), credentails.get('nonce', '').encode('utf-8'), HA2_value.encode('utf-8') - ])) + ]), algorithm) elif credentails.get('qop') == 'auth' or credentails.get('qop') == 'auth-int': for k in 'nonce', 'nc', 'cnonce', 'qop': if k not in credentails: @@ -331,7 +336,7 @@ def response(credentails, password, request): credentails.get('nc').encode('utf-8'), credentails.get('cnonce').encode('utf-8'), credentails.get('qop').encode('utf-8'), - HA2_value.encode('utf-8')])) + HA2_value.encode('utf-8')]), algorithm) else: raise ValueError("qop value are wrong") diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index f858286..567bfb6 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -28,7 +28,7 @@
  • /cookies/delete?name Deletes one or more simple cookies.
  • /basic-auth/:user/:passwd Challenges HTTPBasic Auth.
  • /hidden-basic-auth/:user/:passwd 404'd BasicAuth.
  • -
  • /digest-auth/:qop/:user/:passwd Challenges HTTP Digest Auth.
  • +
  • /digest-auth/:algorithm/:qop/:user/:passwd Challenges HTTP Digest Auth.
  • /stream/:n Streams n–100 lines.
  • /delay/:n Delays responding for n–10 seconds.
  • /drip?numbytes=n&duration=s&delay=s&code=code Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.
  • diff --git a/test_httpbin.py b/test_httpbin.py index 9893308..1721fe6 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -164,9 +164,9 @@ class HttpbinTestCase(unittest.TestCase): self.assertEqual(response.status_code, 200) def test_digest_auth_with_wrong_password(self): - auth_header = 'Digest username="user",realm="wrong",nonce="wrong",uri="/digest-auth/user/passwd",response="wrong",opaque="wrong"' + auth_header = 'Digest username="user",realm="wrong",nonce="wrong",uri="/digest-auth/MD5/user/passwd",response="wrong",opaque="wrong"' response = self.app.get( - '/digest-auth/auth/user/passwd', + '/digest-auth/MD5/auth/user/passwd', environ_base={ # httpbin's digest auth implementation uses the remote addr to # build the nonce @@ -181,7 +181,7 @@ class HttpbinTestCase(unittest.TestCase): def test_digest_auth(self): # make first request unauthorized_response = self.app.get( - '/digest-auth/auth/user/passwd', + '/digest-auth/MD5/auth/user/passwd', environ_base={ # digest auth uses the remote addr to build the nonce 'REMOTE_ADDR': '127.0.0.1', @@ -196,7 +196,7 @@ class HttpbinTestCase(unittest.TestCase): 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' + a2 = b'GET:/digest-auth/MD5/auth/user/passwd' ha2 = md5(a2).hexdigest().encode('utf-8') a3 = ha1 + b':' + d['nonce'].encode('utf-8') + b':' + ha2 auth_response = md5(a3).hexdigest() @@ -204,14 +204,14 @@ class HttpbinTestCase(unittest.TestCase): d['realm'] + \ '",nonce="' + \ d['nonce'] + \ - '",uri="/digest-auth/auth/user/passwd",response="' + \ + '",uri="/digest-auth/MD5/auth/user/passwd",response="' + \ auth_response + \ '",opaque="' + \ d['opaque'] + '"' # make second request authorized_response = self.app.get( - '/digest-auth/auth/user/passwd', + '/digest-auth/MD5/auth/user/passwd', environ_base={ # httpbin's digest auth implementation uses the remote addr to # build the nonce