Added support for another Digest algorithm, SHA256.

This commit is contained in:
teijsden
2015-11-04 15:30:01 +01:00
parent c688307868
commit 14b7f46c71
4 changed files with 31 additions and 24 deletions
+7 -5
View File
@@ -405,9 +405,11 @@ def hidden_basic_auth(user='user', passwd='passwd'):
return jsonify(authenticated=True, user=user)
@app.route('/digest-auth/<qop>/<user>/<passwd>')
def digest_auth(qop=None, user='user', passwd='passwd'):
@app.route('/digest-auth/<algorithm>/<qop>/<user>/<passwd>')
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
+17 -12
View File
@@ -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")
+1 -1
View File
@@ -28,7 +28,7 @@
<li><a href="{{ url_for('delete_cookies', k1='', k2='') }}"><code>/cookies/delete?name</code></a> Deletes one or more simple cookies.</li>
<li><a href="{{ url_for('basic_auth', user='user', passwd='passwd') }}"><code>/basic-auth/:user/:passwd</code></a> Challenges HTTPBasic Auth.</li>
<li><a href="{{ url_for('hidden_basic_auth', user='user', passwd='passwd') }}"><code>/hidden-basic-auth/:user/:passwd</code></a> 404'd BasicAuth.</li>
<li><a href="{{ url_for('digest_auth', qop='auth', user='user', passwd='passwd') }}"><code>/digest-auth/:qop/:user/:passwd</code></a> Challenges HTTP Digest Auth.</li>
<li><a href="{{ url_for('digest_auth', algorithm='MD5', qop='auth', user='user', passwd='passwd') }}"><code>/digest-auth/:algorithm/:qop/:user/:passwd</code></a> Challenges HTTP Digest Auth.</li>
<li><a href="{{ url_for('stream_n_messages', n=20) }}"><code>/stream/:n</code></a> Streams <em>n</em>100 lines.</li>
<li><a href="{{ url_for('delay_response', delay=3) }}"><code>/delay/:n</code></a> Delays responding for <em>n</em>10 seconds.</li>
<li><a href="{{ url_for('drip', numbytes=5, duration=5, code=200) }}"><code>/drip?numbytes=n&amp;duration=s&amp;delay=s&amp;code=code</code></a> Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code.</li>
+6 -6
View File
@@ -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