Skip to content

Commit

Permalink
feat: additional authenticated_callback on login decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
simoncoulton committed Jun 28, 2017
1 parent 32efb03 commit 8d6a6e5
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 54 deletions.
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -72,7 +72,7 @@ def confirm(prompt):
long_description=readme,

author='Simon Coulton',
author_email='simon at bespohk.com',
author_email='simon@bespohk.com',

license=license,
classifiers=[
Expand Down
31 changes: 30 additions & 1 deletion tests/watson/auth/test_decorators.py
Expand Up @@ -7,7 +7,7 @@
from watson.framework import controllers, exceptions
from tests.watson.auth import support
from watson.auth.decorators import auth, login, logout, forgotten, reset
from watson.auth import authentication, managers
from watson.auth import managers
from watson.validators import abc


Expand All @@ -32,6 +32,14 @@ def __call__(self, value):
return False


def authenticated_callback_success(user, request):
return True


def authenticated_callback_failure(user, request):
return False


class SampleController(controllers.Action):

@auth
Expand Down Expand Up @@ -62,6 +70,14 @@ def unauthed_url_redirect(self):
def login_redirect_action(self, form):
return 'login'

@login(authenticated_callback=authenticated_callback_success)
def login_authenticated_callback_success(self):
return 'success'

@login(authenticated_callback=authenticated_callback_failure)
def login_authenticated_callback_failure(self):
return 'success'

@auth(roles='admin', unauthorized_url='/unauthorized-test')
def unauthorized_custom_url_action(self):
pass
Expand Down Expand Up @@ -162,6 +178,19 @@ def test_login_redirect_to_referrer(self):
response = self.controller.login_redirect_action()
assert response.headers['location'] == 'http://127.0.0.1/existing-url?to-here&and-here'

def test_authenticated_callback(self):
post_data = 'username=admin&password=test'
environ = sample_environ(REQUEST_METHOD='POST',
CONTENT_LENGTH=len(post_data))
environ['wsgi.input'] = BufferedReader(
BytesIO(post_data.encode('utf-8')))
self.controller.request = Request.from_environ(environ, 'watson.http.sessions.Memory')
self.controller.login_authenticated_callback_failure()
assert len(self.controller.flash_messages)
self.controller.flash_messages.clear()
self.controller.login_authenticated_callback_success()
assert not len(self.controller.flash_messages)


class TestLogout(object):
def setup(self):
Expand Down
2 changes: 1 addition & 1 deletion watson/auth/__init__.py
@@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
__version__ = '3.3.0'
__version__ = '3.4.4'
7 changes: 6 additions & 1 deletion watson/auth/authentication.py
Expand Up @@ -16,7 +16,8 @@ def generate_password(password, rounds=10, encoding='utf-8'):
mixed: The generated password and the salt used
"""
salt = bcrypt.gensalt(rounds)
return bcrypt.hashpw(password.encode(encoding), salt), salt
hashed_password = bcrypt.hashpw(password.encode(encoding), salt)
return hashed_password.decode(encoding), salt.decode(encoding)


def check_password(password, existing_password, salt, encoding='utf-8'):
Expand All @@ -31,6 +32,10 @@ def check_password(password, existing_password, salt, encoding='utf-8'):
Returns:
boolean: True/False if valid or invalid
"""
if isinstance(salt, str):
salt = salt.encode(encoding)
if isinstance(existing_password, str):
existing_password = existing_password.encode(encoding)
return bcrypt.hashpw(password.encode(encoding), salt) == existing_password


Expand Down
53 changes: 32 additions & 21 deletions watson/auth/decorators.py
Expand Up @@ -85,19 +85,29 @@ def wrapper(self, *args, **kwargs):
return decorator


def login(func=None, method='POST', form_class=None, auto_redirect=True):
def login(
func=None,
method='POST',
form_class=None,
auto_redirect=True,
authenticated_callback=None):
"""Attempts to authenticate a user if the required fields have been posted.
By setting auto_redirect to False, the user roles and permissions can be
checked within the login route and redirected from there.
Args:
func (callable): the function that is being wrapped
func (callable): The function that is being wrapped
method (string): The HTTP method that authentication will be performed
against.
form_class (string): The qualified class name of the form.
auto_redirect (boolean): Whether or not to automatically redirect to a
different url on successful login.
authenticated_callback (callable): An additional callback that can be
used to authenticate the user after
a valid user record has been found.
Takes user and request objects as
arguments.
Example:
Expand Down Expand Up @@ -134,19 +144,21 @@ def wrapper(self, *args, **kwargs):
user = authenticator.authenticate(
username=username_field,
password=password_field)
if user:
if not user:
valid = False
if valid and authenticated_callback and not authenticated_callback(user, self.request):
valid = False
if valid and user:
authenticator.assign_user_to_session(
user,
self.request,
auth_config['session']['key'])
if auto_redirect:
redirect_url = login_config['urls']['success']
if self.request.get['redirect']:
redirect_url = parse.unquote_plus(
self.request.get['redirect'])
return self.redirect(redirect_url, clear=True)
else:
valid = False
if valid and user and auto_redirect:
redirect_url = login_config['urls']['success']
if self.request.get['redirect']:
redirect_url = parse.unquote_plus(
self.request.get['redirect'])
return self.redirect(redirect_url, clear=True)
else:
valid = False
if not valid:
Expand Down Expand Up @@ -285,24 +297,23 @@ def wrapper(self, *args, **kwargs):
form.data = request
if form.is_valid():
self.flash_messages.add(
form_config['messages']['success'], 'error')
form_config['messages']['success'], 'success')
redirect_url = reset_config['urls']['success']
auto_login = authenticate_on_reset or reset_config['authenticate_on_reset']
if auto_login:
authenticator.assign_user_to_session(
token.user,
request,
auth_config['session']['key'])
forgotten_password_token_manager.update_user_password(
token, form.password)
redirect_url = auth_config['login']['urls']['success']
forgotten_password_token_manager.update_user_password(
token, form.password)
forgotten_password_token_manager.notify_user(
token.user,
request=self.request,
subject=auth_config['reset_password']['subject_line'],
template=auth_config['reset_password']['template'],
password=form.password)
forgotten_password_token_manager.delete_token(token)
if auto_login:
authenticator.assign_user_to_session(
token.user,
request,
auth_config['session']['key'])
redirect_url = auth_config['login']['urls']['success']
return self.redirect(redirect_url, clear=True)
else:
self.flash_messages.add(
Expand Down
6 changes: 4 additions & 2 deletions watson/auth/listeners.py
Expand Up @@ -48,9 +48,11 @@ def setup_config(self):
self.app_config['auth'] = auth_config
self.container.get('jinja2_renderer').add_package_loader(
'watson.auth', 'views')
router = self.container.get('router')
for route, definition in config.routes.items():
definition['name'] = route
self.container.get('router').add_definition(definition)
if route not in router:
definition['name'] = route
router.add_definition(definition)

def setup_authenticator(self):
for name, definition in config.definitions.items():
Expand Down
52 changes: 25 additions & 27 deletions watson/auth/views/debug/panels/auth.html
@@ -1,4 +1,3 @@
{% if user %}
<style>
.watson-debug-toolbar__panel__debug {
width: 100%;
Expand All @@ -7,29 +6,7 @@
width: 80px;
}
</style>
<dt>Session</dt>
<dd>
{{ request.session.id }}<br><br>
<table class="watson-debug-toolbar__panel__debug">
<thead>
<tr>
<th>Key</th><th>Data</th>
</tr>
</thead>
<tbody>
{% for key, data in request.session %}
<tr>
<td>{{ key }}</td>
<td>{{ data }}</td>
</tr>
{% else %}
<tr>
<td colspan="3">No session data.</td>
</tr>
{% endfor %}
</tbody>
</table>
</dd>
{% if user %}
<dt>Roles</dt>
<dd>
<table class="watson-debug-toolbar__panel__debug">
Expand Down Expand Up @@ -71,14 +48,35 @@
</tr>
{% else %}
<tr>
<td colspan="3">No permissions set for this user.</td>
<td colspan="4">No permissions set for this user.</td>
</tr>
{% endfor %}
</tbody>
</table>
</dd>
<br><br>

{% else %}
Unauthenticated guest, no data available.
{% endif %}
<dt>Session</dt>
<dd>
{{ request.session.id }} {{ request.session.timeout }}<br><br>
<table class="watson-debug-toolbar__panel__debug">
<thead>
<tr>
<th>Key</th><th>Data</th>
</tr>
</thead>
<tbody>
{% for key, data in request.session %}
<tr>
<td>{{ key }}</td>
<td>{{ data }}</td>
</tr>
{% else %}
<tr>
<td colspan="3">No session data.</td>
</tr>
{% endfor %}
</tbody>
</table>
</dd>

0 comments on commit 8d6a6e5

Please sign in to comment.