Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite errorBanner, register&authenticate security key as ES Modules #5333

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 0 additions & 74 deletions app/assets/javascripts/authenticateSecurityKey.js

This file was deleted.

17 changes: 0 additions & 17 deletions app/assets/javascripts/errorBanner.js

This file was deleted.

12 changes: 12 additions & 0 deletions app/assets/javascripts/esm/all-esm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import FocusBanner from './focus-banner.mjs';
import ColourPreview from './colour-preview.mjs';
import FileUpload from './file-upload.mjs';
import Autofocus from './autofocus.mjs';
import AuthenticateSecurityKey from './authenticate-security-key.mjs';
import RegisterSecurityKey from './register-security-key.mjs';

// Modules from 3rd party vendors
import morphdom from 'morphdom';
Expand Down Expand Up @@ -37,6 +39,16 @@ if ($autoFocus) {
new Autofocus($autoFocus);
}

const $authenticateSecurityKey = document.querySelector('[data-notify-module="authenticate-security-key"]');
if ($authenticateSecurityKey) {
new AuthenticateSecurityKey($authenticateSecurityKey);
}

const $registerSecurityKey = document.querySelector('[data-notify-module="register-security-key"]');
if ($registerSecurityKey) {
new RegisterSecurityKey($registerSecurityKey);
}

const focusBanner = new FocusBanner();

// ES modules do not export to global so in order to
Expand Down
95 changes: 95 additions & 0 deletions app/assets/javascripts/esm/authenticate-security-key.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { isSupported } from 'govuk-frontend';
import ErrorBanner from './error-banner.mjs';
import { decode, encode } from 'cbor2';

// This new way of writing Javascript components is based on the GOV.UK Frontend skeleton Javascript coding standard
// that uses ES2015 Classes -
// https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/coding-standards/js.md#skeleton
//
// It replaces the previously used way of setting methods on the component's `prototype`.
// We use a class declaration way of defining classes -
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class
//
// More on ES2015 Classes at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

class AuthenticateSecurityKey {
constructor($module) {
if (!isSupported()) {
return this;
}
this.authenticatePath = '/webauthn/authenticate';
this.$module = $module;

this.$module.addEventListener("click", () => {
new ErrorBanner().hideBanner();
this.getAuthentication();
});
}
getAuthentication() {
fetch(this.authenticatePath)
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}

return response.arrayBuffer();
})
.then(data => {
var options = decode(new Uint8Array(data));
// triggers browser dialogue to login with authenticator
return window.navigator.credentials.get(options);
})
.then(credential => {
const currentURL = new URL(window.location.href);

// create authenticateURL from admin hostname plus /webauthn/authenticate path
const authenticateURL = new URL(this.authenticatePath, window.location.href);

const nextUrl = currentURL.searchParams.get('next');
if (nextUrl) {
// takes nextUrl from the query string on the current browser URL
// (which should be /two-factor-webauthn) and pass it through to
// the POST. put it in a query string so it's consistent with how
// the other login flows manage it
authenticateURL.searchParams.set('next', nextUrl);
}

return this.postWebAuthnCreateResponse(
credential, this.$module.dataset.csrfToken
);
})
.then(response => {
if (!response.ok) {
throw Error(response.statusText);
}

return response.arrayBuffer();
})
.then(cbor => {
return Promise.resolve(decode(new Uint8Array(cbor)));
})
.then(data => {
window.location.assign(data.redirect_url);
})
.catch(error => {
console.error(error);
// some browsers will show an error dialogue for some
// errors; to be safe we always display an error message on the page.
new ErrorBanner().showBanner();
});
}
postWebAuthnCreateResponse(credential, csrf_token) {
return fetch(this.authenticatePath, {
method: 'POST',
headers: { 'X-CSRFToken': csrf_token },
body: encode({
credentialId: new Uint8Array(credential.rawId),
authenticatorData: new Uint8Array(credential.response.authenticatorData),
signature: new Uint8Array(credential.response.signature),
clientDataJSON: new Uint8Array(credential.response.clientDataJSON),
})
});
}
}

export default AuthenticateSecurityKey;
43 changes: 43 additions & 0 deletions app/assets/javascripts/esm/error-banner.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isSupported } from 'govuk-frontend';

// This new way of writing Javascript components is based on the GOV.UK Frontend skeleton Javascript coding standard
// that uses ES2015 Classes -
// https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/coding-standards/js.md#skeleton
//
// It replaces the previously used way of setting methods on the component's `prototype`.
// We use a class declaration way of defining classes -
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class
//
// More on ES2015 Classes at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

class ErrorBanner {
/*
This module is intended to be used to show and hide an error banner based on a javascript trigger. You should make
sure the banner has an appropriate aria-live attribute, and a tabindex of -1 so that screenreaders and keyboard users
are alerted to the change respectively.

This may behave in unexpected ways if you have more than one element with the `govuk-error-summary` class on your page.
*/
constructor() {
if (!isSupported()) {
return this;
}
// yes some pages have more than one error summary on the page
// depending on if there's no JS or no webuth support
this.errorSummaryArray = document.querySelectorAll('.govuk-error-summary');
}
hideBanner() {
this.errorSummaryArray.forEach(errorSummary => {
errorSummary.classList.add('govuk-!-display-none');
});
}
showBanner() {
this.errorSummaryArray.forEach(errorSummary => {
errorSummary.classList.remove('govuk-!-display-none');
// is works as before but it feels strange to apply focus to all
errorSummary.focus();
});
}
}

export default ErrorBanner;
73 changes: 73 additions & 0 deletions app/assets/javascripts/esm/register-security-key.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { isSupported } from 'govuk-frontend';
import ErrorBanner from './error-banner.mjs';
import { decode, encode } from 'cbor2';

// This new way of writing Javascript components is based on the GOV.UK Frontend skeleton Javascript coding standard
// that uses ES2015 Classes -
// https://github.com/alphagov/govuk-frontend/blob/main/docs/contributing/coding-standards/js.md#skeleton
//
// It replaces the previously used way of setting methods on the component's `prototype`.
// We use a class declaration way of defining classes -
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class
//
// More on ES2015 Classes at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

class RegisterSecurityKey {
constructor($module) {
if (!isSupported()) {
return this;
}
this.registerPath = '/webauthn/register';
this.$module = $module;

this.$module.addEventListener("click", () => {
new ErrorBanner().hideBanner();
this.getAuthentication();
});
}
getAuthentication() {
fetch(this.registerPath)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}

return response.arrayBuffer();
})
.then((data) => {
var options = decode(new Uint8Array(data));
// triggers browser dialogue to select authenticator
return window.navigator.credentials.create(options);
})
.then((credential) => {
return this.postWebAuthnCreateResponse(
credential.response, this.$module.dataset.csrfToken
);
})
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}

window.location.reload();
})
.catch((error) => {
console.error(error);
// some browsers will show an error dialogue for some
// errors; to be safe we always display an error message on the page.
new ErrorBanner().showBanner();
});
}
postWebAuthnCreateResponse(response, csrf_token) {
return fetch(this.registerPath, {
method: 'POST',
headers: { 'X-CSRFToken': csrf_token },
body: encode({
attestationObject: new Uint8Array(response.attestationObject),
clientDataJSON: new Uint8Array(response.clientDataJSON),
})
});
}
}

export default RegisterSecurityKey;
58 changes: 0 additions & 58 deletions app/assets/javascripts/registerSecurityKey.js

This file was deleted.

Loading