From f8a9c5638490a7d79b9d1d9ad3b9b44a6a98b5ec Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Thu, 22 Jun 2017 16:47:13 +0200 Subject: [PATCH 1/7] Add new location where digest authentication reports stale of nonce - first request with proper credentials is rejected with stale=true and cookie is set to be able to detect that request was resent with new nonce value - second request with proper credential is accepted since contains respective cookie value as response cookie value is changed so next request will be rejected with stale=true again --- httpbin/core.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/httpbin/core.py b/httpbin/core.py index 5840850..caccad3 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -482,6 +482,75 @@ def digest_auth(qop=None, user='user', passwd='passwd', algorithm='MD5'): return response return jsonify(authenticated=True, user=user) +@app.route('/digest-auth-stale////') +def digest_auth_stale(qop=None, user='user', passwd='passwd', algorithm='MD5'): + """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 \ + not check_digest_auth(user, passwd) or \ + 'Cookie' not in request.headers: + response = app.make_response('') + response.status_code = 401 + + # RFC2616 Section4.2: HTTP headers are ASCII. That means + # request.remote_addr was originally ASCII, so I should be able to + # encode it back to ascii. Also, RFC2617 says about nonces: "The + # contents of the nonce are implementation dependent" + nonce = H(b''.join([ + getattr(request,'remote_addr',u'').encode('ascii'), + b':', + str(time.time()).encode('ascii'), + b':', + 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, ), algorithm=algorithm) + response.headers['WWW-Authenticate'] = auth.to_header() + response.headers['Set-Cookie'] = 'stale=no; Path=/' + return response + + if not 'stale=yes' in request.headers['Cookie'] : + response = app.make_response('') + response.status_code = 401 + + # RFC2616 Section4.2: HTTP headers are ASCII. That means + # request.remote_addr was originally ASCII, so I should be able to + # encode it back to ascii. Also, RFC2617 says about nonces: "The + # contents of the nonce are implementation dependent" + nonce = H(b''.join([ + getattr(request,'remote_addr',u'').encode('ascii'), + b':', + str(time.time()).encode('ascii'), + b':', + 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, ), algorithm=algorithm) + auth.stale = True; + response.headers['WWW-Authenticate'] = auth.to_header() + response.headers['Set-Cookie'] = 'stale=yes; Path=/' + + return response + + response = jsonify(authenticated=True, user=user) + response.headers['Set-Cookie'] = 'stale=no; Path=/' + + return response + @app.route('/delay/') def delay_response(delay): From 791e31cba4a4545352fa7d6e2d7019910166160a Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Sun, 25 Jun 2017 19:59:18 +0200 Subject: [PATCH 2/7] Move stale functionality to main digest url - now digest url will contain number of request after which request with proper credentials will be rejected since nonce value expiries --- httpbin/core.py | 121 ++++++++----------------------- httpbin/helpers.py | 36 +++++++++ httpbin/templates/httpbin.1.html | 4 +- 3 files changed, 67 insertions(+), 94 deletions(-) diff --git a/httpbin/core.py b/httpbin/core.py index caccad3..e697cac 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -24,7 +24,9 @@ from werkzeug.wrappers import BaseResponse from raven.contrib.flask import Sentry from . import filters -from .helpers import get_headers, status_code, get_dict, get_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII, parse_multi_value_header +from .helpers import get_headers, status_code, get_dict, get_request_range, check_basic_auth, check_digest_auth, \ + secure_cookie, H, ROBOT_TXT, ANGRY_ASCII, parse_multi_value_header, next_stale_after_value, \ + digest_challenge_response from .utils import weighted_choice from .structures import CaseInsensitiveDict @@ -446,110 +448,45 @@ def hidden_basic_auth(user='user', passwd='passwd'): @app.route('/digest-auth///') def digest_auth_md5(qop=None, user='user', passwd='passwd'): - return digest_auth(qop, user, passwd, "MD5") + return digest_auth(qop, user, passwd, "MD5", 'never') + @app.route('/digest-auth////') -def digest_auth(qop=None, user='user', passwd='passwd', algorithm='MD5'): +def digest_auth_nostale(qop=None, user='user', passwd='passwd', algorithm='MD5'): + return digest_auth(qop, user, passwd, algorithm, 'never') + + +@app.route('/digest-auth/////') +def digest_auth(qop=None, user='user', passwd='passwd', algorithm='MD5', stale_after='never'): """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 \ - not check_digest_auth(user, passwd) or \ - 'Cookie' not in request.headers: - response = app.make_response('') - response.status_code = 401 - # RFC2616 Section4.2: HTTP headers are ASCII. That means - # request.remote_addr was originally ASCII, so I should be able to - # encode it back to ascii. Also, RFC2617 says about nonces: "The - # contents of the nonce are implementation dependent" - nonce = H(b''.join([ - getattr(request,'remote_addr',u'').encode('ascii'), - b':', - str(time.time()).encode('ascii'), - b':', - 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, ), algorithm=algorithm) - response.headers['WWW-Authenticate'] = auth.to_header() - response.headers['Set-Cookie'] = 'fake=fake_value' + if 'Authorization' not in request.headers or \ + not check_digest_auth(user, passwd) or \ + 'Cookie' not in request.headers: + response = digest_challenge_response(app, qop, algorithm) + response.set_cookie('stale_after', value=stale_after) return response - return jsonify(authenticated=True, user=user) -@app.route('/digest-auth-stale////') -def digest_auth_stale(qop=None, user='user', passwd='passwd', algorithm='MD5'): - """Prompts the user for authorization using HTTP Digest auth""" + if 'stale_after' in request.cookies: + stale_after_value = request.cookies.get('stale_after') - if algorithm not in ('MD5', 'SHA-256'): - algorithm = 'MD5' + if stale_after_value == '0': + response = digest_challenge_response(app, qop, algorithm, True) + response.set_cookie('stale_after', value=stale_after) + return response - if qop not in ('auth', 'auth-int'): - qop = None + response = jsonify(authenticated=True, user=user) + response.set_cookie('stale_after', value=next_stale_after_value(stale_after_value)) + return response - if 'Authorization' not in request.headers or \ - not check_digest_auth(user, passwd) or \ - 'Cookie' not in request.headers: - response = app.make_response('') - response.status_code = 401 - - # RFC2616 Section4.2: HTTP headers are ASCII. That means - # request.remote_addr was originally ASCII, so I should be able to - # encode it back to ascii. Also, RFC2617 says about nonces: "The - # contents of the nonce are implementation dependent" - nonce = H(b''.join([ - getattr(request,'remote_addr',u'').encode('ascii'), - b':', - str(time.time()).encode('ascii'), - b':', - 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, ), algorithm=algorithm) - response.headers['WWW-Authenticate'] = auth.to_header() - response.headers['Set-Cookie'] = 'stale=no; Path=/' - return response - - if not 'stale=yes' in request.headers['Cookie'] : - response = app.make_response('') - response.status_code = 401 - - # RFC2616 Section4.2: HTTP headers are ASCII. That means - # request.remote_addr was originally ASCII, so I should be able to - # encode it back to ascii. Also, RFC2617 says about nonces: "The - # contents of the nonce are implementation dependent" - nonce = H(b''.join([ - getattr(request,'remote_addr',u'').encode('ascii'), - b':', - str(time.time()).encode('ascii'), - b':', - 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, ), algorithm=algorithm) - auth.stale = True; - response.headers['WWW-Authenticate'] = auth.to_header() - response.headers['Set-Cookie'] = 'stale=yes; Path=/' - - return response - - response = jsonify(authenticated=True, user=user) - response.headers['Set-Cookie'] = 'stale=no; Path=/' - - return response + response = jsonify(authenticated=True, user=user) + response.set_cookie('stale_after', value=stale_after) + return response @app.route('/delay/') diff --git a/httpbin/helpers.py b/httpbin/helpers.py index c838fff..6f84f50 100644 --- a/httpbin/helpers.py +++ b/httpbin/helpers.py @@ -10,8 +10,11 @@ This module provides helper functions for httpbin. import json import base64 import re +import time +import os from hashlib import md5, sha256 from werkzeug.http import parse_authorization_header +from werkzeug.datastructures import WWWAuthenticate from flask import request, make_response from six.moves.urllib.parse import urlparse, urlunparse @@ -432,3 +435,36 @@ def parse_multi_value_header(header_str): if match is not None: parsed_parts.append(match.group(2)) return parsed_parts + + +def next_stale_after_value(stale_after): + try: + stal_after_count = int(stale_after) - 1 + return str(stal_after_count) + except ValueError: + return 'never' + + +def digest_challenge_response(app, qop, algorithm, stale = False): + response = app.make_response('') + response.status_code = 401 + + # RFC2616 Section4.2: HTTP headers are ASCII. That means + # request.remote_addr was originally ASCII, so I should be able to + # encode it back to ascii. Also, RFC2617 says about nonces: "The + # contents of the nonce are implementation dependent" + nonce = H(b''.join([ + getattr(request, 'remote_addr', u'').encode('ascii'), + b':', + str(time.time()).encode('ascii'), + b':', + 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,), algorithm=algorithm) + auth.stale = stale + response.headers['WWW-Authenticate'] = auth.to_header() + return response diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index 0a50477..4e51fa5 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -38,8 +38,8 @@
  • /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/:algorithm Challenges HTTP Digest Auth.
  • -
  • /digest-auth/:qop/:user/:passwd Challenges HTTP Digest Auth.
  • +
  • /digest-auth/:qop/:user/:passwd/:algorithm Challenges HTTP Digest Auth.
  • +
  • /digest-auth/:qop/:user/:passwd Challenges HTTP Digest Auth.
  • /stream/:n Streams min(n, 100) lines.
  • /delay/:n Delays responding for min(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.
  • From 8f93e11699bdb076edf265872a15e26d8abcc02f Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Thu, 6 Jul 2017 08:14:03 +0200 Subject: [PATCH 3/7] Improve when stale is reported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - when authentication fails cookie „last_nonce” is set, this cookie is used to check that next authentication is not used for next authentication attempt. If it is challenge response is send with stale=true flag. --- httpbin/core.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/httpbin/core.py b/httpbin/core.py index e697cac..2171f0a 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -21,6 +21,7 @@ from six.moves import range as xrange from werkzeug.datastructures import WWWAuthenticate, MultiDict from werkzeug.http import http_date from werkzeug.wrappers import BaseResponse +from werkzeug.http import parse_authorization_header from raven.contrib.flask import Sentry from . import filters @@ -466,26 +467,40 @@ def digest_auth(qop=None, user='user', passwd='passwd', algorithm='MD5', stale_a qop = None if 'Authorization' not in request.headers or \ - not check_digest_auth(user, passwd) or \ - 'Cookie' not in request.headers: + 'Cookie' not in request.headers: response = digest_challenge_response(app, qop, algorithm) response.set_cookie('stale_after', value=stale_after) return response - if 'stale_after' in request.cookies: + credentails = parse_authorization_header(request.headers.get('Authorization')) + if not credentails : + response = digest_challenge_response(app, qop, algorithm) + response.set_cookie('stale_after', value=stale_after) + return response + + current_nonce = credentails.get('nonce') + + stale_after_value = None + if 'stale_after' in request.cookies : stale_after_value = request.cookies.get('stale_after') - if stale_after_value == '0': - response = digest_challenge_response(app, qop, algorithm, True) - response.set_cookie('stale_after', value=stale_after) - return response + if 'last_nonce' in request.cookies and current_nonce == request.cookies.get('last_nonce') or \ + stale_after_value == '0' : + response = digest_challenge_response(app, qop, algorithm, True) + response.set_cookie('stale_after', value=stale_after) + response.set_cookie('last_nonce', value=current_nonce) + return response - response = jsonify(authenticated=True, user=user) - response.set_cookie('stale_after', value=next_stale_after_value(stale_after_value)) + if not check_digest_auth(user, passwd) : + response = digest_challenge_response(app, qop, algorithm, False) + response.set_cookie('stale_after', value=stale_after) + response.set_cookie('last_nonce', value=current_nonce) return response response = jsonify(authenticated=True, user=user) - response.set_cookie('stale_after', value=stale_after) + if stale_after_value : + response.set_cookie('stale_after', value=next_stale_after_value(stale_after_value)) + return response From 0c968cbd35f2fb7d6e32b1de18b2631a70c26aab Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Thu, 6 Jul 2017 23:24:44 +0200 Subject: [PATCH 4/7] Refactor digest_auth test to be able add missing test cases in next commits --- test_httpbin.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test_httpbin.py b/test_httpbin.py index 56f374f..30030d5 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -286,7 +286,7 @@ class HttpbinTestCase(unittest.TestCase): for body in None, b'', b'request payload': self._test_digest_auth(username, password, qop, algorithm, body) - def _test_digest_auth(self, username, password, qop=None, algorithm=None, body=None): + def _test_digest_auth(self, username, password, qop, algorithm=None, body=None): uri = '/digest-auth/{0}/{1}/{2}'.format(qop or 'wrong-qop', username, password) if algorithm: uri += '/' + algorithm @@ -300,22 +300,34 @@ class HttpbinTestCase(unittest.TestCase): ) # make sure it returns a 401 self.assertEqual(unauthorized_response.status_code, 401) + header = unauthorized_response.headers.get('WWW-Authenticate') + + authorized_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, body) + + # done! + self.assertEqual(authorized_response.status_code, 200) + + def _test_digest_response_for_auth_request(self, header, username, password, qop, uri, body, nonce=None): auth_type, auth_info = header.split(None, 1) self.assertEqual(auth_type, 'Digest') d = parse_dict_header(auth_info) - nonce = d['nonce'] + nonce = nonce or d['nonce'] realm = d['realm'] opaque = d['opaque'] + if qop : + self.assertIn(qop, d['qop'].split(', '), 'Challenge should contains expected qop') + algorithm = d['algorithm'] + cnonce, nc = (_hash(os.urandom(10), "MD5"), '00000001') if qop in ('auth', 'auth-int') else (None, None) auth_header = _make_digest_auth_header( username, password, 'GET', uri, nonce, realm, opaque, algorithm, qop, cnonce, nc, body) # make second request - authorized_response = self.app.get( + return self.app.get( uri, environ_base={ # httpbin's digest auth implementation uses the remote addr to @@ -328,9 +340,6 @@ class HttpbinTestCase(unittest.TestCase): data=body ) - # done! - self.assertEqual(authorized_response.status_code, 200) - def test_drip(self): response = self.app.get('/drip?numbytes=400&duration=2&delay=1') self.assertEqual(response.content_length, 400) From 7e8015237d23ea5daddf2b1e47bca1eac47b83ea Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Sat, 8 Jul 2017 10:59:21 +0200 Subject: [PATCH 5/7] =?UTF-8?q?Add=20test=20to=20verify=20=E2=80=9Estale?= =?UTF-8?q?=20after=E2=80=9D=20scenario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test_httpbin.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/test_httpbin.py b/test_httpbin.py index 30030d5..a5a7ee9 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -284,13 +284,17 @@ class HttpbinTestCase(unittest.TestCase): for qop in None, 'auth', 'auth-int',: for algorithm in None, 'MD5', 'SHA-256': for body in None, b'', b'request payload': - self._test_digest_auth(username, password, qop, algorithm, body) + for stale_after in (None, 1, 4) if algorithm else (None,) : + self._test_digest_auth(username, password, qop, algorithm, body, stale_after) - def _test_digest_auth(self, username, password, qop, algorithm=None, body=None): + def _test_digest_auth(self, username, password, qop, algorithm=None, body=None, stale_after=None): uri = '/digest-auth/{0}/{1}/{2}'.format(qop or 'wrong-qop', username, password) if algorithm: uri += '/' + algorithm + if stale_after: + uri += '/{0}'.format(stale_after) + unauthorized_response = self.app.get( uri, environ_base={ @@ -304,11 +308,24 @@ class HttpbinTestCase(unittest.TestCase): header = unauthorized_response.headers.get('WWW-Authenticate') authorized_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, body) - - # done! self.assertEqual(authorized_response.status_code, 200) - def _test_digest_response_for_auth_request(self, header, username, password, qop, uri, body, nonce=None): + if None == stale_after : + return + + # test stale after scenerio + for nc in range(2, stale_after + 1) : + authorized_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ + body, nc) + self.assertEqual(authorized_response.status_code, 200) + + stale_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ + body, stale_after + 1) + self.assertEqual(stale_response.status_code, 401) + header = stale_response.headers.get('WWW-Authenticate') + self.assertIn('stale=TRUE', header) + + def _test_digest_response_for_auth_request(self, header, username, password, qop, uri, body, nc=1, nonce=None): auth_type, auth_info = header.split(None, 1) self.assertEqual(auth_type, 'Digest') @@ -321,7 +338,7 @@ class HttpbinTestCase(unittest.TestCase): self.assertIn(qop, d['qop'].split(', '), 'Challenge should contains expected qop') algorithm = d['algorithm'] - cnonce, nc = (_hash(os.urandom(10), "MD5"), '00000001') if qop in ('auth', 'auth-int') else (None, None) + cnonce, nc = (_hash(os.urandom(10), "MD5"), '{:08}'.format(nc)) if qop in ('auth', 'auth-int') else (None, None) auth_header = _make_digest_auth_header( username, password, 'GET', uri, nonce, realm, opaque, algorithm, qop, cnonce, nc, body) From 477721960c08bd9c621ad196f4facd8cc78a6bcc Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Sat, 8 Jul 2017 11:27:26 +0200 Subject: [PATCH 6/7] Add test for wrong password and expired (reused) nonce value --- test_httpbin.py | 73 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/test_httpbin.py b/test_httpbin.py index a5a7ee9..40a2a04 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -288,13 +288,22 @@ class HttpbinTestCase(unittest.TestCase): self._test_digest_auth(username, password, qop, algorithm, body, stale_after) def _test_digest_auth(self, username, password, qop, algorithm=None, body=None, stale_after=None): - uri = '/digest-auth/{0}/{1}/{2}'.format(qop or 'wrong-qop', username, password) - if algorithm: - uri += '/' + algorithm + uri = self._digest_auth_create_uri(username, password, qop, algorithm, stale_after) - if stale_after: - uri += '/{0}'.format(stale_after) + unauthorized_response = self._test_digest_auth_first_challenge(uri) + header = unauthorized_response.headers.get('WWW-Authenticate') + + authorized_response, nonce = self._test_digest_response_for_auth_request(header, username, password, qop, uri, body) + self.assertEqual(authorized_response.status_code, 200) + + if None == stale_after : + return + + # test stale after scenerio + self._digest_auth_stale_after_check(header, username, password, uri, body, qop, stale_after) + + def _test_digest_auth_first_challenge(self, uri): unauthorized_response = self.app.get( uri, environ_base={ @@ -304,23 +313,23 @@ class HttpbinTestCase(unittest.TestCase): ) # make sure it returns a 401 self.assertEqual(unauthorized_response.status_code, 401) + return unauthorized_response - header = unauthorized_response.headers.get('WWW-Authenticate') + def _digest_auth_create_uri(self, username, password, qop, algorithm, stale_after): + uri = '/digest-auth/{0}/{1}/{2}'.format(qop or 'wrong-qop', username, password) + if algorithm: + uri += '/' + algorithm + if stale_after: + uri += '/{0}'.format(stale_after) + return uri - authorized_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, body) - self.assertEqual(authorized_response.status_code, 200) - - if None == stale_after : - return - - # test stale after scenerio - for nc in range(2, stale_after + 1) : - authorized_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ + def _digest_auth_stale_after_check(self, header, username, password, uri, body, qop, stale_after): + for nc in range(2, stale_after + 1): + authorized_response, nonce = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ body, nc) self.assertEqual(authorized_response.status_code, 200) - - stale_response = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ - body, stale_after + 1) + stale_response, nonce = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ + body, stale_after + 1) self.assertEqual(stale_response.status_code, 401) header = stale_response.headers.get('WWW-Authenticate') self.assertIn('stale=TRUE', header) @@ -355,7 +364,33 @@ class HttpbinTestCase(unittest.TestCase): 'Authorization': auth_header, }, data=body - ) + ), nonce + + def test_digest_auth_wrong_pass(self): + """Test different combinations of digest auth parameters""" + username = 'user' + password = 'passwd' + for qop in None, 'auth', 'auth-int',: + for algorithm in None, 'MD5', 'SHA-256': + for body in None, b'', b'request payload': + self._test_digest_auth_wrong_pass(username, password, qop, algorithm, body, 3) + + def _test_digest_auth_wrong_pass(self, username, password, qop, algorithm=None, body=None, stale_after=None): + uri = self._digest_auth_create_uri(username, password, qop, algorithm, stale_after) + unauthorized_response = self._test_digest_auth_first_challenge(uri) + + header = unauthorized_response.headers.get('WWW-Authenticate') + + wrong_pass_response, nonce = self._test_digest_response_for_auth_request(header, username, "wrongPassword", qop, uri, body) + self.assertEqual(wrong_pass_response.status_code, 401) + header = wrong_pass_response.headers.get('WWW-Authenticate') + self.assertNotIn('stale=TRUE', header) + + reused_nonce_response, nonce = self._test_digest_response_for_auth_request(header, username, password, qop, uri, \ + body, nonce=nonce) + self.assertEqual(reused_nonce_response.status_code, 401) + header = reused_nonce_response.headers.get('WWW-Authenticate') + self.assertIn('stale=TRUE', header) def test_drip(self): response = self.app.get('/drip?numbytes=400&duration=2&delay=1') From 84edf61ad61ccd4bb7ecdf47fdeeffd49014cb73 Mon Sep 17 00:00:00 2001 From: Marek Ruszczak Date: Sun, 9 Jul 2017 19:26:01 +0200 Subject: [PATCH 7/7] Split and strip qop. --- test_httpbin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_httpbin.py b/test_httpbin.py index 40a2a04..a3ad77c 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -344,7 +344,7 @@ class HttpbinTestCase(unittest.TestCase): realm = d['realm'] opaque = d['opaque'] if qop : - self.assertIn(qop, d['qop'].split(', '), 'Challenge should contains expected qop') + self.assertIn(qop, [x.strip() for x in d['qop'].split(',')], 'Challenge should contains expected qop') algorithm = d['algorithm'] cnonce, nc = (_hash(os.urandom(10), "MD5"), '{:08}'.format(nc)) if qop in ('auth', 'auth-int') else (None, None)