Merge pull request #15 from alphagov/3fa

3 Factor authentication
This commit is contained in:
minglis
2015-12-04 16:27:55 +00:00
10 changed files with 103 additions and 17 deletions

View File

@@ -9,6 +9,7 @@ from flask_wtf import CsrfProtect
from webassets.filter import get_filter
from werkzeug.exceptions import abort
from app.notify_client.api_client import AdminAPIClient
from app.its_dangerous_session import ItsdangerousSessionInterface
import app.proxy_fix
from config import configs
@@ -18,6 +19,8 @@ db = SQLAlchemy()
login_manager = LoginManager()
csrf = CsrfProtect()
admin_api_client = AdminAPIClient()
def create_app(config_name):
application = Flask(__name__)
@@ -37,6 +40,7 @@ def create_app(config_name):
proxy_fix.init_app(application)
application.session_interface = ItsdangerousSessionInterface()
admin_api_client.init_app(application)
return application

5
app/main/exceptions.py Normal file
View File

@@ -0,0 +1,5 @@
class AdminApiClientException(Exception):
def __init__(self, message):
self.value = message

View File

@@ -15,19 +15,16 @@ def govuk():
@main.route("/register-from-invite")
@login_required
def registerfrominvite():
return render_template('register-from-invite.html')
@main.route("/verify")
@login_required
def verify():
return render_template('verify.html')
@main.route("/verify-mobile")
@login_required
def verifymobile():
return render_template('verify-mobile.html')
@@ -50,7 +47,6 @@ def addservice():
@main.route("/two-factor")
@login_required
def twofactor():
return render_template('two-factor.html')

View File

@@ -1,10 +1,14 @@
from datetime import datetime
from datetime import datetime, timedelta
from random import randint
from flask import render_template, redirect, jsonify
from flask_login import login_user
from flask import render_template, redirect, jsonify, session
from sqlalchemy.exc import SQLAlchemyError
from app import admin_api_client
from app.main import main
from app.main.dao import users_dao
from app.main.encryption import hashpw
from app.main.exceptions import AdminApiClientException
from app.main.forms import RegisterUserForm
from app.models import User
@@ -26,10 +30,43 @@ def process_register():
created_at=datetime.now(),
role_id=1)
try:
sms_code = send_sms_code(form.mobile_number.data)
email_code = send_email_code(form.email_address.data)
session['sms_code'] = hashpw(sms_code)
session['email_code'] = hashpw(email_code)
session['expiry_date'] = str(datetime.now() + timedelta(hours=1))
users_dao.insert_user(user)
login_user(user)
return redirect('/two-factor')
except Exception as e:
return jsonify(database_error=e.message), 400
except AdminApiClientException as e:
return jsonify(admin_api_client_error=e.value)
except SQLAlchemyError:
return jsonify(database_error='encountered database error'), 400
else:
return jsonify(form.errors), 400
return redirect('/verify')
def send_sms_code(mobile_number):
sms_code = _create_code()
try:
admin_api_client.send_sms(mobile_number, message=sms_code, token=admin_api_client.auth_token)
except:
raise AdminApiClientException('Exception when sending sms.')
return sms_code
def send_email_code(email):
email_code = _create_code()
try:
admin_api_client.send_email(email_address=email,
from_str='notify@digital.cabinet-office.gov.uk',
message=email_code,
subject='Verification code',
token=admin_api_client.auth_token)
except:
raise AdminApiClientException('Exception when sending email.')
return email_code
def _create_code():
return ''.join(["%s" % randint(0, 9) for _ in range(0, 5)])

View File

View File

@@ -0,0 +1,8 @@
from __future__ import unicode_literals
from notify_client import NotifyAPIClient
class AdminAPIClient(NotifyAPIClient):
def init_app(self, app):
self.base_url = app.config['NOTIFY_DATA_API_URL']
self.auth_token = app.config['NOTIFY_DATA_API_AUTH_TOKEN']

