Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
- 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>
38 lines
1.5 KiB
TypeScript
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 })
|
|
}
|
|
}
|