Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for password hashes #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
author_email="[email protected]",
install_requires=[
'webob>=1.0.0',
'passlib>=1.7.1',
],
tests_require=tests_require,
extras_require={
Expand Down
16 changes: 12 additions & 4 deletions src/wsgi_basic_auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from base64 import b64decode

from passlib.context import CryptContext
from webob import Request
from webob.exc import HTTPUnauthorized

Expand All @@ -15,7 +16,8 @@ class BasicAuth(object):
:param users: dictionary with username -> password mapping. When not
supplied the values from the environment variable
``WSGI_AUTH_CREDENTIALS``. If no users are defined then
the middleware is disabled.
the middleware is disabled. Passwords can be hashed by
any scheme specified in ``hash_schemes`` parameter.

:param exclude_paths: list of path prefixes to exclude from auth. When not
supplied the values from the ``WSGI_AUTH_EXCLUDE_PATHS``
Expand All @@ -24,11 +26,13 @@ class BasicAuth(object):
supplied the values from the ``WSGI_AUTH_PATHS``
environment variable are used (splitted by ``;``)
:param env_prefix: prefix for the environment variables above, default ``''``

:param hash_schemes: list of password hash schemes to validate passwords.
Any schemes available in ``passlib`` library can be used.
Defaults to ['plaintext'] for backward compatibility.
"""

def __init__(self, app, realm='Protected', users=None, exclude_paths=None,
include_paths=None, env_prefix=''):
include_paths=None, env_prefix='', hash_schemes=['plaintext']):
self._app = app
self._env_prefix = env_prefix
self._realm = realm
Expand All @@ -37,6 +41,7 @@ def __init__(self, app, realm='Protected', users=None, exclude_paths=None,
exclude_paths or _exclude_paths_from_environ(env_prefix))
self._include_paths = set(
include_paths or _include_paths_from_environ(env_prefix))
self._crypt_context = CryptContext(schemes=hash_schemes)

def __call__(self, environ, start_response):
if self._users:
Expand All @@ -61,12 +66,15 @@ def is_authorized(self, request):
if auth and auth[0] == 'Basic':
credentials = b64decode(auth[1]).decode('UTF-8')
username, password = credentials.split(':', 1)
return self._users.get(username) == password
return self._verify_password(password, self._users.get(username))
else:
return False
else:
return True

def _verify_password(self, password, hash_):
return self._crypt_context.verify(password, hash_)

def _login(self, environ, start_response):
"""Send a login response back to the client."""
response = HTTPUnauthorized()
Expand Down
13 changes: 13 additions & 0 deletions tests/test_wsgi_basic_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,16 @@ def test_include_paths_from_environ_multiple(monkeypatch):
monkeypatch.setenv('WSGI_AUTH_PATHS', '/foo/bar;/bar/foo')
result = wsgi_basic_auth._include_paths_from_environ()
assert result == ['/foo/bar', '/bar/foo']


def test_auth_hash(monkeypatch):
monkeypatch.setenv('WSGI_AUTH_CREDENTIALS', 'foo:$apr1$yIPw8J/l$0mPnNW9gbONm8oUgO5EUX1') # foo:bar
application = wsgi_basic_auth.BasicAuth(wsgi_app, hash_schemes=['apr_md5_crypt'])
app = TestApp(application)
app.get('/', status=401)

app.authorization = ('Basic', ('foo', 'baz'))
app.get('/', status=401)

app.authorization = ('Basic', ('foo', 'bar'))
app.get('/', status=200)