diff options
| author | Joe Carstairs <65492573+Sycamost@users.noreply.github.com> | 2023-12-21 21:22:47 +0000 |
|---|---|---|
| committer | Joe Carstairs <jcarstairs@scottlogic.com> | 2024-01-29 10:46:46 +0000 |
| commit | 2d0634cdc3d00b3e55cf773ff03c7dc841112d36 (patch) | |
| tree | 424f0b9ce2907e3d58336a2b25233bf0f9bead9d /api/auth/registration.ts | |
| parent | f44b2a82b74337c6424c576fdbf4c1376166e553 (diff) | |
33 / Users can sign up and log in with WebAuthn (#39)
* Installs @vercel/postgres
* Installs @simplewebauthn/server
* Installs @simplewebauthn/browser
* Git-ignores all files starting with .env
* Reorganises folders in API
* Defines User type
* Defines Subscription type
* Defines Authenticator type
* Sets up table definition file
* Can get user from database
* Can add user to database
* Can get user's current challenge
* Can set user's current challenge
* Can get user's authenticators from database
* Can get authenticator by ID from database
* Can add user authenticator to database
* Can update authenticator in database
* Defines Relying Party information
* Can generate registration options
* Can verify registration response
* Defines registration API endpoint
* Defines user API endpoint
* Reorganises API functions on frontend
* Can access authentication API functions on frontend
* Can generate authentication options
* Can verify authentication response
* Documents the registration flow
* Fix dev_csso
* Form styling
* WIP adds sign up page
Diffstat (limited to 'api/auth/registration.ts')
| -rw-r--r-- | api/auth/registration.ts | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/api/auth/registration.ts b/api/auth/registration.ts new file mode 100644 index 0000000..d318d83 --- /dev/null +++ b/api/auth/registration.ts @@ -0,0 +1,159 @@ +import type { RegistrationResponseJSON } from '@simplewebauthn/server/script/deps'; +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +import generateRegistrationOptionsForNewUser from './_generateRegistrationOptionsForNewUser'; +import verifyRegistrationResponse from './_verifyRegistrationResponse'; +import getUser from '../db/_getUser'; +import getCurrentChallenge from '../db/_getCurrentChallenge'; +import setCurrentChallenge from '../db/_setCurrentChallenge'; + +/** + * # registration + * + * ## GET + * + * Generates registration options. + * + * ### Parameters + * + * - `userId: string`. User must not already exist with this ID. + * - `displayName: string`. Name of the user to use in UIs. + * + * ### Responses + * + * - `200 OK`: Registration options. Client should consume these options using + * \@simplewebauthn/browser's `startRegistration()` method. + * - `400 BAD REQUEST`: client failed to provide all the required parameters. + * - `409 CONFLICT`: user with provided ID already exists. + * + * ## POST + * + * Verifies registration options from \@simplewebauthn/browser's + * `startRegistration()` method. + * + * ### Parameters + * + * - `userId: string`. User ID as provided in the registration options. The + * user ID must not already exist in the database. + * - `displayName: string`. Display name as provided in the registration + * options. + * - `registrationResponse: RegistrationResponseJSON`. The result of calling + * \@simplewebauthn/browser's `startRegistration()` method on the client. + * + * ### Response + * + * - `200 OK`: Returns a boolean string. `'true'` if verification was + * successful, `'false'` otherwise. + * - `400 BAD REQUEST`: either the client failed to provide all the required + * parameters, or the user was not associated with any existing challenges. + * In the latter case, the user probably skipped or messed up the step to get + * registration options. + * - `409 CONFLICT`: a user already exists with the given ID. + */ +export default async function handler(request: VercelRequest, response: VercelResponse): Promise<void> { + if (request.method === 'GET') { + (await handleGet(request))(response); + return; + } + + else if (request.method === 'POST') { + (await handlePost(request))(response); + return; + } +} + +async function handleGet(request: VercelRequest): Promise<(r: VercelResponse) => void> { + const userId = getFirstQueryParam('userId', request.query); + const displayName = getFirstQueryParam('displayName', request.query); + + if (!userId) { + return (response) => void ( + response.status(400).send('Expected body to contain userId, but none provided.') + ); + } + + if (!displayName) { + return (response) => void ( + response.status(400).send('Expected body to contain displayName, but none provided.') + ); + } + + const userAlreadyExists = await getUser(userId); + if (userAlreadyExists) { + return (response) => void ( + response.status(409).send(` + Failed to generate registration options because there was already a user + with the ID ${userId}. + `) + ); + } + + const options = await generateRegistrationOptionsForNewUser(userId, displayName); + setCurrentChallenge(options.user.id, options.challenge); + return (response) => void (response.status(200).json(options)); +} + +function getFirstQueryParam(key: string, query: VercelRequest['query']): string | null { + const value = query[key]; + if (!value) { + return null; + } + if (typeof(value) === 'string') { + return value; + } + return value[0] as string; +} + +async function handlePost(request: VercelRequest): Promise<(r: VercelResponse) => void> { + type Body = { + userId: string, + displayName: string, + registrationResponse: RegistrationResponseJSON + }; + const { userId, displayName, registrationResponse }: Body = request.body; + + if (!userId) { + return (response) => void ( + response.status(400).send('Expected body to contain userId, but none provided.') + ); + } + + if (!displayName) { + return (response) => void ( + response.status(400).send('Expected body to contain displayName, but none provided.') + ); + } + + if (!registrationResponse) { + return (response) => void ( + response.status(400).send('Expected body to contain registrationOptions, but none provided.') + ); + } + + const userIdAlreadyExists = await getUser(userId); + if (userIdAlreadyExists) { + return (response) => void ( + response.status(409).send(` + Failed to verify registration response because there was already a user + with the user ID ${userId}. + `) + ); + } + + const newUser = { id: userId, displayName }; + + const expectedChallenge = await getCurrentChallenge(newUser.id); + if (!expectedChallenge) { + return (response) => void ( + response.status(400).send(` + Failed to verify registration response because user ID ${newUser.id} was not + associated with any existing challenges in the database. + `) + ); + } + + const isValid = await verifyRegistrationResponse(newUser, expectedChallenge, registrationResponse); + return (response) => void ( + response.status(200).send(isValid ? 'true' : 'false') + ); +} |
