feat(auth): WebAuthn (YubiKey/Passkeys) + Admin-2FA-Reset-Button (v1.8.1)
Some checks failed
Build and Push Docker Image / build-and-push (push) Has been cancelled
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>
This commit is contained in:
37
src/app/api/auth/mfa-login/webauthn/options/route.ts
Normal file
37
src/app/api/auth/mfa-login/webauthn/options/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user