Skip to content

Commit

Permalink
Merge cf60e4e into 632db23
Browse files Browse the repository at this point in the history
  • Loading branch information
HerveMignot committed Sep 29, 2019
2 parents 632db23 + cf60e4e commit 0545761
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 8 deletions.
6 changes: 4 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
language: python
python:
- 2.6
- 2.7
- 3.3
- 3.5
- 3.6
- 3.7
- 3.8
- pypy
install:
- pip install .
Expand Down
20 changes: 20 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ just set ``BASIC_AUTH_FORCE`` configuration variable to `True`::
You might find this useful, for example, if you would like to protect your
staging server from uninvited guests.

To allow for multiple users, initialize ``app.config`` with a list of username & password dictionaries::

app = Flask(__name__)

app.config['BASIC_AUTH_USERS'] = [
{'USERNAME': 'john', 'PASSWORD': 'matrix'},
{'USERNAME': 'homer', 'PASSWORD': 'simpson'},
{'USERNAME': 'alan', 'PASSWORD': 'turing'},
]

basic_auth = BasicAuth(app)


.. warning::

Please make sure that you use SSL/TLS (HTTPS) to encrypt the connection
Expand Down Expand Up @@ -80,6 +93,13 @@ A list of configuration keys currently understood by the extension:
You can override :meth:`BasicAuth.check_credentials <flask.ext.basicauth.BasicAuth.check_credentials>`,
if you need a different authentication logic for your application.

``BASIC_AUTH_USERS``
Optionally, a list of dictionaries with ``USERNAME`` and ``PASSWORD`` keys that grant access for the
client to the protected resource.

Both ``BASIC_AUTH_USERNAME`` and ``BASIC_AUTH_USERS`` are checked if existing and then processed
accordingly.


API reference
-------------
Expand Down
27 changes: 23 additions & 4 deletions flask_basicauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from functools import wraps

from flask import current_app, request, Response
from werkzeug.security import safe_str_cmp


__version__ = '0.2.0'
Expand Down Expand Up @@ -56,16 +57,34 @@ def check_credentials(self, username, password):
By default compares the given username and password to
``BASIC_AUTH_USERNAME`` and ``BASIC_AUTH_PASSWORD``
configuration variables.
configuration variables or to a list of users in
``BASIC_AUTH_USERS`` (list of dictionaries USERNAME and PASSWORD).
:param username: a username provided by the client
:param password: a password provided by the client
:returns: `True` if the username and password combination was correct,
and `False` otherwise.
"""
correct_username = current_app.config['BASIC_AUTH_USERNAME']
correct_password = current_app.config['BASIC_AUTH_PASSWORD']
return username == correct_username and password == correct_password

if 'BASIC_AUTH_USERNAME' in current_app.config:
correct_username = current_app.config['BASIC_AUTH_USERNAME']
correct_password = current_app.config['BASIC_AUTH_PASSWORD']
username_matched = safe_str_cmp(username, correct_username)
password_matched = safe_str_cmp(password, correct_password)
if username_matched and password_matched:
return True

if 'BASIC_AUTH_USERS' in current_app.config:
for login in current_app.config['BASIC_AUTH_USERS']:
correct_username = login['USERNAME']
correct_password = login['PASSWORD']
username_matched = safe_str_cmp(username, correct_username)
password_matched = safe_str_cmp(password, correct_password)
if username_matched and password_matched:
return True

return False


def authenticate(self):
"""
Expand Down
138 changes: 136 additions & 2 deletions test_basicauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import unittest

from flask import Flask
from flask.ext.basicauth import BasicAuth
from flask_basicauth import BasicAuth


class BasicAuthTestCase(unittest.TestCase):
Expand Down Expand Up @@ -114,8 +114,142 @@ def test_runs_decorated_view_after_authentication(self):
)


class BasicAuthMultipleUsersTestCase(unittest.TestCase):

def assertIn(self, value, container):
self.assertTrue(value in container)

def setUp(self):
app = Flask(__name__)

