diff --git a/app/assets/javascripts/authenticateSecurityKey.js b/app/assets/javascripts/authenticateSecurityKey.js index 9ac503223..f5f4c50ce 100644 --- a/app/assets/javascripts/authenticateSecurityKey.js +++ b/app/assets/javascripts/authenticateSecurityKey.js @@ -1,13 +1,51 @@ (function (window) { - "use strict"; + "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.GOVUK.Modules.AuthenticateSecurityKey = function () { + this.start = function (component) { + $(component) + .on('click', function (event) { + event.preventDefault(); + + fetch('/webauthn/authenticate') + .then(response => { + if (!response.ok) { + throw Error(response.statusText); + } + + return response.arrayBuffer(); + }) + .then(data => { + var options = window.CBOR.decode(data); + // triggers browser dialogue to login with authenticator + return window.navigator.credentials.get(options); + }) + .then(credential => { + return fetch('/webauthn/authenticate', { + method: 'POST', + headers: { 'X-CSRFToken': component.data('csrfToken') }, + body: window.CBOR.encode({ + credentialId: new Uint8Array(credential.rawId), + authenticatorData: new Uint8Array(credential.response.authenticatorData), + signature: new Uint8Array(credential.response.signature), + clientDataJSON: new Uint8Array(credential.response.clientDataJSON), + }) + }); + }) + .then(response => { + if (!response.ok) { + throw Error(response.statusText); + } + // TODO: redirect + }) + .catch(error => { + console.error(error); + // some browsers will show an error dialogue for some + // errors; to be safe we always pop up an alert + var message = error.message || error; + alert('Error during authentication.\n\n' + message); + }); + }); }; -})(window); + }; +}) (window); diff --git a/app/main/views/webauthn_credentials.py b/app/main/views/webauthn_credentials.py index e212374d2..1f5cd273f 100644 --- a/app/main/views/webauthn_credentials.py +++ b/app/main/views/webauthn_credentials.py @@ -1,11 +1,14 @@ from fido2 import cbor -from flask import current_app, request, session +from fido2.client import ClientData +from fido2.ctap2 import AuthenticatorData +from flask import abort, current_app, request, session from flask_login import current_user from app.main import main +from app.models.user import User from app.models.webauthn_credential import RegistrationError, WebAuthnCredential from app.notify_client.user_api_client import user_api_client -from app.utils import user_is_platform_admin +from app.utils import redirect_to_sign_in, user_is_platform_admin @main.route('/webauthn/register') @@ -50,3 +53,49 @@ def webauthn_complete_register(): ) return cbor.encode('') + + +@main.route('/webauthn/authenticate', methods=['GET']) +@redirect_to_sign_in +def webauthn_begin_authentication(): + # get user from session + user_to_login = User.from_id(session['user_details']['id']) + + authentication_data, state = current_app.webauthn_server.authenticate_begin( + credentials=[ + credential.to_credential_data() + for credential in user_to_login.webauthn_credentials + ], + user_verification=None, # required, preferred, discouraged. sets whether to ask for PIN + ) + session["webauthn_authentication_state"] = state + return cbor.encode(authentication_data) + + +@main.route('/webauthn/authenticate', methods=['POST']) +@redirect_to_sign_in +def webauthn_complete_authentication(): + state = session.pop("webauthn_authentication_state") + request_data = cbor.decode(request.get_data()) + + user_id = session['user_details']['id'] + user_to_login = User.from_id(user_id) + + try: + current_app.webauthn_server.authenticate_complete( + state=state, + credentials=[ + credential.to_credential_data() + for credential in user_to_login.webauthn_credentials + ], + credential_id=request_data['credentialId'], + client_data=ClientData(request_data['clientDataJSON']), + auth_data=AuthenticatorData(request_data['authenticatorData']), + signature=request_data['signature'] + ) + except ValueError as exc: + current_app.logger.info(f'User {user_id} could not sign in using their webauthn token - {exc}') + abort(403) + + from app.main.views.two_factor import log_in_user + return log_in_user(user_id) diff --git a/tests/app/main/views/test_webauthn_credentials.py b/tests/app/main/views/test_webauthn_credentials.py index c74b1685e..4bfbfa904 100644 --- a/tests/app/main/views/test_webauthn_credentials.py +++ b/tests/app/main/views/test_webauthn_credentials.py @@ -157,3 +157,27 @@ def test_complete_register_handles_missing_state( assert response.status_code == 400 assert cbor.decode(response.data) == 'No registration in progress' + + +def test_begin_authentication_returns_encoded_options(client): + pass + + +def test_begin_authentication_includes_existing_credentials(client): + pass + + +def test_begin_authentication_stores_state_in_session(client): + pass + + +def test_complete_authentication_logs_user_in(client): + pass + + +def test_complete_authentication_403s_if_key_isnt_in_users_credentials(client): + pass + + +def test_complete_authentication_clears_session(client): + pass diff --git a/tests/app/test_navigation.py b/tests/app/test_navigation.py index 3e947de47..f29ea0a0d 100644 --- a/tests/app/test_navigation.py +++ b/tests/app/test_navigation.py @@ -341,6 +341,8 @@ EXCLUDED_ENDPOINTS = tuple(map(Navigation.get_endpoint_with_blueprint, { 'view_template_versions', 'webauthn_begin_register', 'webauthn_complete_register', + 'webauthn_begin_authentication', + 'webauthn_complete_authentication', 'who_can_use_notify', 'who_its_for', 'write_new_broadcast',