Files
Lageplan/src/app/api/auth/mfa-login/webauthn/options/route.ts
Pepe Ziberi f58f1b31bf
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
feat(auth): WebAuthn (YubiKey/Passkeys) + Admin-2FA-Reset-Button (v1.8.1)
- WebAuthn/FIDO2 via @simplewebauthn v13: Registrierung (Session) + Login-Faktor (öffentlich),
  zustandslose Challenge-Token (JWT), Zähler/Klon-Schutz, Credentials in webauthn_credentials
  - Routen: /api/auth/mfa/webauthn/register/{options,verify}, DELETE /webauthn/[id],
    /api/auth/mfa-login/webauthn/{options,verify}
- Settings: Sicherheitsschlüssel/Passkey hinzufügen + Liste + entfernen
- Login: "Mit Sicherheitsschlüssel/Passkey anmelden" (neben TOTP)
- Admin: 2FA-Zurücksetzen-Button in der Benutzerliste (nutzt bestehende mfa-reset-API)
- Sicherheitsseite + TOM-Doku: 2FA als vorhanden dokumentiert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 18:15:03 +02:00

38 lines
1.5 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { generateAuthenticationOptions } from '@simplewebauthn/server'
import { prisma } from '@/lib/db'
import { verifyMfaToken } from '@/lib/auth'
import { rpFromRequest, signChallenge } from '@/lib/webauthn'
// POST (öffentlich): Authentisierungs-Optionen für den zweiten Faktor per WebAuthn.
// body: { mfaToken }
export async function POST(req: NextRequest) {
try {
const body = await req.json().catch(() => ({}))
const mfa = await verifyMfaToken(String(body.mfaToken || ''))
if (!mfa) return NextResponse.json({ error: 'Sitzung abgelaufen.' }, { status: 401 })
const { rpID } = rpFromRequest(req)
const creds = await (prisma as any).webAuthnCredential.findMany({
where: { userId: mfa.userId },
select: { credentialId: true, transports: true },
})
if (creds.length === 0) return NextResponse.json({ error: 'Kein Sicherheitsschlüssel registriert.' }, { status: 400 })
const options = await generateAuthenticationOptions({
rpID,
allowCredentials: creds.map((c: any) => ({
id: c.credentialId,
transports: c.transports ? JSON.parse(c.transports) : undefined,
})),
userVerification: 'preferred',
})
const challengeToken = await signChallenge(options.challenge, mfa.userId, 'wa-auth')
return NextResponse.json({ options, challengeToken })
} catch (error) {
console.error('WebAuthn auth options error:', error)
return NextResponse.json({ error: 'Serverfehler' }, { status: 500 })
}
}