From 2d0634cdc3d00b3e55cf773ff03c7dc841112d36 Mon Sep 17 00:00:00 2001 From: Joe Carstairs <65492573+Sycamost@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:22:47 +0000 Subject: 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 --- api/auth/registration.ts | 159 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 api/auth/registration.ts (limited to 'api/auth/registration.ts') 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 { + 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') + ); +} -- cgit v1.2.3