diff --git a/app/assets/javascripts/authenticateSecurityKey.js b/app/assets/javascripts/authenticateSecurityKey.js
new file mode 100644
index 000000000..9ac503223
--- /dev/null
+++ b/app/assets/javascripts/authenticateSecurityKey.js
@@ -0,0 +1,13 @@
+(function (window) {
+ "use strict";
+
+ window.GOVUK.Modules.AuthenticateSecurityKey = function () {
+ this.start = function (component) {
+ $(component)
+ .on('click', function (event) {
+ event.preventDefault();
+ console.log('pretend you just logged in okay');
+ });
+ };
+ };
+})(window);
diff --git a/app/main/views/sign_in.py b/app/main/views/sign_in.py
index 6a1d9ba4b..edca1bc12 100644
--- a/app/main/views/sign_in.py
+++ b/app/main/views/sign_in.py
@@ -50,7 +50,7 @@ def sign_in():
if user.email_auth:
return redirect(url_for('.two_factor_email_sent', next=redirect_url))
if user.webauthn_auth:
- raise NotImplementedError('webauthn not supported yet')
+ return redirect(url_for('.two_factor_webauthn', next=redirect_url))
# Vague error message for login in case of user not known, locked, inactive or password not verified
flash(Markup(
diff --git a/app/main/views/two_factor.py b/app/main/views/two_factor.py
index 8bbbe0ba1..6757d6391 100644
--- a/app/main/views/two_factor.py
+++ b/app/main/views/two_factor.py
@@ -82,6 +82,13 @@ def two_factor():
return render_template('views/two-factor.html', form=form, redirect_url=redirect_url)
+@main.route('/two-factor-webauthn', methods=['GET'])
+@redirect_to_sign_in
+def two_factor_webauthn():
+ redirect_url = request.args.get('next')
+ return render_template('views/two-factor-webauthn.html', redirect_url=redirect_url)
+
+
@main.route('/re-validate-email', methods=['GET'])
def revalidate_email_sent():
title = 'Email resent' if request.args.get('email_resent') else 'Check your email'
diff --git a/app/navigation.py b/app/navigation.py
index 77e1f59e3..447a1b102 100644
--- a/app/navigation.py
+++ b/app/navigation.py
@@ -111,6 +111,7 @@ class HeaderNavigation(Navigation):
'two_factor_email',
'two_factor_email_sent',
'two_factor_email_interstitial',
+ 'two_factor_webauthn',
'verify',
'verify_email',
},
diff --git a/app/templates/views/two-factor-webauthn.html b/app/templates/views/two-factor-webauthn.html
new file mode 100644
index 000000000..a2758e76e
--- /dev/null
+++ b/app/templates/views/two-factor-webauthn.html
@@ -0,0 +1,33 @@
+{% extends "withoutnav_template.html" %}
+{% from "components/page-header.html" import page_header %}
+{% from "components/button/macro.njk" import govukButton %}
+
+{% set page_title = 'Security keys' %}
+
+{% block per_page_title %}
+ {{ page_title }}
+{% endblock %}
+
+{% block maincolumn_content %}
+ {{ page_header(
+ page_title,
+ back_link=url_for('.user_profile')
+ ) }}
+
+
+
+
Security key
+
When you are ready to authenticate, press the button below.
+
+ {{ govukButton({
+ "element": "button",
+ "text": "Webauthn authenticate click me click me",
+ "classes": "govuk-button--secondary",
+ "attributes": {
+ "data-module": "authenticate-security-key",
+ "data-csrf-token": csrf_token(),
+ }
+ }) }}
+
+
+{% endblock %}
diff --git a/gulpfile.js b/gulpfile.js
index 88e55fc77..9dbd998ee 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -180,6 +180,7 @@ const javascripts = () => {
paths.src + 'javascripts/collapsibleCheckboxes.js',
paths.src + 'javascripts/radioSlider.js',
paths.src + 'javascripts/registerSecurityKey.js',
+ paths.src + 'javascripts/authenticateSecurityKey.js',
paths.src + 'javascripts/updateStatus.js',
paths.src + 'javascripts/homepage.js',
paths.src + 'javascripts/main.js',
diff --git a/tests/app/main/views/test_sign_in.py b/tests/app/main/views/test_sign_in.py
index 9d4aafbcf..d803e26ec 100644
--- a/tests/app/main/views/test_sign_in.py
+++ b/tests/app/main/views/test_sign_in.py
@@ -160,6 +160,34 @@ def test_process_email_auth_sign_in_return_2fa_template(
mock_verify_password.assert_called_with(api_user_active_email_auth['id'], 'val1dPassw0rd!')
+@pytest.mark.parametrize('redirect_url', [
+ None,
+ f'/services/{SERVICE_ONE_ID}/templates',
+])
+def test_process_webauthn_auth_sign_in_redirects_to_webauthn_with_next_redirect(
+ client,
+ api_user_active,
+ mocker,
+ mock_verify_password,
+ redirect_url
+):
+ api_user_active['auth_type'] = 'webauthn_auth'
+ mock_get_user_by_email = mocker.patch('app.user_api_client.get_user_by_email', return_value=api_user_active)
+
+ response = client.post(
+ url_for(
+ 'main.sign_in', next=redirect_url
+ ),
+ data={
+ 'email_address': 'valid@example.gov.uk',
+ 'password': 'val1dPassw0rd!'
+ }
+ )
+ mock_get_user_by_email.assert_called_once_with('valid@example.gov.uk')
+ assert response.status_code == 302
+ assert response.location == url_for('.two_factor_webauthn', _external=True, next=redirect_url)
+
+
def test_should_return_locked_out_true_when_user_is_locked(
client,
mock_get_user_by_email_locked,
diff --git a/tests/app/main/views/test_two_factor.py b/tests/app/main/views/test_two_factor.py
index 07ffabe17..7a0120bda 100644
--- a/tests/app/main/views/test_two_factor.py
+++ b/tests/app/main/views/test_two_factor.py
@@ -258,15 +258,25 @@ def test_two_factor_returns_error_when_user_is_locked(
assert 'Code not found' in response.get_data(as_text=True)
-def test_two_factor_should_redirect_to_sign_in_if_user_not_in_session(
- client,
- api_user_active,
- mock_get_user,
+def test_two_factor_post_should_redirect_to_sign_in_if_user_not_in_session(
+ client_request,
):
- response = client.post(url_for('main.two_factor'),
- data={'sms_code': '12345'})
- assert response.status_code == 302
- assert response.location == url_for('main.sign_in', _external=True)
+ client_request.post(
+ 'main.two_factor',
+ _data={'sms_code': '12345'},
+ _expected_redirect=url_for('main.sign_in', _external=True)
+ )
+
+
+@pytest.mark.parametrize('endpoint', ['main.two_factor_webauthn', 'main.two_factor'])
+def test_two_factor_get_should_redirect_to_sign_in_if_user_not_in_session(
+ client_request,
+ endpoint,
+):
+ client_request.get(
+ endpoint,
+ _expected_redirect=url_for('main.sign_in', _external=True)
+ )
@freeze_time('2020-01-27T12:00:00')
diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py
index 9871491c2..da168c240 100644
--- a/tests/app/test_navigation.py
+++ b/tests/app/test_navigation.py
@@ -293,6 +293,7 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, {
'two_factor_email',
'two_factor_email_interstitial',
'two_factor_email_sent',
+ 'two_factor_webauthn',
'update_email_branding',
'update_letter_branding',
'upload_a_letter',