import type { APIRoute } from "astro"; import { verifyAuthenticationResponse } from "@simplewebauthn/server"; import { db } from "../../../../../db"; import { users, passkeys, passkeyChallenges } from "../../../../../db/schema"; import { eq, and, gt } from "drizzle-orm"; import { createSession } from "../../../../../lib/auth"; export const POST: APIRoute = async ({ request, cookies }) => { const body = await request.json(); const { id } = body; const passkey = await db.query.passkeys.findFirst({ where: eq(passkeys.id, id), }); if (!passkey) { return new Response(JSON.stringify({ error: "Passkey not found" }), { status: 400, }); } const user = await db.query.users.findFirst({ where: eq(users.id, passkey.userId), }); if (!user) return new Response(null, { status: 400 }); const clientDataJSON = Buffer.from( body.response.clientDataJSON, "base64url", ).toString("utf-8"); const clientData = JSON.parse(clientDataJSON); const challenge = clientData.challenge; const dbChallenge = await db.query.passkeyChallenges.findFirst({ where: and( eq(passkeyChallenges.challenge, challenge), gt(passkeyChallenges.expiresAt, new Date()), ), }); if (!dbChallenge) { return new Response( JSON.stringify({ error: "Invalid or expired challenge" }), { status: 400, }, ); } let verification; try { verification = await verifyAuthenticationResponse({ response: body, expectedChallenge: challenge as string, expectedOrigin: new URL(request.url).origin, expectedRPID: new URL(request.url).hostname, credential: { id: passkey.id, publicKey: new Uint8Array(Buffer.from(passkey.publicKey, "base64")), counter: passkey.counter, transports: passkey.transports ? JSON.parse(passkey.transports) : undefined, }, }); } catch (error) { return new Response(JSON.stringify({ error: (error as Error).message }), { status: 400, }); } if (verification.verified) { const { authenticationInfo } = verification; await db .update(passkeys) .set({ counter: authenticationInfo.newCounter, lastUsedAt: new Date(), }) .where(eq(passkeys.id, passkey.id)); const { sessionId, expiresAt } = await createSession(user.id); cookies.set("session_id", sessionId, { path: "/", httpOnly: true, secure: import.meta.env.PROD, sameSite: "lax", expires: expiresAt, }); await db .delete(passkeyChallenges) .where(eq(passkeyChallenges.challenge, challenge)); return new Response(JSON.stringify({ verified: true })); } return new Response(JSON.stringify({ verified: false }), { status: 400 }); };