import { NextResponse } from "next/server"; import { verifyRegistrationResponse } from "@simplewebauthn/server"; import type { RegistrationResponseJSON } from "@simplewebauthn/server"; import { query } from "@/lib/db"; import { requireSession, UnauthorizedError } from "@/lib/auth/session"; import { origin, rpID } from "@/lib/auth/webauthn-config"; import { consumeChallenge } from "@/lib/auth/webauthn-challenges"; import { isSameOrigin } from "@/lib/origin"; export async function POST(request: Request) { if (!isSameOrigin(request)) { return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 }); } try { const session = await requireSession(); const body = (await request.json()) as { response: RegistrationResponseJSON; name?: string; }; const challenge = extractChallengeFromClientData(body.response); const pending = await consumeChallenge(challenge, "registration"); if (!pending || pending.userId !== session.user.id) { return NextResponse.json({ error: "challenge_expired" }, { status: 400 }); } const verification = await verifyRegistrationResponse({ response: body.response, expectedChallenge: challenge, expectedOrigin: origin(), expectedRPID: rpID(), }); if (!verification.verified || !verification.registrationInfo) { return NextResponse.json({ error: "verification_failed" }, { status: 400 }); } const { credential } = verification.registrationInfo; await query( `insert into passkey_credentials (user_id, credential_id, public_key, counter, transports, name) values ($1, $2, $3, $4, $5, $6)`, [ session.user.id, credential.id, Buffer.from(credential.publicKey), credential.counter, credential.transports ?? [], body.name ?? null, ] ); return NextResponse.json({ ok: true }); } catch (err) { if (err instanceof UnauthorizedError) { return NextResponse.json({ error: "unauthorized" }, { status: 401 }); } throw err; } } // clientDataJSON ist base64url-kodiertes JSON mit dem Feld "challenge" – // darüber wird die zugehörige, serverseitig gespeicherte Challenge // gefunden, ohne sie zusätzlich im Request-Body mitschicken zu müssen. function extractChallengeFromClientData(response: RegistrationResponseJSON): string { const json = JSON.parse( Buffer.from(response.response.clientDataJSON, "base64url").toString("utf8") ) as { challenge: string }; return json.challenge; }