From 044b7d4a44425a4b9d02280b80988e8986641a0d Mon Sep 17 00:00:00 2001 From: Henrique Carvalho Alves Date: Mon, 7 Apr 2014 17:56:10 -0300 Subject: [PATCH] Ignore authentication headers for OPTIONS We need to ignore authentication headers for OPTIONS to avoid unwanted interactions with CORS. Chrome and Firefox issue a preflight OPTIONS request to check Access-Control-* headers, and will fail if it returns 401. https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS#Overview --- AUTHORS | 2 ++ flask_httpauth.py | 17 +++++++++++------ test_httpauth.py | 42 ++++++++++++++++++++++++++---------------- 3 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..972fa1c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Miguel Grinberg +Henrique Carvalho Alves diff --git a/flask_httpauth.py b/flask_httpauth.py index 608a821..fbeb0af 100644 --- a/flask_httpauth.py +++ b/flask_httpauth.py @@ -45,11 +45,16 @@ def login_required(self, f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization - if not auth: - return self.auth_error_callback() - password = self.get_password_callback(auth.username) - if not self.authenticate(auth, password): - return self.auth_error_callback() + # We need to ignore authentication headers for OPTIONS to avoid + # unwanted interactions with CORS. + # Chrome and Firefox issue a preflight OPTIONS request to check + # Access-Control-* headers, and will fail if it returns 401. + if request.method != 'OPTIONS': + if not auth: + return self.auth_error_callback() + password = self.get_password_callback(auth.username) + if not self.authenticate(auth, password): + return self.auth_error_callback() return f(*args, **kwargs) return decorated @@ -93,7 +98,7 @@ def __init__(self): def get_nonce(self): return md5(str(self.random.random()).encode('utf-8')).hexdigest() - + def authenticate_header(self): session["auth_nonce"] = self.get_nonce() session["auth_opaque"] = self.get_nonce() diff --git a/test_httpauth.py b/test_httpauth.py index dce1c2c..8e787cf 100644 --- a/test_httpauth.py +++ b/test_httpauth.py @@ -24,7 +24,7 @@ def setUp(self): digest_auth = HTTPDigestAuth() digest_auth_my_realm = HTTPDigestAuth() digest_auth_my_realm.realm = 'My Realm' - + @basic_auth.get_password def get_basic_password(username): if username == 'john': @@ -80,7 +80,7 @@ def get_digest_password(username): return 'bye' else: return None - + @digest_auth_my_realm.get_password def get_digest_password(username): if username == 'susan': @@ -89,16 +89,16 @@ def get_digest_password(username): return 'bye' else: return None - + @app.route('/') def index(): return 'index' - + @app.route('/basic') @basic_auth.login_required def basic_auth_route(): return 'basic_auth:' + basic_auth.username() - + @app.route('/basic-with-realm') @basic_auth_my_realm.login_required def basic_auth_my_realm_route(): @@ -118,7 +118,7 @@ def basic_verify_auth_route(): @digest_auth.login_required def digest_auth_route(): return 'digest_auth:' + digest_auth.username() - + @app.route('/digest-with-realm') @digest_auth_my_realm.login_required def digest_auth_my_realm_route(): @@ -131,7 +131,7 @@ def digest_auth_my_realm_route(): self.basic_verify_auth = basic_verify_auth self.digest_auth = digest_auth self.client = app.test_client() - + def test_no_auth(self): response = self.client.get('/') self.assertTrue(response.data.decode('utf-8') == 'index') @@ -142,6 +142,11 @@ def test_basic_auth_prompt(self): self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(response.headers['WWW-Authenticate'] == 'Basic realm="Authentication Required"') + def test_basic_auth_ignore_options(self): + response = self.client.options('/basic') + self.assertTrue(response.status_code == 200) + self.assertTrue('WWW-Authenticate' not in response.headers) + def test_basic_auth_prompt_with_custom_realm(self): response = self.client.get('/basic-with-realm') self.assertTrue(response.status_code == 401) @@ -150,17 +155,17 @@ def test_basic_auth_prompt_with_custom_realm(self): self.assertTrue(response.data.decode('utf-8') == 'custom error') def test_basic_auth_login_valid(self): - response = self.client.get('/basic', + response = self.client.get('/basic', headers = { 'Authorization': 'Basic ' + base64.b64encode(b'john:hello').decode('utf-8').strip('\r\n') }) self.assertTrue(response.data.decode('utf-8') == 'basic_auth:john') def test_basic_auth_login_valid_with_hash1(self): - response = self.client.get('/basic-custom', + response = self.client.get('/basic-custom', headers = { 'Authorization': 'Basic ' + base64.b64encode(b'john:hello').decode('utf-8').strip('\r\n') }) self.assertTrue(response.data.decode('utf-8') == 'basic_custom_auth:john') - + def test_basic_auth_login_valid_with_hash2(self): - response = self.client.get('/basic-with-realm', + response = self.client.get('/basic-with-realm', headers = { 'Authorization': 'Basic ' + base64.b64encode(b'john:hello').decode('utf-8').strip('\r\n') }) self.assertTrue(response.data.decode('utf-8') == 'basic_auth_my_realm:john') @@ -199,6 +204,11 @@ def test_digest_auth_prompt(self): self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="Authentication Required",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.assertTrue(response.status_code == 200) + self.assertTrue('WWW-Authenticate' not in response.headers) + def test_digest_auth_prompt_with_custom_realm(self): response = self.client.get('/digest-with-realm') self.assertTrue(response.status_code == 401) @@ -219,7 +229,7 @@ def test_digest_auth_login_valid(self): a3 = ha1 + ':' + d['nonce'] + ':' + ha2 auth_response = md5(a3).hexdigest() - response = self.client.get('/digest', + response = self.client.get('/digest', headers = { 'Authorization': 'Digest username="john",realm="' + d['realm'] + '",nonce="' + d['nonce'] + '",uri="/digest",response="' + auth_response + '",opaque="' + d['opaque'] + '"' }) self.assertTrue(response.data == b'digest_auth:john') @@ -237,19 +247,19 @@ def test_digest_auth_login_bad_realm(self): a3 = ha1 + ':' + d['nonce'] + ':' + ha2 auth_response = md5(a3).hexdigest() - response = self.client.get('/digest', + response = self.client.get('/digest', headers = { 'Authorization': 'Digest username="john",realm="' + d['realm'] + '",nonce="' + d['nonce'] + '",uri="/digest",response="' + auth_response + '",opaque="' + d['opaque'] + '"' }) self.assertTrue(response.status_code == 401) self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="Authentication Required",nonce="[0-9a-f]+",opaque="[0-9a-f]+"$', response.headers['WWW-Authenticate'])) - + def test_digest_auth_login_invalid(self): - response = self.client.get('/digest-with-realm', + response = self.client.get('/digest-with-realm', headers = { "Authorization": 'Digest username="susan",realm="My Realm",nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",uri="/digest-with-realm",response="ca306c361a9055b968810067a37fb8cb",opaque="5ccc069c403ebaf9f0171e9517f40e41"' }) self.assertTrue(response.status_code == 401) self.assertTrue('WWW-Authenticate' in response.headers) self.assertTrue(re.match(r'^Digest realm="My Realm",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.assertTrue(response.status_code == 401)