Merge pull request #195 from alphagov/remember_me_sign_on

Remember me functionality added and tested.
This commit is contained in:
Adam Shimali
2016-02-24 17:36:40 +00:00
11 changed files with 118 additions and 15 deletions

View File

@@ -7,7 +7,8 @@ from wtforms import (
ValidationError,
TextAreaField,
FileField,
RadioField
RadioField,
BooleanField
)
from wtforms.fields.html5 import EmailField, TelField
from wtforms.validators import DataRequired, Email, Length, Regexp
@@ -110,6 +111,7 @@ class TwoFactorForm(Form):
super(TwoFactorForm, self).__init__(*args, **kwargs)
sms_code = sms_code()
remember_me = BooleanField("Remember me")
def validate_sms_code(self, field):
is_valid, reason = self.validate_code_func(field.data)

View File

@@ -7,10 +7,10 @@ from flask import (
flash
)
from flask.ext.login import current_user
from flask.ext.login import (current_user, login_fresh, confirm_login)
from app.main import main
from app.main.dao import users_dao
from app.main.dao import (users_dao, services_dao)
from app.main.forms import LoginForm
@@ -18,11 +18,24 @@ from app.main.forms import LoginForm
def sign_in():
if current_user and current_user.is_authenticated():
return redirect(url_for('main.choose_service'))
form = LoginForm()
if form.validate_on_submit():
user = users_dao.get_user_by_email(form.email_address.data)
user = _get_and_verify_user(user, form.password.data)
if user:
# Remember me login
if not login_fresh() and \
not current_user.is_anonymous() and \
current_user.id == user.id and \
user.is_active():
confirm_login()
services = services_dao.get_services(user.id).get('data', [])
if (len(services) == 1):
return redirect(url_for('main.service_dashboard', service_id=services[0]['id']))
else:
return redirect(url_for('main.choose_service'))
session['user_details'] = {"email": user.email_address, "id": user.id}
if user.state == 'pending':
return redirect(url_for('.verify'))

View File

@@ -6,8 +6,7 @@ from app.main import main
@main.route('/sign-out', methods=(['GET']))
@login_required
def sign_out():
session.clear()
logout_user()
return redirect(url_for('main.index'))
return redirect(url_for('main.sign_in'))

View File

@@ -12,7 +12,10 @@ from app.main.forms import TwoFactorForm
@main.route('/two-factor', methods=['GET', 'POST'])
def two_factor():
# TODO handle user_email not in session
user_id = session['user_details']['id']
try:
user_id = session['user_details']['id']
except KeyError:
return redirect('main.sign_in')
def _check_code(code):
return users_dao.check_verify_code(user_id, code, "sms")
@@ -27,7 +30,7 @@ def two_factor():
if 'password' in session['user_details']:
user.set_password(session['user_details']['password'])
users_dao.update_user(user)
login_user(user)
login_user(user, remember=form.remember_me.data if form.remember_me.data else False)
finally:
del session['user_details']
if (len(services) == 1):

View File

@@ -1,7 +1,7 @@
from notifications_python_client.notifications import BaseAPIClient
from notifications_python_client.errors import HTTPError
from flask.ext.login import UserMixin
from flask.ext.login import (UserMixin, login_fresh)
class UserApiClient(BaseAPIClient):
@@ -100,6 +100,12 @@ class User(UserMixin):
def is_active(self):
return self.state == 'active'
def is_authenticated(self):
# To handle remember me token renewal
if not login_fresh():
return False
return super(User, self).is_authenticated()
@property
def id(self):
return self._id

View File

@@ -0,0 +1,30 @@
{% macro checkbox(
field,
hint=False,
help_link=None,
help_link_text=None,
width='2-3',
suffix=None
) %}
<label class="form-checkbox" for="{{ field.name }}">
{{ field.label }}
{% if hint %}
<span class="form-hint">
{{ hint }}
</span>
{% endif %}
{% if field.errors %}
<span class="error-message">
{{ field.errors[0] }}
</span>
{% endif %}
{{ field(**{
'class': 'form-control form-control-{} textbox-highlight-textbox'.format(width) if highlight_tags else 'form-control form-control-{} {}'.format(width, 'textbox-right-aligned' if suffix else ''),
'data-module': 'highlight-tags' if highlight_tags else ''})}}
{% if help_link and help_link_text %}
<p class="textbox-help-link">
<a href='{{ help_link }}'>{{ help_link_text }}</a>
</p>
{% endif %}
</label>
{% endmacro %}

View File

