diff options
| author | Joe Carstairs <jcarstairs@scottlogic.com> | 2024-01-22 17:15:48 +0000 |
|---|---|---|
| committer | Joe Carstairs <jcarstairs@scottlogic.com> | 2024-01-29 10:51:49 +0000 |
| commit | 48bf60f8347db2a22f207441d463e649a9e4e08c (patch) | |
| tree | c95a87e9d2f1667f76809ae8ba53e95a35062eff /api/auth | |
| parent | 1c5ee8b6f0d22ca9aebdfda88b0ceca8aededbf5 (diff) | |
Adds an auth/authentication endpoint
Diffstat (limited to 'api/auth')
| -rw-r--r-- | api/auth/authentication.ts | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/api/auth/authentication.ts b/api/auth/authentication.ts new file mode 100644 index 0000000..80966bc --- /dev/null +++ b/api/auth/authentication.ts @@ -0,0 +1,164 @@ +import type { AuthenticationResponseJSON } from '@simplewebauthn/server/script/deps'; +import type { VercelRequest, VercelResponse } from '@vercel/node'; +import generateAuthenticationOptions from './_generateAuthenticationOptions'; +import { getFirstQueryParam } from '_getFirstQueryParam'; +import getUser from 'db/_getUser'; +import setCurrentChallenge from 'db/_setCurrentChallenge'; +import getCurrentChallenge from 'db/_getCurrentChallenge'; +import verifyAuthenticationResponse from './_verifyAuthenticationResponse'; +import updateAuthenticator from 'db/_updateAuthenticator'; +import getAuthenticators from 'db/_getAuthenticators'; + +/** + * # authentication + * + * ## GET + * + * Generates authentication options. + * + * ### Parameters + * + * - `userId: string`. A user must exist with this ID. + * + * ### Responses + * + * - `200 OK`: Authentication options. Client should consume these using + * \@simplewebauthn/browser’s `startAuthentication()` method. + * - `400 BAD REQUEST`: No `userId` was provided. + * - `404 NOT FOUND`: No user exists with the provided `userId`. + * + * ## POST + * + * Verifies authentication response from \@simplewebauthn/browser’s + * `startAuthentication()` method. + * + * ### Parameters + * + * - `userId: string`. A user must exist with this ID, and must have an + * existing challenge and at least one authenticator in the database. + * - `authenticationResponse: AuthenticationResponseJSON`. The response from + * \@simplewebauthn/browser’s `startAuthentication()` method. + * + * ### Responses + * + * - `200 OK`: Returns a boolean string. `'true'` if verification was + * successful, and `'false'` otherwise. + * - `400 BAD REQUEST`: No `userId` was provided. + * - `404 NOT FOUND`: No user exists with the provided `userId`. + * - `500 INTERNAL SERVER ERROR`: The user with the provided `userId` did not + * have any existing challenges or authenticators in the database. + * - `501 NOT IMPLEMENTED`: The user with the provided `userId` had more than + * one authenticator in the database. This is not yet supported. + */ +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); + + if (!userId) { + return (response) => void ( + response.status(400).send('Expected body to contain userId, but none provided.') + ); + } + + const user = await getUser(userId); + if (!user) { + return (response) => void ( + response.status(404).send(` + Failed to generate authentication options because there was no user + with the ID ${userId}. + `) + ); + } + + const options = await generateAuthenticationOptions(user); + + await setCurrentChallenge(user.id, options.challenge); + return (response) => void response.json(options); +} + +async function handlePost(request: VercelRequest): Promise<(r: VercelResponse) => void> { + type Body = { + userId: string; + authenticationResponse: AuthenticationResponseJSON; + }; + const { userId, authenticationResponse }: Body = request.body; + + if (!userId) { + return (response) => void ( + response.status(400).send('Expected body to contain userId, but none provided.') + ); + } + + if (!authenticationResponse) { + return (response) => void ( + response.status(400).send('Expected body to contain authenticationResponse, but none provided.') + ); + } + + const user = await getUser(userId); + if (!user) { + return (response) => void ( + response.status(404).send(` + Failed to verify authentication response because no user existed with + ID ${userId}. + `) + ); + } + + const expectedChallenge = await getCurrentChallenge(userId); + if (!expectedChallenge) { + return (response) => void ( + response.status(500).send(` + Failed to verify authentication response because user with ID ${userId} + did not have any existing challenges in the database. + `) + ); + } + + const authenticators = await getAuthenticators(user.id, ['id', 'publicKey', 'counter', 'transports']); + if (!authenticators || authenticators.length === 0) { + return (response) => void ( + response.status(500).send(` + Failed to verify authentication response because user with ID ${userId} + did not have any authenticators in the database. + `) + ); + } else if (authenticators.length > 1) { + // TODO: implement support for multiple authenticators + return (response) => void ( + response.status(501).send(` + Failed to verify authentication response because user with ID ${userId} + had more than one authenticator in the database. We only currently + support a single authenticator per user. + `) + ); + } + + const isVerified = await verifyAuthenticationResponse( + authenticators[0], + authenticationResponse, + expectedChallenge + ); + + if (isVerified) { + for (const authenticator of authenticators) { + updateAuthenticator(authenticator.id, { counter: authenticator.counter + 1 }); + } + } + + return (response) => void ( + response.status(200).send(isVerified ? 'true' : 'false') + ); +} + |
