"use server"; import { generateRegistrationOptions, generateAuthenticationOptions, verifyRegistrationResponse, verifyAuthenticationResponse, RegistrationResponseJSON, AuthenticationResponseJSON, } from "@simplewebauthn/server"; import { sessions } from "@/session/store"; import { requestInfo } from "rwsdk/worker"; import { db } from "@/db"; import { env } from "cloudflare:workers"; const IS_DEV = process.env.NODE_ENV === "development"; function getWebAuthnConfig(request: Request) { const rpID = env.WEBAUTHN_RP_ID ?? new URL(request.url).hostname; const rpName = IS_DEV ? "Development App" : env.WEBAUTHN_APP_NAME; return { rpName, rpID, }; } export async function startPasskeyRegistration(username: string) { const { rpName, rpID } = getWebAuthnConfig(requestInfo.request); const { headers } = requestInfo; const options = await generateRegistrationOptions({ rpName, rpID, userName: username, authenticatorSelection: { // Require the authenticator to store the credential, enabling a username-less login experience residentKey: "required", // Prefer user verification (biometric, PIN, etc.), but allow authentication even if it's not available userVerification: "preferred", }, }); await sessions.save(headers, { challenge: options.challenge }); return options; } export async function startPasskeyLogin() { const { rpID } = getWebAuthnConfig(requestInfo.request); const { headers } = requestInfo; const options = await generateAuthenticationOptions({ rpID, userVerification: "preferred", allowCredentials: [], }); await sessions.save(headers, { challenge: options.challenge }); return options; } export async function finishPasskeyRegistration( username: string, registration: RegistrationResponseJSON, ) { const { request, headers } = requestInfo; const { origin } = new URL(request.url); const session = await sessions.load(request); const challenge = session?.challenge; if (!challenge) { return false; } const verification = await verifyRegistrationResponse({ response: registration, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: env.WEBAUTHN_RP_ID || new URL(request.url).hostname, }); if (!verification.verified || !verification.registrationInfo) { return false; } await sessions.save(headers, { challenge: null }); const user = await db.user.create({ data: { username, }, }); await db.credential.create({ data: { userId: user.id, credentialId: verification.registrationInfo.credential.id, publicKey: verification.registrationInfo.credential.publicKey, counter: verification.registrationInfo.credential.counter, }, }); return true; } export async function finishPasskeyLogin(login: AuthenticationResponseJSON) { const { request, headers } = requestInfo; const { origin } = new URL(request.url); const session = await sessions.load(request); const challenge = session?.challenge; if (!challenge) { return false; } const credential = await db.credential.findUnique({ where: { credentialId: login.id, }, }); if (!credential) { return false; } const verification = await verifyAuthenticationResponse({ response: login, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: env.WEBAUTHN_RP_ID || new URL(request.url).hostname, requireUserVerification: false, credential: { id: credential.credentialId, publicKey: credential.publicKey, counter: credential.counter, }, }); if (!verification.verified) { return false; } await db.credential.update({ where: { credentialId: login.id, }, data: { counter: verification.authenticationInfo.newCounter, }, }); const user = await db.user.findUnique({ where: { id: credential.userId, }, }); if (!user) { return false; } await sessions.save(headers, { userId: user.id, challenge: null, }); return true; }