@@ -1,5 +1,6 @@
{% extends "withoutnav_template.html" %}
{% from "components/textbox.html" import textbox %}
{% from "components/checkbox.html" import checkbox %}
{% from "components/page-footer.html" import page_footer %}
{% block page_title %}
@@ -22,6 +23,7 @@
help_link=url_for('.verification_code_not_received'),
help_link_text='I havent received a text message'
) }}
{{ checkbox(form.remember_me) }}
{{ page_footer(
"Continue"
) }}

View File

@@ -1,4 +1,5 @@
import os
from datetime import timedelta
class Config(object):
@@ -21,9 +22,14 @@ class Config(object):
SESSION_COOKIE_NAME = 'notify_admin_session'
SESSION_COOKIE_PATH = '/admin'
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = True
PERMANENT_SESSION_LIFETIME = 3600 # seconds
SESSION_REFRESH_EACH_REQUEST = True
REMEMBER_COOKIE_NAME = 'notify_admin_remember_me'
REMEMBER_COOKIE_PATH = '/admin'
REMEMBER_COOKIE_DURATION = timedelta(days=1)
REMEMBER_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_SECURE = True
API_HOST_NAME = os.getenv('API_HOST_NAME')
NOTIFY_API_SECRET = os.getenv('NOTIFY_API_SECRET', "dev-secret")
@@ -54,6 +60,8 @@ class Development(Config):
ADMIN_CLIENT_USER_NAME = 'dev-notify-admin'
ADMIN_CLIENT_SECRET = 'dev-notify-secret-key'
WTF_CSRF_ENABLED = False
REMEMBER_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False
class Test(Development):
@@ -64,7 +72,6 @@ class Test(Development):
class Preview(Config):
DEBUG = False
HTTP_PROTOCOL = 'https'
SESSION_COOKIE_SECURE = True
HEADER_COLOUR = '#F47738' # $orange

View File

@@ -90,3 +90,26 @@ def test_should_return_redirect_when_user_is_pending(app_,
'password': 'val1dPassw0rd!'})
assert response.status_code == 302
assert response.location == url_for('main.verify', _external=True)
def test_not_fresh_session_login(app_,
api_user_active,
mock_login,
mock_get_user_by_email,
mock_verify_password,
mock_get_services_with_one_service):
with app_.test_request_context():
with app_.test_client() as client:
client.login(api_user_active)
with client.session_transaction() as session:
assert session['_fresh']
session['_fresh'] = False
# This should skip the two factor
response = client.post(
url_for('main.sign_in'), data={
'email_address': api_user_active.email_address,
'password': 'val1dPassw0rd!'})
assert response.status_code == 302
service_dct = mock_get_services_with_one_service(api_user_active.id)['data'][0]
assert response.location == url_for(
'main.service_dashboard', service_id=service_dct['id'], _external=True)

View File

@@ -7,7 +7,7 @@ def test_render_sign_out_redirects_to_sign_in(app_):
url_for('main.sign_out'))
assert response.status_code == 302
assert response.location == url_for(
'main.sign_in', _external=True, next=url_for('main.sign_out'))
'main.sign_in', _external=True)
def test_sign_out_user(app_,
@@ -22,9 +22,9 @@ def test_sign_out_user(app_,
email = 'valid@example.gov.uk'
password = 'val1dPassw0rd!'
with app_.test_client() as client:
with client.session_transaction() as session:
print('session: {}'.format(session))
client.login(api_user_active)
with client.session_transaction() as session:
assert session.get('user_id') is not None
# Check we are logged in
response = client.get(
url_for('main.service_dashboard', service_id="123"))
@@ -32,5 +32,6 @@ def test_sign_out_user(app_,
response = client.get(url_for('main.sign_out'))
assert response.status_code == 302
assert response.location == url_for(
'main.index', _external=True)
assert session.get('ItsdangerousSession') is None
'main.sign_in', _external=True)
with client.session_transaction() as session:
assert session.get('user_id') is None

View File

@@ -92,3 +92,20 @@ def test_should_login_user_when_multiple_valid_codes_exist(app_,
response = client.post(url_for('main.two_factor'),
data={'sms_code': '23456'})
assert response.status_code == 302
def test_remember_me_set(app_,
api_user_active,
mock_get_user,
mock_get_user_by_email,
mock_check_verify_code,
mock_get_services_with_one_service):
with app_.test_request_context():
with app_.test_client() as client:
with client.session_transaction() as session:
session['user_details'] = {
'id': api_user_active.id,
'email': api_user_active.email_address}
response = client.post(url_for('main.two_factor'),
data={'sms_code': '23456', 'remember_me': True})
assert response.status_code == 302