app.config['BASIC_AUTH_USERS'] = [
{'USERNAME': 'john', 'PASSWORD': 'matrix'},
{'USERNAME': 'homer', 'PASSWORD': 'simpson'},
{'USERNAME': 'alan', 'PASSWORD': 'turing'},
]

basic_auth = BasicAuth(app)

@app.route('/')
def normal_view():
return 'This view does not normally require authentication.'

@app.route('/protected')
@basic_auth.required
def protected_view():
return 'This view always requires authentication.'

self.app = app
self.basic_auth = basic_auth
self.client = app.test_client()

def make_headers(self, username, password):
auth = base64.b64encode(username + b':' + password)
return {'Authorization': b'Basic ' + auth}

def test_sets_default_values_for_configuration(self):
self.assertEqual(self.app.config['BASIC_AUTH_REALM'], '')
self.assertEqual(self.app.config['BASIC_AUTH_FORCE'], False)

def test_views_without_basic_auth_decorator_respond_with_200(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)

def test_requires_authentication_for_all_views_when_forced(self):
self.app.config['BASIC_AUTH_FORCE'] = True
response = self.client.get('/')
self.assertEqual(response.status_code, 401)

def test_responds_with_401_without_authorization(self):
response = self.client.get('/protected')
self.assertEqual(response.status_code, 401)

def test_asks_for_authentication(self):
response = self.client.get('/protected')
self.assertIn('WWW-Authenticate', response.headers)
self.assertEqual(
response.headers['WWW-Authenticate'],
'Basic realm=""'
)

def test_asks_for_authentication_with_custom_realm(self):
self.app.config['BASIC_AUTH_REALM'] = 'Secure Area'
response = self.client.get('/protected')
self.assertIn('WWW-Authenticate', response.headers)
self.assertEqual(
response.headers['WWW-Authenticate'],
'Basic realm="Secure Area"'
)

def test_check_credentials_with_correct_credentials(self):
with self.app.test_request_context():
self.assertTrue(
self.basic_auth.check_credentials('john', 'matrix') and \
self.basic_auth.check_credentials('homer', 'simpson') and \
self.basic_auth.check_credentials('alan', 'turing')
)

def test_check_credentials_with_incorrect_credentials(self):
with self.app.test_request_context():
self.assertFalse(
self.basic_auth.check_credentials('john', 'rambo')
)

def test_responds_with_401_with_incorrect_credentials(self):
response = self.client.get(
'/protected',
headers=self.make_headers(b'john', b'rambo')
)
self.assertEqual(response.status_code, 401)

def test_responds_with_200_with_correct_credentials(self):
response = self.client.get(
'/protected',
headers=self.make_headers(b'john', b'matrix')
)
self.assertEqual(response.status_code, 200)

def test_responds_with_200_with_correct_credentials_containing_colon(self):
self.app.config['BASIC_AUTH_USERS'] = [
{'USERNAME': 'john', 'PASSWORD': 'matrix:'},
{'USERNAME': 'homer', 'PASSWORD': 'simpson:'},
{'USERNAME': 'alan', 'PASSWORD': 'turing:'},
]
response = self.client.get(
'/protected',
headers=self.make_headers(b'john', b'matrix:')
)
self.assertEqual(response.status_code, 200)

response = self.client.get(
'/protected',
headers=self.make_headers(b'homer', b'simpson:')
)
self.assertEqual(response.status_code, 200)

response = self.client.get(
'/protected',
headers=self.make_headers(b'alan', b'turing:')
)
self.assertEqual(response.status_code, 200)


def test_runs_decorated_view_after_authentication(self):
response = self.client.get(
'/protected',
headers=self.make_headers(b'john', b'matrix')
)
self.assertEqual(
response.data,
b'This view always requires authentication.'
)


def suite():
return unittest.makeSuite(BasicAuthTestCase)
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(BasicAuthTestCase))
suite.addTest(unittest.makeSuite(BasicAuthMultipleUsersTestCase))
return suite


if __name__ == '__main__':
Expand Down

0 comments on commit 0545761

Please sign in to comment.