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',