Skip to content

Commit

Permalink
Ignore authentication headers for OPTIONS
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hcarvalhoalves committed Apr 7, 2014
1 parent 9db40a9 commit 044b7d4
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 22 deletions.
2 changes: 2 additions & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Miguel Grinberg <miguelgrinberg50@gmail.com>
Henrique Carvalho Alves <hcarvalhoalves@gmail.com>
17 changes: 11 additions & 6 deletions flask_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
42 changes: 26 additions & 16 deletions test_httpauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down Expand Up @@ -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':
Expand All @@ -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():
Expand All @@ -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():
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -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')

Expand Down Expand Up @@ -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)
Expand All @@ -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')

Expand All @@ -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)
Expand Down

0 comments on commit 044b7d4

Please sign in to comment.