fix(auth): guard the webauthn login challenge kind and require user verification

Mirror the TOTP login path: both the security-key verify and verify-options
routes now reject any challenge whose kind is not "login" with the same generic
401, so a future step-up or non-login challenge can never mint a login session
through these endpoints. Set the registration and authentication ceremonies to
userVerification:"required" to match the server's UV enforcement — a key that
passes the browser but skips UV would otherwise fail server-side.
This commit is contained in:
Marc-André Bombeck committed 2026-06-27 22:22:46 +02:00
1 parent 51b8db166d
commit 2c1151dd40
3 files changed
+24 -4

No files matched your search

@@ -54,6 +54,13 @@ export const POST = apiHandler(async (request: NextRequest) => {
return apiError("Invalid or expired challenge", 401);
}
// Only a login-issued challenge may begin a login assertion here. Reject any
// other challenge kind with the same generic 401.
if (challenge.kind !== "login") {
annotate({ action: { name: "auth.mfa.verify.invalid_ticket" } });
return apiError("Invalid or expired challenge", 401);
}
const result = await createMfaAuthenticationOptions(challenge.userId);
if (!result) {
// No registered security key for this account — the client should fall
@@ -69,6 +69,14 @@ export const POST = apiHandler(async (request: NextRequest) => {
return apiError("Invalid or expired challenge", 401);
}
// Only a login-issued challenge may be redeemed for a session here. A future
// step-up or non-login challenge kind must never mint a login session through
// this endpoint, so reject anything but "login" with the same generic 401.
if (challenge.kind !== "login") {
annotate({ action: { name: "auth.mfa.verify.invalid_ticket" } });
return apiError("Invalid or expired challenge", 401);
}
const user = await prisma.user.findUnique({
where: { id: challenge.userId },
select: { id: true, username: true, onboardingCompletedAt: true },