diff options
| author | Joe Carstairs <65492573+Sycamost@users.noreply.github.com> | 2023-12-25 15:44:50 +0000 |
|---|---|---|
| committer | Joe Carstairs <jcarstairs@scottlogic.com> | 2024-01-29 10:51:47 +0000 |
| commit | 078d2d370d8cb45381b7cf793d99a63dd4c064a4 (patch) | |
| tree | c924ce4f39ec3144791b88060222434d01e81bfb | |
| parent | 7e5fd083800f4c029853dc8ca1f749342deaff31 (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.md | 2 | ||||
| -rw-r--r-- | api/auth/_verifyRegistrationResponse.ts | 4 | ||||
| -rw-r--r-- | scss/custom-properties/_colours.scss | 4 | ||||
| -rw-r--r-- | scss/modules/_btn.scss | 15 | ||||
| -rw-r--r-- | scss/values/_colours.scss | 15 | ||||
| -rw-r--r-- | src/pages/[locale]/sign-up/index.astro | 73 | ||||
| -rw-r--r-- | src/types/UserAlreadyExistsError.d.ts | 8 |
7 files changed, 93 insertions, 28 deletions
@@ -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; |