View File

@@ -1,3 +1,5 @@
import os
class Config(object):
DEBUG = False
@@ -11,6 +13,14 @@ class Config(object):
MAX_FAILED_LOGIN_COUNT = 10
PASS_SECRET_KEY = 'secret-key-unique-changeme'
SESSION_COOKIE_NAME = 'notify_admin_session'
SESSION_COOKIE_PATH = '/admin'
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
NOTIFY_DATA_API_URL = os.getenv('NOTIFY_API_URL', "http://localhost:6001")
NOTIFY_DATA_API_AUTH_TOKEN = os.getenv('NOTIFY_API_TOKEN', "dev-token")
WTF_CSRF_ENABLED = True
SECRET_KEY = 'secret-key'
HTTP_PROTOCOL = 'http'

View File

@@ -8,4 +8,6 @@ SQLAlchemy==1.0.5
SQLAlchemy-Utils==0.30.5
Flask-WTF==0.11
Flask-Login==0.2.11
Flask-Bcrypt==0.6.2
Flask-Bcrypt==0.6.2
git+https://github.com/alphagov/notify-api-client.git@0.1.4#egg=notify-api-client==0.1.4

View File

@@ -1,3 +1,4 @@
-r requirements.txt
pep8==1.5.7
pytest==2.8.1
pytest==2.8.1
pytest-mock==0.8.1

View File

@@ -7,17 +7,22 @@ def test_render_register_returns_template_with_form(notifications_admin, notific
assert 'Create an account' in response.get_data(as_text=True)
def test_process_register_creates_new_user(notifications_admin, notifications_admin_db):
def test_process_register_creates_new_user(notifications_admin, notifications_admin_db, mocker):
_set_up_mocker(mocker)
response = notifications_admin.test_client().post('/register',
data={'name': 'Some One Valid',
'email_address': 'someone@example.gov.uk',
'mobile_number': '+441231231231',
'password': 'validPassword!'})
assert response.status_code == 302
assert response.location == 'http://localhost/two-factor'
assert response.location == 'http://localhost/verify'
def test_process_register_returns_400_when_mobile_number_is_invalid(notifications_admin, notifications_admin_db):
def test_process_register_returns_400_when_mobile_number_is_invalid(notifications_admin,
notifications_admin_db,
mocker):
_set_up_mocker(mocker)
response = notifications_admin.test_client().post('/register',
data={'name': 'Bad Mobile',
'email_address': 'bad_mobile@example.gov.uk',
@@ -28,7 +33,8 @@ def test_process_register_returns_400_when_mobile_number_is_invalid(notification
assert 'Please enter a +44 mobile number' in response.get_data(as_text=True)
def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notifications_admin_db):
def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notifications_admin_db, mocker):
_set_up_mocker(mocker)
response = notifications_admin.test_client().post('/register',
data={'name': 'Bad Mobile',
'email_address': 'bad_mobile@example.not.right',
@@ -39,6 +45,23 @@ def test_should_return_400_when_email_is_not_gov_uk(notifications_admin, notific
assert 'Please enter a gov.uk email address' in response.get_data(as_text=True)
def test_should_add_verify_codes_on_session(notifications_admin, notifications_admin_db, mocker):
_set_up_mocker(mocker)
with notifications_admin.test_client() as client:
response = client.post('/register',
data={'name': 'Test Codes',
'email_address': 'test_codes@example.gov.uk',
'mobile_number': '+441234567890',
'password': 'validPassword!'})
assert response.status_code == 302
assert 'notify_admin_session' in response.headers.get('Set-Cookie')
def _set_up_mocker(mocker):
mocker.patch("app.admin_api_client.send_sms")
mocker.patch("app.admin_api_client.send_email")
def test_should_return_400_if_password_is_blacklisted(notifications_admin, notifications_admin_db):
response = notifications_admin.test_client().post('/register',
data={'name': 'Bad Mobile',