summaryrefslogtreecommitdiff
path: root/api/auth/registration.ts
diff options
context:
space:
mode:
authorJoe Carstairs <65492573+Sycamost@users.noreply.github.com>2023-12-21 21:22:47 +0000
committerJoe Carstairs <jcarstairs@scottlogic.com>2024-01-29 10:46:46 +0000
commit2d0634cdc3d00b3e55cf773ff03c7dc841112d36 (patch)
tree424f0b9ce2907e3d58336a2b25233bf0f9bead9d /api/auth/registration.ts
parentf44b2a82b74337c6424c576fdbf4c1376166e553 (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.ts159
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')
+ );
+}