-
-
Notifications
You must be signed in to change notification settings - Fork 228
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for digest auth with qop=auth
- Loading branch information
1 parent
1e9a98b
commit d311fe5
Showing
5 changed files
with
274 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
#!/usr/bin/env python | ||
"""Digest authentication example | ||
This example demonstrates how to protect Flask endpoints with digest | ||
authentication. | ||
After running this example, visit http://localhost:5000 in your browser. To | ||
gain access, you can use (username=john, password=hello) or | ||
(username=susan, password=bye). | ||
""" | ||
from flask import Flask | ||
from flask_httpauth import HTTPDigestAuth | ||
|
||
app = Flask(__name__) | ||
app.secret_key = 'this-is-a-secret-key' | ||
auth = HTTPDigestAuth(qop='auth') | ||
|
||
users = { | ||
"john": "hello", | ||
"susan": "bye", | ||
} | ||
|
||
|
||
@auth.get_password | ||
def get_password(username): | ||
return users.get(username) | ||
|
||
|
||
@app.route('/') | ||
@auth.login_required | ||
def index(): | ||
return "Hello, %s!" % auth.current_user() | ||
|
||
|
||
if __name__ == '__main__': | ||
app.run(debug=True, host='0.0.0.0') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
import unittest | ||
import re | ||
from hashlib import md5 as basic_md5 | ||
from flask import Flask | ||
from flask_httpauth import HTTPDigestAuth | ||
from werkzeug.http import parse_dict_header | ||
|
||
|
||
def md5(str): | ||
if type(str).__name__ == 'str': | ||
str = str.encode('utf-8') | ||
return basic_md5(str) | ||
|
||
|
||
def get_ha1(user, pw, realm): | ||
a1 = user + ":" + realm + ":" + pw | ||
return md5(a1).hexdigest() | ||
|
||
|
||
class HTTPAuthTestCase(unittest.TestCase): | ||
def setUp(self): | ||
app = Flask(__name__) | ||
app.config['SECRET_KEY'] = 'my secret' | ||
|
||
digest_auth = HTTPDigestAuth(qop=None) | ||
|
||
@digest_auth.get_password | ||
def get_digest_password_2(username): | ||
if username == 'susan': | ||
return 'hello' | ||
elif username == 'john': | ||
return 'bye' | ||
else: | ||
return None | ||
|
||
@app.route('/') | ||
def index(): | ||
return 'index' | ||
|
||
@app.route('/digest') | ||
@digest_auth.login_required | ||
def digest_auth_route(): | ||
return 'digest_auth:' + digest_auth.username() | ||
|
||
self.app = app | ||
self.digest_auth = digest_auth | ||
self.client = app.test_client() | ||
|
||
def test_digest_auth_prompt(self): | ||
response = self.client.get('/digest') | ||
self.assertEqual(response.status_code, 401) | ||
self.assertTrue('WWW-Authenticate' in response.headers) | ||
self.assertTrue(re.match(r'^Digest realm="Authentication Required",' | ||
r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$', | ||
response.headers['WWW-Authenticate'])) | ||
|
||
def test_digest_auth_ignore_options(self): | ||
response = self.client.options('/digest') | ||
self.assertEqual(response.status_code, 200) | ||
self.assertTrue('WWW-Authenticate' not in response.headers) | ||
|
||
def test_digest_auth_login_valid(self): | ||
response = self.client.get('/digest') | ||
self.assertTrue(response.status_code == 401) | ||
header = response.headers.get('WWW-Authenticate') | ||
auth_type, auth_info = header.split(None, 1) | ||
d = parse_dict_header(auth_info) | ||
|
||
a1 = 'john:' + d['realm'] + ':bye' | ||
ha1 = md5(a1).hexdigest() | ||
a2 = 'GET:/digest' | ||
ha2 = md5(a2).hexdigest() | ||
a3 = ha1 + ':' + d['nonce'] + ':' + ha2 | ||
auth_response = md5(a3).hexdigest() | ||
|
||
response = self.client.get( | ||
'/digest', headers={ | ||
'Authorization': 'Digest username="john",realm="{0}",' | ||
'nonce="{1}",uri="/digest",response="{2}",' | ||
'opaque="{3}"'.format(d['realm'], | ||
d['nonce'], | ||
auth_response, | ||
d['opaque'])}) | ||
self.assertEqual(response.data, b'digest_auth:john') | ||
|
||
def test_digest_auth_login_bad_realm(self): | ||
response = self.client.get('/digest') | ||
self.assertTrue(response.status_code == 401) | ||
header = response.headers.get('WWW-Authenticate') | ||
auth_type, auth_info = header.split(None, 1) | ||
d = parse_dict_header(auth_info) | ||
|
||
a1 = 'john:' + 'Wrong Realm' + ':bye' | ||
ha1 = md5(a1).hexdigest() | ||
a2 = 'GET:/digest' | ||
ha2 = md5(a2).hexdigest() | ||
a3 = ha1 + ':' + d['nonce'] + ':' + ha2 | ||
auth_response = md5(a3).hexdigest() | ||
|
||
response = self.client.get( | ||
'/digest', headers={ | ||
'Authorization': 'Digest username="john",realm="{0}",' | ||
'nonce="{1}",uri="/digest",response="{2}",' | ||
'opaque="{3}"'.format(d['realm'], | ||
d['nonce'], | ||
auth_response, | ||
d['opaque'])}) | ||
self.assertEqual(response.status_code, 401) | ||
self.assertTrue('WWW-Authenticate' in response.headers) | ||
self.assertTrue(re.match(r'^Digest realm="Authentication Required",' | ||
r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$', | ||
response.headers['WWW-Authenticate'])) | ||
|
||
def test_digest_auth_login_invalid2(self): | ||
response = self.client.get('/digest') | ||
self.assertEqual(response.status_code, 401) | ||
header = response.headers.get('WWW-Authenticate') | ||
auth_type, auth_info = header.split(None, 1) | ||
d = parse_dict_header(auth_info) | ||
|
||
a1 = 'david:' + 'Authentication Required' + ':bye' | ||
ha1 = md5(a1).hexdigest() | ||
a2 = 'GET:/digest' | ||
ha2 = md5(a2).hexdigest() | ||
a3 = ha1 + ':' + d['nonce'] + ':' + ha2 | ||
auth_response = md5(a3).hexdigest() | ||
|
||
response = self.client.get( | ||
'/digest', headers={ | ||
'Authorization': 'Digest username="david",realm="{0}",' | ||
'nonce="{1}",uri="/digest",response="{2}",' | ||
'opaque="{3}"'.format(d['realm'], | ||
d['nonce'], | ||
auth_response, | ||
d['opaque'])}) | ||
self.assertEqual(response.status_code, 401) | ||
self.assertTrue('WWW-Authenticate' in response.headers) | ||
self.assertTrue(re.match(r'^Digest realm="Authentication Required",' | ||
r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$', | ||
response.headers['WWW-Authenticate'])) | ||
|
||
def test_digest_generate_ha1(self): | ||
ha1 = self.digest_auth.generate_ha1('pawel', 'test') | ||
ha1_expected = get_ha1('pawel', 'test', self.digest_auth.realm) | ||
self.assertEqual(ha1, ha1_expected) | ||
|
||
def test_digest_custom_nonce_checker(self): | ||
@self.digest_auth.generate_nonce | ||
def noncemaker(): | ||
return 'not a good nonce' | ||
|
||
@self.digest_auth.generate_opaque | ||
def opaquemaker(): | ||
return 'some opaque' | ||
|
||
verify_nonce_called = [] | ||
|
||
@self.digest_auth.verify_nonce | ||
def verify_nonce(provided_nonce): | ||
verify_nonce_called.append(provided_nonce) | ||
return True | ||
|
||
verify_opaque_called = [] | ||
|
||
@self.digest_auth.verify_opaque | ||
def verify_opaque(provided_opaque): | ||
verify_opaque_called.append(provided_opaque) | ||
return True | ||
|
||
response = self.client.get('/digest') | ||
self.assertEqual(response.status_code, 401) | ||
header = response.headers.get('WWW-Authenticate') | ||
auth_type, auth_info = header.split(None, 1) | ||
d = parse_dict_header(auth_info) | ||
|
||
self.assertEqual(d['nonce'], 'not a good nonce') | ||
self.assertEqual(d['opaque'], 'some opaque') | ||
|
||
a1 = 'john:' + d['realm'] + ':bye' | ||
ha1 = md5(a1).hexdigest() | ||
a2 = 'GET:/digest' | ||
ha2 = md5(a2).hexdigest() | ||
a3 = ha1 + ':' + d['nonce'] + ':' + ha2 | ||
auth_response = md5(a3).hexdigest() | ||
|
||
response = self.client.get( | ||
'/digest', headers={ | ||
'Authorization': 'Digest username="john",realm="{0}",' | ||
'nonce="{1}",uri="/digest",response="{2}",' | ||
'opaque="{3}"'.format(d['realm'], | ||
d['nonce'], | ||
auth_response, | ||
d['opaque'])}) | ||
self.assertEqual(response.data, b'digest_auth:john') | ||
self.assertEqual(verify_nonce_called, ['not a good nonce'], | ||
"Should have verified the nonce.") | ||
self.assertEqual(verify_opaque_called, ['some opaque'], | ||
"Should have verified the opaque.") |