Codebase list python-flask-httpauth / 37b4e53
New upstream release. Debian Janitor 1 year, 3 months ago
12 changed file(s) with 402 addition(s) and 88 deletion(s). Raw diff Collapse all Expand all
2121 strategy:
2222 matrix:
2323 os: [ubuntu-latest, macos-latest, windows-latest]
24 python: ['3.6', '3.7', '3.8', '3.9', 'pypy3']
25 exclude:
26 # pypy3 currently fails to run on Windows
27 - os: windows-latest
28 python: pypy3
24 python: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy-3.8']
2925 fail-fast: false
3026 runs-on: ${{ matrix.os }}
3127 steps:
00 # Flask-HTTPAuth change log
1
2 **Release 4.7.0** - 2022-05-29
3
4 - Fallback to latin-1 encoding for credentials when utf-8 fails [#151](https://github.com/miguelgrinberg/flask-httpauth/issues/151) ([commit](https://github.com/miguelgrinberg/flask-httpauth/commit/4a92b75b79ea8e29ed76910792208d0a0a9e897a))
5 - Documentation updates ([commit](https://github.com/miguelgrinberg/flask-httpauth/commit/b42168ed174cde0a9404dbf0b05b5b5c5d6eb46d))
6
7 **Release 4.6.0** - 2022-04-21
8
9 - Add MD5-Sess algorithm for Digest auth ([commit](https://github.com/miguelgrinberg/flask-httpauth/commit/8a5d1eb87c9b3cb71cc6c5839a4a3411ede1f505))
10 - Add qop=auth option for Digest auth ([commit](https://github.com/miguelgrinberg/flask-httpauth/commit/d311fe5e996d43316989daf8a67ded22a6a567e2)) (thanks **Edward**!)
11 - Add Python 3.10 and PyPy 3.8 to build ([commit](https://github.com/miguelgrinberg/flask-httpauth/commit/ffeab170a8230e4defb117f242937865072c8094))
112
213 **Release 4.5.0** - 2021-10-25
314
0 python-flask-httpauth (4.7.0-1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Debian Janitor <janitor@jelmer.uk> Sat, 31 Dec 2022 00:20:44 -0000
5
06 python-flask-httpauth (4.5.0-4) unstable; urgency=medium
17
28 * Another attempt at fixing the manpage. (Hopefully) closes: #1003606
7171 if __name__ == '__main__':
7272 app.run()
7373
74 Security Concerns with Digest Authentication
75 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
76
77 The digest authentication algorithm requires a *challenge* to be sent to the client for use in encrypting the password for transmission. This challenge needs to be used again when the password is decoded at the server, so the challenge information needs to be stored so that it can be recalled later.
78
79 By default, Flask-HTTPAuth stores the challenge data in the Flask session. To make the authentication flow secure when using session storage, it is required that server-side sessions are used instead of the default Flask cookie based sessions, as this ensures that the challenge data is not at risk of being captured as it moves in a cookie between server and client. The Flask-Session and Flask-KVSession extensions are both very good options to implement server-side sessions.
80
81 As an alternative to using server-side sessions, an application can implement its own generation and storage of challenge data. To do this, there are four callback functions that the application needs to implement::
82
83 @auth.generate_nonce
84 def generate_nonce():
85 """Return the nonce value to use for this client."""
86 pass
87
88 @auth.generate_opaque
89 def generate_opaque():
90 """Return the opaque value to use for this client."""
91 pass
92
93 @auth.verify_nonce
94 def verify_nonce(nonce):
95 """Verify that the nonce value sent by the client is correct."""
96 pass
97
98 @auth.verify_opaque
99 def verify_opaque(opaque):
100 """Verify that the opaque value sent by the client is correct."""
101 pass
102
103 For information of what the ``nonce`` and ``opaque`` values are and how they are used in digest authentication, consult `RFC 2617 <http://tools.ietf.org/html/rfc2617#section-3.2.1>`_.
104
10574 Token Authentication Example
10675 ----------------------------
10776
325294
326295 .. class:: HTTPDigestAuth
327296
328 This class handles HTTP Digest authentication for Flask routes. The ``SECRET_KEY`` configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies. To make this authentication method secure, a `session interface <http://flask.pocoo.org/docs/api/#flask.Flask.session_interface>`_ that writes sessions in the server must be used.
329
330 .. method:: __init__(self, scheme=None, realm=None, use_ha1_pw=False)
297 This class handles HTTP Digest authentication for Flask routes. The ``SECRET_KEY`` configuration must be set in the Flask application to enable the session to work. Flask by default stores user sessions in the client as secure cookies, so the client must be able to handle cookies.
298
299 .. method:: __init__(self, scheme=None, realm=None, use_ha1_pw=False, qop='auth', algorithm='MD5')
331300
332301 Create a digest authentication object.
333302
336305 The ``realm`` argument can be used to provide an application defined realm with the ``WWW-Authenticate`` header.
337306
338307 If ``use_ha1_pw`` is False, then the ``get_password`` callback needs to return the plain text password for the given user. If ``use_ha1_pw`` is True, the ``get_password`` callback needs to return the HA1 value for the given user. The advantage of setting ``use_ha1_pw`` to ``True`` is that it allows the application to store the HA1 hash of the password in the user database.
308
309 The ``qop`` option configures a list of accepted quality of protection extensions. This argument can be given as a comma-separated string, a list of strings, or ``None`` to disable. The default is ``auth``. The ``auth-int`` option is currently not implemented.
310
311 The ``algorithm`` option configures the hash generation algorithm to use. The default is ``MD5``. The two algorithms that are implemented are ``MD5`` and ``MD5-Sess``.
339312
340313 .. method:: generate_ha1(username, password)
341314
0 #!/usr/bin/env python
1 """Digest authentication example
2
3 This example demonstrates how to protect Flask endpoints with digest
4 authentication.
5
6 After running this example, visit http://localhost:5000 in your browser. To
7 gain access, you can use (username=john, password=hello) or
8 (username=susan, password=bye).
9 """
10 from flask import Flask
11 from flask_httpauth import HTTPDigestAuth
12
13 app = Flask(__name__)
14 app.secret_key = 'this-is-a-secret-key'
15 auth = HTTPDigestAuth(qop='auth')
16
17 users = {
18 "john": "hello",
19 "susan": "bye",
20 }
21
22
23 @auth.get_password
24 def get_password(username):
25 return users.get(username)
26
27
28 @app.route('/')
29 @auth.login_required
30 def index():
31 return "Hello, %s!" % auth.current_user()
32
33
34 if __name__ == '__main__':
35 app.run(debug=True, host='0.0.0.0')
00 [metadata]
11 name = Flask-HTTPAuth
2 version = 4.5.0
2 version = 4.7.0
33 author = Miguel Grinberg
44 author_email = miguel.grinberg@gmail.com
55 description = HTTP authentication for Flask routes
216216 value = request.headers[header].encode('utf-8')
217217 try:
218218 scheme, credentials = value.split(b' ', 1)
219 username, password = b64decode(credentials).split(b':', 1)
219 encoded_username, encoded_password = b64decode(
220 credentials).split(b':', 1)
220221 except (ValueError, TypeError):
221222 return None
222223 try:
223 username = username.decode('utf-8')
224 password = password.decode('utf-8')
224 username = encoded_username.decode('utf-8')
225 password = encoded_password.decode('utf-8')
225226 except UnicodeDecodeError:
226 username = None
227 password = None
227 # try to decode again with latin-1, which should always work
228 username = encoded_username.decode('latin1')
229 password = encoded_password.decode('latin1')
230
228231 return Authorization(
229232 scheme, {'username': username, 'password': password})
230233
253256
254257
255258 class HTTPDigestAuth(HTTPAuth):
256 def __init__(self, scheme=None, realm=None, use_ha1_pw=False):
259 def __init__(self, scheme=None, realm=None, use_ha1_pw=False, qop='auth',
260 algorithm='MD5'):
257261 super(HTTPDigestAuth, self).__init__(scheme or 'Digest', realm)
258262 self.use_ha1_pw = use_ha1_pw
263 if isinstance(qop, str):
264 self.qop = [v.strip() for v in qop.split(',')]
265 else:
266 self.qop = qop
267 if algorithm.lower() == 'md5':
268 self.algorithm = 'MD5'
269 elif algorithm.lower() == 'md5-sess':
270 self.algorithm = 'MD5-Sess'
271 else:
272 raise ValueError(f'Algorithm {algorithm} is not supported')
259273 self.random = SystemRandom()
260274 try:
261275 self.random.random()
325339 def authenticate_header(self):
326340 nonce = self.get_nonce()
327341 opaque = self.get_opaque()
328 return '{0} realm="{1}",nonce="{2}",opaque="{3}"'.format(
329 self.scheme, self.realm, nonce,
330 opaque)
342 if self.qop:
343 return ('{0} realm="{1}",nonce="{2}",opaque="{3}",algorithm="{4}"'
344 ',qop="{5}"').format(
345 self.scheme, self.realm, nonce,
346 opaque, self.algorithm, ','.join(self.qop))
347 else:
348 return '{0} realm="{1}",nonce="{2}",opaque="{3}"'.format(
349 self.scheme, self.realm, nonce,
350 opaque)
331351
332352 def authenticate(self, auth, stored_password_or_ha1):
333353 if not auth or not auth.username or not auth.realm or not auth.uri \
337357 if not(self.verify_nonce_callback(auth.nonce)) or \
338358 not(self.verify_opaque_callback(auth.opaque)):
339359 return False
360 if auth.qop and auth.qop not in self.qop: # pragma: no cover
361 return False
340362 if self.use_ha1_pw:
341363 ha1 = stored_password_or_ha1
342364 else:
343365 a1 = auth.username + ":" + auth.realm + ":" + \
344366 stored_password_or_ha1
345367 ha1 = md5(a1.encode('utf-8')).hexdigest()
368 if self.algorithm == 'MD5-Sess':
369 ha1 = md5((ha1 + ':' + auth.nonce + ':' + auth.cnonce).encode(
370 'utf-8')).hexdigest()
346371 a2 = request.method + ":" + auth.uri
347372 ha2 = md5(a2.encode('utf-8')).hexdigest()
348 a3 = ha1 + ":" + auth.nonce + ":" + ha2
373 if auth.qop == 'auth':
374 a3 = ha1 + ":" + auth.nonce + ":" + auth.nc + ":" + \
375 auth.cnonce + ":auth:" + ha2
376 else:
377 a3 = ha1 + ":" + auth.nonce + ":" + ha2
349378 response = md5(a3.encode('utf-8')).hexdigest()
350379 return hmac.compare_digest(response, auth.response)
351380
2020 return password == 'hello'
2121 elif username == 'susan':
2222 return password == 'bye'
23 elif username == 'garçon':
24 return password == 'áéíóú'
2325 elif username == '':
2426 g.anon = True
2527 return True
3032 return 'john'
3133 elif username == 'susan' and password == 'bye':
3234 return 'susan'
35 elif username == 'garçon' and password == 'áéíóú':
36 return 'garçon'
3337 elif username == '':
3438 g.anon = True
3539 return ''
5862 self.client = app.test_client()
5963
6064 def test_verify_auth_login_valid(self):
61 creds = base64.b64encode(b'susan:bye').decode('utf-8')
65 creds = base64.b64encode(b'susan:bye').decode()
6266 response = self.client.get(
6367 '/basic-verify', headers={'Authorization': 'Basic ' + creds})
6468 self.assertEqual(response.data, b'basic_verify_auth:susan anon:False')
69
70 def test_verify_auth_login_valid_latin1(self):
71 creds = base64.b64encode('garçon:áéíóú'.encode('latin1')).decode()
72 response = self.client.get(
73 '/basic-verify', headers={'Authorization': 'Basic ' + creds})
74 self.assertEqual(response.data.decode(),
75 'basic_verify_auth:garçon anon:False')
6576
6677 def test_verify_auth_login_empty(self):
6778 response = self.client.get('/basic-verify')
6879 self.assertEqual(response.data, b'basic_verify_auth: anon:True')
6980
7081 def test_verify_auth_login_invalid(self):
71 creds = base64.b64encode(b'john:bye').decode('utf-8')
82 creds = base64.b64encode(b'john:bye').decode()
7283 response = self.client.get(
7384 '/basic-verify', headers={'Authorization': 'Basic ' + creds})
7485 self.assertEqual(response.status_code, 403)
88 app = Flask(__name__)
99 app.config['SECRET_KEY'] = 'my secret'
1010
11 digest_auth_my_realm = HTTPDigestAuth(realm='My Realm')
11 digest_auth_my_realm = HTTPDigestAuth(realm='My Realm', qop=None)
1212
1313 @digest_auth_my_realm.get_password
1414 def get_digest_password_3(username):
00 import unittest
11 import re
2 import pytest
23 from hashlib import md5 as basic_md5
34 from flask import Flask
45 from flask_httpauth import HTTPDigestAuth
4546 self.digest_auth = digest_auth
4647 self.client = app.test_client()
4748
49 def test_constructor(self):
50 d = HTTPDigestAuth()
51 assert d.qop == ['auth']
52 assert d.algorithm == 'MD5'
53 d = HTTPDigestAuth(qop=None)
54 assert d.qop is None
55 d = HTTPDigestAuth(qop='auth')
56 assert d.qop == ['auth']
57 d = HTTPDigestAuth(qop=['foo', 'bar'])
58 assert d.qop == ['foo', 'bar']
59 d = HTTPDigestAuth(qop='foo,bar, baz')
60 assert d.qop == ['foo', 'bar', 'baz']
61 d = HTTPDigestAuth(algorithm='md5')
62 assert d.algorithm == 'MD5'
63 d = HTTPDigestAuth(algorithm='md5-sess')
64 assert d.algorithm == 'MD5-Sess'
65 with pytest.raises(ValueError):
66 HTTPDigestAuth(algorithm='foo')
67
4868 def test_digest_auth_prompt(self):
4969 response = self.client.get('/digest')
5070 self.assertEqual(response.status_code, 401)
5171 self.assertTrue('WWW-Authenticate' in response.headers)
5272 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
53 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
73 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",'
74 r'algorithm="MD5",qop="auth"$',
5475 response.headers['WWW-Authenticate']))
5576
5677 def test_digest_auth_ignore_options(self):
6990 ha1 = md5(a1).hexdigest()
7091 a2 = 'GET:/digest'
7192 ha2 = md5(a2).hexdigest()
72 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
73 auth_response = md5(a3).hexdigest()
74
75 response = self.client.get(
76 '/digest', headers={
77 'Authorization': 'Digest username="john",realm="{0}",'
78 'nonce="{1}",uri="/digest",response="{2}",'
93 a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2
94 auth_response = md5(a3).hexdigest()
95
96 response = self.client.get(
97 '/digest', headers={
98 'Authorization': 'Digest username="john",realm="{0}",'
99 'nonce="{1}",uri="/digest",qop=auth,'
100 'nc=00000001,cnonce="foobar",response="{2}",'
79101 'opaque="{3}"'.format(d['realm'],
80102 d['nonce'],
81103 auth_response,
82104 d['opaque'])})
83105 self.assertEqual(response.data, b'digest_auth:john')
84106
107 def test_digest_auth_md5_sess_login_valid(self):
108 self.digest_auth.algorithm = 'MD5-Sess'
109
110 response = self.client.get('/digest')
111 self.assertTrue(response.status_code == 401)
112 header = response.headers.get('WWW-Authenticate')
113 auth_type, auth_info = header.split(None, 1)
114 d = parse_dict_header(auth_info)
115
116 a1 = 'john:' + d['realm'] + ':bye'
117 ha1 = md5(
118 md5(a1).hexdigest() + ':' + d['nonce'] + ':foobar').hexdigest()
119 a2 = 'GET:/digest'
120 ha2 = md5(a2).hexdigest()
121 a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2
122 auth_response = md5(a3).hexdigest()
123
124 response = self.client.get(
125 '/digest', headers={
126 'Authorization': 'Digest username="john",realm="{0}",'
127 'nonce="{1}",uri="/digest",qop=auth,'
128 'nc=00000001,cnonce="foobar",response="{2}",'
129 'opaque="{3}"'.format(d['realm'],
130 d['nonce'],
131 auth_response,
132 d['opaque'])})
133 self.assertEqual(response.data, b'digest_auth:john')
134
85135 def test_digest_auth_login_bad_realm(self):
86136 response = self.client.get('/digest')
87137 self.assertTrue(response.status_code == 401)
93143 ha1 = md5(a1).hexdigest()
94144 a2 = 'GET:/digest'
95145 ha2 = md5(a2).hexdigest()
96 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
97 auth_response = md5(a3).hexdigest()
98
99 response = self.client.get(
100 '/digest', headers={
101 'Authorization': 'Digest username="john",realm="{0}",'
102 'nonce="{1}",uri="/digest",response="{2}",'
146 a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2
147 auth_response = md5(a3).hexdigest()
148
149 response = self.client.get(
150 '/digest', headers={
151 'Authorization': 'Digest username="john",realm="{0}",'
152 'nonce="{1}",uri="/digest",qop=auth,'
153 'nc=00000001,cnonce="foobar",response="{2}",'
103154 'opaque="{3}"'.format(d['realm'],
104155 d['nonce'],
105156 auth_response,
107158 self.assertEqual(response.status_code, 401)
108159 self.assertTrue('WWW-Authenticate' in response.headers)
109160 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
110 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
161 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",'
162 r'algorithm="MD5",qop="auth"$',
111163 response.headers['WWW-Authenticate']))
112164
113165 def test_digest_auth_login_invalid2(self):
121173 ha1 = md5(a1).hexdigest()
122174 a2 = 'GET:/digest'
123175 ha2 = md5(a2).hexdigest()
124 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
125 auth_response = md5(a3).hexdigest()
126
127 response = self.client.get(
128 '/digest', headers={
129 'Authorization': 'Digest username="david",realm="{0}",'
130 'nonce="{1}",uri="/digest",response="{2}",'
176 a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2
177 auth_response = md5(a3).hexdigest()
178
179 response = self.client.get(
180 '/digest', headers={
181 'Authorization': 'Digest username="john",realm="{0}",'
182 'nonce="{1}",uri="/digest",qop=auth,'
183 'nc=00000001,cnonce="foobar",response="{2}",'
131184 'opaque="{3}"'.format(d['realm'],
132185 d['nonce'],
133186 auth_response,
135188 self.assertEqual(response.status_code, 401)
136189 self.assertTrue('WWW-Authenticate' in response.headers)
137190 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
138 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
191 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+",'
192 r'algorithm="MD5",qop="auth"$',
139193 response.headers['WWW-Authenticate']))
140194
141195 def test_digest_generate_ha1(self):
179233 ha1 = md5(a1).hexdigest()
180234 a2 = 'GET:/digest'
181235 ha2 = md5(a2).hexdigest()
182 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
183 auth_response = md5(a3).hexdigest()
184
185 response = self.client.get(
186 '/digest', headers={
187 'Authorization': 'Digest username="john",realm="{0}",'
188 'nonce="{1}",uri="/digest",response="{2}",'
236 a3 = ha1 + ':' + d['nonce'] + ':00000001:foobar:auth:' + ha2
237 auth_response = md5(a3).hexdigest()
238
239 response = self.client.get(
240 '/digest', headers={
241 'Authorization': 'Digest username="john",realm="{0}",'
242 'nonce="{1}",uri="/digest",qop=auth,'
243 'nc=00000001,cnonce="foobar",response="{2}",'
189244 'opaque="{3}"'.format(d['realm'],
190245 d['nonce'],
191246 auth_response,
0 import unittest
1 import re
2 from hashlib import md5 as basic_md5
3 from flask import Flask
4 from flask_httpauth import HTTPDigestAuth
5 from werkzeug.http import parse_dict_header
6
7
8 def md5(str):
9 if type(str).__name__ == 'str':
10 str = str.encode('utf-8')
11 return basic_md5(str)
12
13
14 def get_ha1(user, pw, realm):
15 a1 = user + ":" + realm + ":" + pw
16 return md5(a1).hexdigest()
17
18
19 class HTTPAuthTestCase(unittest.TestCase):
20 def setUp(self):
21 app = Flask(__name__)
22 app.config['SECRET_KEY'] = 'my secret'
23
24 digest_auth = HTTPDigestAuth(qop=None)
25
26 @digest_auth.get_password
27 def get_digest_password_2(username):
28 if username == 'susan':
29 return 'hello'
30 elif username == 'john':
31 return 'bye'
32 else:
33 return None
34
35 @app.route('/')
36 def index():
37 return 'index'
38
39 @app.route('/digest')
40 @digest_auth.login_required
41 def digest_auth_route():
42 return 'digest_auth:' + digest_auth.username()
43
44 self.app = app
45 self.digest_auth = digest_auth
46 self.client = app.test_client()
47
48 def test_digest_auth_prompt(self):
49 response = self.client.get('/digest')
50 self.assertEqual(response.status_code, 401)
51 self.assertTrue('WWW-Authenticate' in response.headers)
52 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
53 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
54 response.headers['WWW-Authenticate']))
55
56 def test_digest_auth_ignore_options(self):
57 response = self.client.options('/digest')
58 self.assertEqual(response.status_code, 200)
59 self.assertTrue('WWW-Authenticate' not in response.headers)
60
61 def test_digest_auth_login_valid(self):
62 response = self.client.get('/digest')
63 self.assertTrue(response.status_code == 401)
64 header = response.headers.get('WWW-Authenticate')
65 auth_type, auth_info = header.split(None, 1)
66 d = parse_dict_header(auth_info)
67
68 a1 = 'john:' + d['realm'] + ':bye'
69 ha1 = md5(a1).hexdigest()
70 a2 = 'GET:/digest'
71 ha2 = md5(a2).hexdigest()
72 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
73 auth_response = md5(a3).hexdigest()
74
75 response = self.client.get(
76 '/digest', headers={
77 'Authorization': 'Digest username="john",realm="{0}",'
78 'nonce="{1}",uri="/digest",response="{2}",'
79 'opaque="{3}"'.format(d['realm'],
80 d['nonce'],
81 auth_response,
82 d['opaque'])})
83 self.assertEqual(response.data, b'digest_auth:john')
84
85 def test_digest_auth_login_bad_realm(self):
86 response = self.client.get('/digest')
87 self.assertTrue(response.status_code == 401)
88 header = response.headers.get('WWW-Authenticate')
89 auth_type, auth_info = header.split(None, 1)
90 d = parse_dict_header(auth_info)
91
92 a1 = 'john:' + 'Wrong Realm' + ':bye'
93 ha1 = md5(a1).hexdigest()
94 a2 = 'GET:/digest'
95 ha2 = md5(a2).hexdigest()
96 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
97 auth_response = md5(a3).hexdigest()
98
99 response = self.client.get(
100 '/digest', headers={
101 'Authorization': 'Digest username="john",realm="{0}",'
102 'nonce="{1}",uri="/digest",response="{2}",'
103 'opaque="{3}"'.format(d['realm'],
104 d['nonce'],
105 auth_response,
106 d['opaque'])})
107 self.assertEqual(response.status_code, 401)
108 self.assertTrue('WWW-Authenticate' in response.headers)
109 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
110 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
111 response.headers['WWW-Authenticate']))
112
113 def test_digest_auth_login_invalid2(self):
114 response = self.client.get('/digest')
115 self.assertEqual(response.status_code, 401)
116 header = response.headers.get('WWW-Authenticate')
117 auth_type, auth_info = header.split(None, 1)
118 d = parse_dict_header(auth_info)
119
120 a1 = 'david:' + 'Authentication Required' + ':bye'
121 ha1 = md5(a1).hexdigest()
122 a2 = 'GET:/digest'
123 ha2 = md5(a2).hexdigest()
124 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
125 auth_response = md5(a3).hexdigest()
126
127 response = self.client.get(
128 '/digest', headers={
129 'Authorization': 'Digest username="david",realm="{0}",'
130 'nonce="{1}",uri="/digest",response="{2}",'
131 'opaque="{3}"'.format(d['realm'],
132 d['nonce'],
133 auth_response,
134 d['opaque'])})
135 self.assertEqual(response.status_code, 401)
136 self.assertTrue('WWW-Authenticate' in response.headers)
137 self.assertTrue(re.match(r'^Digest realm="Authentication Required",'
138 r'nonce="[0-9a-f]+",opaque="[0-9a-f]+"$',
139 response.headers['WWW-Authenticate']))
140
141 def test_digest_generate_ha1(self):
142 ha1 = self.digest_auth.generate_ha1('pawel', 'test')
143 ha1_expected = get_ha1('pawel', 'test', self.digest_auth.realm)
144 self.assertEqual(ha1, ha1_expected)
145
146 def test_digest_custom_nonce_checker(self):
147 @self.digest_auth.generate_nonce
148 def noncemaker():
149 return 'not a good nonce'
150
151 @self.digest_auth.generate_opaque
152 def opaquemaker():
153 return 'some opaque'
154
155 verify_nonce_called = []
156
157 @self.digest_auth.verify_nonce
158 def verify_nonce(provided_nonce):
159 verify_nonce_called.append(provided_nonce)
160 return True
161
162 verify_opaque_called = []
163
164 @self.digest_auth.verify_opaque
165 def verify_opaque(provided_opaque):
166 verify_opaque_called.append(provided_opaque)
167 return True
168
169 response = self.client.get('/digest')
170 self.assertEqual(response.status_code, 401)
171 header = response.headers.get('WWW-Authenticate')
172 auth_type, auth_info = header.split(None, 1)
173 d = parse_dict_header(auth_info)
174
175 self.assertEqual(d['nonce'], 'not a good nonce')
176 self.assertEqual(d['opaque'], 'some opaque')
177
178 a1 = 'john:' + d['realm'] + ':bye'
179 ha1 = md5(a1).hexdigest()
180 a2 = 'GET:/digest'
181 ha2 = md5(a2).hexdigest()
182 a3 = ha1 + ':' + d['nonce'] + ':' + ha2
183 auth_response = md5(a3).hexdigest()
184
185 response = self.client.get(
186 '/digest', headers={
187 'Authorization': 'Digest username="john",realm="{0}",'
188 'nonce="{1}",uri="/digest",response="{2}",'
189 'opaque="{3}"'.format(d['realm'],
190 d['nonce'],
191 auth_response,
192 d['opaque'])})
193 self.assertEqual(response.data, b'digest_auth:john')
194 self.assertEqual(verify_nonce_called, ['not a good nonce'],
195 "Should have verified the nonce.")
196 self.assertEqual(verify_opaque_called, ['some opaque'],
197 "Should have verified the opaque.")
00 [tox]
1 envlist=flake8,py36,py37,py38,py39,pypy3,docs
1 envlist=flake8,py36,py37,py38,py39,py310pypy3,docs
22 skip_missing_interpreters=True
33
44 [gh-actions]
55 python =
6 2.7: py27
76 3.6: py36
87 3.7: py37
98 3.8: py38
109 3.9: py39
11 pypy2: pypy2
12 pypy3: pypy3
10 3.10: py310
11 pypy-3: pypy3
1312
1413 [testenv]
1514 commands=