import { Buffer } from 'node:buffer'; import { error, json, type RequestHandler } from '@sveltejs/kit'; import type { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts } from '@simplewebauthn/server'; import { verifyAuthenticationResponse } from '@simplewebauthn/server'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import jwt from 'jsonwebtoken'; import { NoResultError } from 'kysely'; import { env } from '$env/dynamic/private'; import { getAuthSessionIdFromCookie, setJwtCookie } from '$lib/server/auth/utilities'; import { type Authenticator, findAuthenticatorByIdentifier, updateAuthenticator } from '$lib/server/data/authentication/authenticator'; import { deleteChallenges, resolveCurrentChallenge } from '$lib/server/data/authentication/challenge'; export const POST: RequestHandler = async function handler({ url, request, cookies, locals: { database } }) { const sessionId = getAuthSessionIdFromCookie(cookies); if (!sessionId) { throw error(401, 'Not authenticated'); } let challenge: string; try { challenge = await resolveCurrentChallenge(database, sessionId); } catch (err) { if (!(err instanceof Error)) { throw err; } throw error(400, `Failed to resolve challenge: ${err.message}`); } let response: AuthenticationResponseJSON; try { response = await request.json() as AuthenticationResponseJSON; } catch (err) { if (!(err instanceof Error)) { throw err; } await deleteChallenges(database, sessionId); return error(400, `Invalid request body: ${err.message}`); } const userId = response.response.userHandle; if (!userId) { await deleteChallenges(database, sessionId); return error(400, `Invalid payload: Missing user handle`); } let authenticator: Authenticator | null; try { authenticator = await findAuthenticatorByIdentifier( database, response.rawId ); } catch (err) { if (!(err instanceof NoResultError)) { await deleteChallenges(database, sessionId); throw err; } authenticator = null; } if (!authenticator || authenticator.user_id !== userId) { await deleteChallenges(database, sessionId); return error(400, 'Authenticator is not registered with this site'); } let verification: VerifiedAuthenticationResponse; try { verification = await verifyAuthenticationResponse({ response, expectedChallenge: `${challenge}`, expectedOrigin: url.origin, // <-- TODO: Use origin from RP ID instead expectedRPID: url.hostname, // <-- TODO: Use hostname from env instead authenticator: { credentialPublicKey: Buffer.from(authenticator.public_key, 'base64url'), credentialID: Buffer.from(authenticator.identifier, 'base64url'), counter: Number(authenticator.counter), transports: authenticator.transports }, requireUserVerification: true } satisfies VerifyAuthenticationResponseOpts); } catch (err) { if (!(err instanceof Error)) { throw err; } await deleteChallenges(database, sessionId); return error(400, err.message); } const { verified, authenticationInfo } = verification; const { newCounter: counter } = authenticationInfo; if (verified) { // Update the authenticator's counter in the DB to the newest count in the authentication await updateAuthenticator(database, response.rawId, { counter: counter.toString(), last_used_at: new Date() }); } await deleteChallenges(database, sessionId); // Sign the user token: We have authenticated the user successfully using the passcode, so they // may use this JWT to create their pass *key*. const token = jwt.sign({ authenticator: authenticator.id }, env.JWT_SECRET, { subject: authenticator.user_id }); // Set the cookie on the response: It will be included in any requests to the server, including // for tRPC. This makes for a nice, transparent, and "just works" authentication scheme. setJwtCookie(cookies, token); return json({ verified }); };