summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Carstairs <65492573+Sycamost@users.noreply.github.com>2023-12-25 15:44:50 +0000
committerJoe Carstairs <jcarstairs@scottlogic.com>2024-01-29 10:51:47 +0000
commit078d2d370d8cb45381b7cf793d99a63dd4c064a4 (patch)
treec924ce4f39ec3144791b88060222434d01e81bfb
parent7e5fd083800f4c029853dc8ca1f749342deaff31 (diff)
33 / WebAuthn (#57)
* Working message on sign-up form * Disables form nicely when processing request * Updates README * Better error logging in verifyRegistrationResponse()
-rw-r--r--README.md2
-rw-r--r--api/auth/_verifyRegistrationResponse.ts4
-rw-r--r--scss/custom-properties/_colours.scss4
-rw-r--r--scss/modules/_btn.scss15
-rw-r--r--scss/values/_colours.scss15
-rw-r--r--src/pages/[locale]/sign-up/index.astro73
-rw-r--r--src/types/UserAlreadyExistsError.d.ts8
7 files changed, 93 insertions, 28 deletions
diff --git a/README.md b/README.md
index 5fa74d3..a9a2a6c 100644
--- a/README.md
+++ b/README.md
@@ -161,7 +161,7 @@ This website implements [WebAuthn](https://webauthn.guide) for user authenticati
1. A user goes to the signup page, `/sign-up` and enters their username and display name.
2. That sends their details to `GET /registration` on the backend.
3. If a user with the provided username already exists, the flow fails and the user is directed to
- log in or provide a different username. (TODO!)
+ log in or provide a different username.
4. Otherwise, the backend returns registration options.
5. On the client, these registration options are handed to the user's authenticator, which will
generate a public-private keypair and expose the public key. The authenticator might be a
diff --git a/api/auth/_verifyRegistrationResponse.ts b/api/auth/_verifyRegistrationResponse.ts
index 99be2bb..90899ad 100644
--- a/api/auth/_verifyRegistrationResponse.ts
+++ b/api/auth/_verifyRegistrationResponse.ts
@@ -30,7 +30,7 @@ export default async function verifyRegistrationResponse(
: 'https://staging.scotsleidassocie.org',
expectedRPID: RELYING_PARTY.id,
}).catch((err) => {
- throw new Error('Registration response verification failed.', err);
+ throw new Error(`Registration response verification failed. ${err}`);
});
if (!verification.verified || !verification.registrationInfo) {
@@ -59,4 +59,4 @@ export default async function verifyRegistrationResponse(
addUser(newUser);
addAuthenticator(authenticator);
-} \ No newline at end of file
+}
diff --git a/scss/custom-properties/_colours.scss b/scss/custom-properties/_colours.scss
index dadb50e..efa9cdc 100644
--- a/scss/custom-properties/_colours.scss
+++ b/scss/custom-properties/_colours.scss
@@ -7,6 +7,8 @@
--background-highlight-color: #{colours.$primary-90};
--special-text-color: #{colours.$secondary-90};
--special-text-highlight-color: #{colours.$secondary-10};
+ --neutral-text-color: #{colours.$neutral-50};
+ --neutral-background-color: #{colours.$neutral-30};
}
:is(main, article) {
@@ -16,6 +18,8 @@
--background-highlight-color: #{colours.$primary-10};
--special-text-color: #{colours.$secondary-40};
--special-text-highlight-color: #{colours.$secondary-90};
+ --neutral-text-color: #{colours.$neutral-70};
+ --neutral-background-color: #{colours.$neutral-95};
}
:is(main, article) > section:nth-child(even):not(:is(section, header) :is(section, header)) {
diff --git a/scss/modules/_btn.scss b/scss/modules/_btn.scss
index b906262..45e292f 100644
--- a/scss/modules/_btn.scss
+++ b/scss/modules/_btn.scss
@@ -7,12 +7,13 @@
padding-inline: var(--spacing-inline-md);
padding-block: var(--spacing-block-sm);
background-color: transparent;
+ cursor: pointer;
transition:
background-color 100ms,
transform 100ms;
}
-:is(button, .btn):is(:hover, :focus, :focus-visible, :focus-within, :active) {
+:is(button, .btn):is(:hover, :focus, :focus-visible, :focus-within, :active):not(:is(:disabled, [disabled])) {
box-shadow: none;
outline: none;
color: var(--text-highlight-color);
@@ -23,12 +24,12 @@
transform 300ms;
}
-:is(button, .btn):is(:focus, :focus-visible, :focus-within) {
+:is(button, .btn):is(:focus, :focus-visible, :focus-within):not(:is(:disabled, [disabled])) {
border-color: var(--special-text-highlight-color);
border-style: dotted;
}
-:is(button, .btn):active {
+:is(button, .btn):active:not(:is(:disabled), [disabled]) {
transform: scale(1.1, 1.1);
}
@@ -38,3 +39,11 @@ a.btn {
text-decoration: none;
}
+:is(button, .btn):is(:disabled, [disabled]) {
+ color: var(--neutral-text-color);
+ background-color: var(--neutral-background-color);
+ border-color: var(--neutral-text-color);
+ cursor: initial;
+}
+
+
diff --git a/scss/values/_colours.scss b/scss/values/_colours.scss
index 40db6e8..5c00e7d 100644
--- a/scss/values/_colours.scss
+++ b/scss/values/_colours.scss
@@ -32,3 +32,18 @@ $secondary-98: #e4fffb;
$secondary-99: #f2fffc;
$secondary-100: #ffffff;
+$neutral-0: #000;
+$neutral-10: #03020f;
+$neutral-20: #141327;
+$neutral-30: #2c2b41;
+$neutral-40: #45455d;
+$neutral-50: #61607a;
+$neutral-60: #7d7d98;
+$neutral-70: #9b9bb7;
+$neutral-80: #bbbbd7;
+$neutral-90: #dbdbf8;
+$neutral-95: #ecedff;
+$neutral-98: #f7f8ff;
+$neutral-99: #fbfbff;
+$neutral-100: #fff;
+
diff --git a/src/pages/[locale]/sign-up/index.astro b/src/pages/[locale]/sign-up/index.astro
index 204dbcc..27b40a2 100644
--- a/src/pages/[locale]/sign-up/index.astro
+++ b/src/pages/[locale]/sign-up/index.astro
@@ -35,11 +35,15 @@ const t = translate(locale);
import * as api from '$lib/api';
import { startRegistration } from '@simplewebauthn/browser';
+ import UserAlreadyExistsError from '$types/UserAlreadyExistsError.d';
const form = document.getElementById('sign-up-form') as HTMLFormElement;
const displayNameInput = document.getElementById('display-name') as HTMLInputElement;
const emailInput = document.getElementById('email') as HTMLInputElement;
+ const submitButton = document.getElementsByTagName('button')[0] as HTMLButtonElement;
const locale = document.getElementsByTagName('html')[0].lang as Locale;
+ const statusInfoMsg = document.createElement('p');
+ statusInfoMsg.textContent = locale === 'en-GB' ? 'Signing you up…' : 'Signin you up…';
const successPageUrl = `${locale}/sign-up/success`;
@@ -47,6 +51,10 @@ const t = translate(locale);
// Prevent default behaviour of submit button (which includes refreshing the page)
event.preventDefault();
+ // Disable the form and leave message to let the user know it’s been submitted
+ setIsFormDisabled('true');
+ form.after(statusInfoMsg);
+
let registrationOptions;
try {
registrationOptions = await getRegistrationOptions();
@@ -90,21 +98,52 @@ const t = translate(locale);
url.searchParams.set('username', registrationOptions.user.name);
console.info(`Redirecting to the success page ${url}...`);
+ statusInfoMsg.innerHTML =
+ locale === 'en-GB'
+ ? `Redirecting to <a href="${url.toString()}">the success page</a>…`
+ : `Redirectin til <a href="${url.toString()}">the success page</a>…`;
window.location.href = url.toString();
return;
});
+ function setIsFormDisabled(isDisabled: boolean) {
+ if (isDisabled) {
+ form.setAttribute('disabled', '');
+ emailInput.setAttribute('disabled', '');
+ displayNameInput.setAttribute('disabled', '');
+ submitButton.setAttribute('disabled', '');
+ } else {
+ form.removeAttribute('disabled');
+ emailInput.removeAttribute('disabled');
+ displayNameInput.removeAttribute('disabled');
+ submitButton.removeAttribute('disabled');
+ }
+ }
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function reportError(context: string, err: any) {
- console.error(context, err);
- window.alert(`
- Encountered an unexpected error. Please try again, or, if the error
- persists, contact lallans@hotmail.co.uk.
-
- ${context}
+ let msg;
+
+ if (err instanceof UserAlreadyExistsError) {
+ console.warn(err);
+ msg = `
+ A user already exists with that username. Please log in, or try a
+ different username.
+ `;
+ } else {
+ msg = `
+ Encountered an unexpected error. Please try again, or, if the error
+ persists, contact lallans@hotmail.co.uk.
+
+ ${err?.toString() ?? ''}
+
+ ${context}
+ `;
+ console.error('Unexpected error in sign-up.', err);
+ }
- ${err?.toString() ?? ''}
- `);
+ statusInfoMsg.textContent = msg;
+ setIsFormDisabled(false);
}
async function getRegistrationOptions(): Promise<GetRegistrationOptionsResponseBody> {
@@ -113,12 +152,8 @@ const t = translate(locale);
userId: emailInput.value,
});
- if (response.status === 409 /* CONFLICT: user ID already exists in database */) {
- // TODO: go to a nice localised error page instead of showing an alert
- window.alert('Username already exists! Log in or choose a different username.');
-
- // TODO: obviously this is stupid. Do something non-stupid instead
- return null as unknown as GetRegistrationOptionsResponseBody;
+ if (response.status === 409) {
+ throw new UserAlreadyExistsError(emailInput.value);
}
if (!response.ok) {
@@ -141,14 +176,8 @@ const t = translate(locale);
): Promise<boolean> {
const response = await api.verifyRegistrationResponse(params);
- if (response.status === 409 /* CONFLICT: user ID already exists*/) {
- // TODO: go to a nice localised error page instead of showing an alert
- window.alert(
- 'Username or ID already exists! Try logging in or choosing a different username.'
- );
-
- // TODO: obviously this is stupid. Do something non-stupid instead
- return null as unknown as boolean;
+ if (response.status === 409) {
+ throw new UserAlreadyExistsError(params.userId);
}
if (!response.ok) {
diff --git a/src/types/UserAlreadyExistsError.d.ts b/src/types/UserAlreadyExistsError.d.ts
new file mode 100644
index 0000000..33ad8fc
--- /dev/null
+++ b/src/types/UserAlreadyExistsError.d.ts
@@ -0,0 +1,8 @@
+class UserAlreadyExistsError extends Error {
+ constructor(userId) {
+ super(`A user already exists with the user ID ${userId}.`);
+ this.name = 'UserAlreadyExistsError';
+ }
+}
+
+export default UserAlreadyExistsError;