Anmeldung haerten: Einmalcodes, Anschluss, Host, Passkey-Vorzimmer
Vier Stellen, an denen eine Zusage nicht gehalten wurde. 1. Ein TOTP-Code galt mehrfach. Der Kommentar in totp.ts versprach seit dem Anfang, der Aufrufer speichere das getroffene Zeitfenster - getan hat es niemand. Damit liess sich ein abgelesener Code innerhalb von 90 Sekunden noch einmal einloesen. Jetzt geht jede Pruefung durch zweitenFaktorEinloesen(), und die Bedingung steht im WHERE von updateMany: Zwei gleichzeitige Anmeldungen mit demselben Code treffen nicht beide eine Zeile. 2. Der Anschluss kam aus dem ERSTEN Eintrag von "x-forwarded-for". Das ist der Teil, den der Aufrufer selbst schreibt. Wer bei jeder Anfrage eine andere Zahl hineinschrieb, sah fuer die Anmeldesperre wie tausend Anschluesse aus - die Zaehlung war nicht nur wirkungslos, sondern irrefuehrend. Jetzt von hinten gelesen, und nur wenn VERTRAUE_PROXY gesetzt ist. Ohne Proxy ist der Kopf fremdbestimmt; dann lieber keine Zaehlung nach Anschluss als eine, die eine Kopfzeile aushebelt. Beim ersten Mal steht eine Zeile im Log, damit das Fehlen auffaellt. 3. trustHost stand fest auf true. Ist AUTH_URL gesetzt - der dokumentierte Betrieb setzt es, Passkeys brauchen es -, gibt es keinen Grund, die Adresse aus einer Kopfzeile von aussen abzuleiten. AUTH_TRUST_HOST bleibt wirksam, damit die dokumentierte Einstellung nicht stillschweigend uebergangen wird. 4. Passkeys liessen sich ohne erneute Bestaetigung anlegen und entfernen. Ein Passkey ist ein vollwertiger Zugang, der einen Passwortwechsel ueberlebt - an einem unbeaufsichtigten Rechner waren das zwei Klicks. Jetzt dasselbe Vorzimmer wie beim Vollexport. Dabei musste alsFehler() lernen, die Weiterleitung von Next durchzulassen: Sonst verschwand die Zwischenfrage in "Der Passkey konnte nicht geprueft werden". Refs #38
This commit is contained in:
@@ -20,7 +20,11 @@ import {
|
||||
registrierungPruefen,
|
||||
registrierungVorbereiten,
|
||||
} from '@/lib/auth/passkey'
|
||||
import { erfordereBenutzer, reauthBestaetigen } from '@/lib/auth/sitzung'
|
||||
import {
|
||||
erfordereBenutzer,
|
||||
erfordereFrischeAnmeldung,
|
||||
reauthBestaetigen,
|
||||
} from '@/lib/auth/sitzung'
|
||||
|
||||
/**
|
||||
* Server Actions rund um Passkeys.
|
||||
@@ -42,6 +46,21 @@ export type PasskeyStart<T> = { ok: true; optionen: T } | { ok: false; fehler: s
|
||||
/** Ergebnis eines abschliessenden Schritts. */
|
||||
export type PasskeyErgebnis = { ok: true; meldung?: string } | { ok: false; fehler: string }
|
||||
|
||||
/**
|
||||
* Erkennt die Weiterleitung, die Next.js intern als Ausnahme wirft.
|
||||
* Wie in anmelden.ts ueber das digest-Feld statt ueber einen Import aus
|
||||
* next/dist - interne Pfade aendern sich zwischen Nebenversionen.
|
||||
*/
|
||||
function istWeiterleitung(fehler: unknown): boolean {
|
||||
return (
|
||||
typeof fehler === 'object' &&
|
||||
fehler !== null &&
|
||||
'digest' in fehler &&
|
||||
typeof (fehler as { digest?: unknown }).digest === 'string' &&
|
||||
(fehler as { digest: string }).digest.startsWith('NEXT_REDIRECT')
|
||||
)
|
||||
}
|
||||
|
||||
function alsFehler(fehler: unknown): { ok: false; fehler: string } {
|
||||
if (fehler instanceof PasskeyFehler) {
|
||||
return { ok: false, fehler: PASSKEY_MELDUNG[fehler.grund] }
|
||||
@@ -50,6 +69,18 @@ function alsFehler(fehler: unknown): { ok: false; fehler: string } {
|
||||
return { ok: false, fehler: PASSKEY_MELDUNG.ungueltig }
|
||||
}
|
||||
|
||||
/**
|
||||
* Wie alsFehler, wirft aber die Weiterleitung von Next durch.
|
||||
*
|
||||
* Ohne das verschwindet der Ruf nach einer erneuten Bestaetigung in der
|
||||
* allgemeinen Meldung "Der Passkey konnte nicht geprueft werden" - der
|
||||
* Benutzer sieht einen Fehler, wo eine Zwischenfrage gemeint war.
|
||||
*/
|
||||
function alsFehlerOderWeiter(fehler: unknown): { ok: false; fehler: string } {
|
||||
if (istWeiterleitung(fehler)) throw fehler
|
||||
return alsFehler(fehler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst die Antwort des Browsers.
|
||||
*
|
||||
@@ -74,14 +105,26 @@ function antwortLesen<T>(roh: string): T {
|
||||
// Einrichten (angemeldet, unter "Mein Konto")
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ein Passkey ist ein vollwertiger Zugang - wer einen anlegt, kommt danach
|
||||
* ohne Passwort und ohne Code herein. Deshalb dasselbe Vorzimmer wie beim
|
||||
* Vollexport: Die Anmeldung muss frisch sein.
|
||||
*
|
||||
* Das ist genau der Fall, gegen den die erneute Bestaetigung da ist: Eine
|
||||
* offene Sitzung an einem unbeaufsichtigten Rechner ist in diesem Umfeld
|
||||
* der wahrscheinlichste Angriffsweg. Wer daran vorbeikaeme, richtete sich
|
||||
* in zwei Klicks einen dauerhaften eigenen Zugang ein - und der ueberlebte
|
||||
* auch einen Passwortwechsel.
|
||||
*/
|
||||
export async function passkeyEinrichtungStarten(): Promise<
|
||||
PasskeyStart<PublicKeyCredentialCreationOptionsJSON>
|
||||
> {
|
||||
try {
|
||||
const benutzer = await erfordereBenutzer()
|
||||
await erfordereFrischeAnmeldung(benutzer, '/konto')
|
||||
return { ok: true, optionen: await registrierungVorbereiten(benutzer) }
|
||||
} catch (fehler) {
|
||||
return alsFehler(fehler)
|
||||
return alsFehlerOderWeiter(fehler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +140,11 @@ export async function passkeyEinrichtungAbschliessen(
|
||||
): Promise<PasskeyErgebnis> {
|
||||
try {
|
||||
const benutzer = await erfordereBenutzer()
|
||||
// Auch im zweiten Schritt: Zwischen Optionen holen und Antwort
|
||||
// einreichen liegt der Griff zum Sensor, und die Frist kann in dieser
|
||||
// Zeit ablaufen. Ohne die Pruefung waere der erste Schritt nur eine
|
||||
// Empfehlung.
|
||||
await erfordereFrischeAnmeldung(benutzer, '/konto')
|
||||
|
||||
const geprueft = NameSchema.safeParse(name)
|
||||
if (!geprueft.success) {
|
||||
@@ -122,7 +170,7 @@ export async function passkeyEinrichtungAbschliessen(
|
||||
revalidatePath('/konto')
|
||||
return { ok: true, meldung: `„${passkey.name}“ ist eingerichtet.` }
|
||||
} catch (fehler) {
|
||||
return alsFehler(fehler)
|
||||
return alsFehlerOderWeiter(fehler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +178,15 @@ export async function passkeyEinrichtungAbschliessen(
|
||||
// Verwalten
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Entfernen braucht die frische Anmeldung genauso wie das Anlegen: Wer
|
||||
* einem Betreuer alle Passkeys wegnimmt, sperrt ihn von dem Weg aus, auf
|
||||
* dem er hereinkommt.
|
||||
*/
|
||||
export async function passkeyEntfernen(id: string): Promise<PasskeyErgebnis> {
|
||||
try {
|
||||
const benutzer = await erfordereBenutzer()
|
||||
await erfordereFrischeAnmeldung(benutzer, '/konto')
|
||||
|
||||
// Zeilenweise Pruefung: Es genuegt nicht, dass die ID existiert - sie
|
||||
// muss zu DIESEM Benutzer gehoeren. deleteMany mit userId im WHERE
|
||||
@@ -163,7 +217,7 @@ export async function passkeyEntfernen(id: string): Promise<PasskeyErgebnis> {
|
||||
revalidatePath('/konto')
|
||||
return { ok: true, meldung: `„${passkey.name}“ wurde entfernt.` }
|
||||
} catch (fehler) {
|
||||
return alsFehler(fehler)
|
||||
return alsFehlerOderWeiter(fehler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,21 +241,6 @@ function sicheresZiel(ziel: string): string {
|
||||
return ziel
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt die Weiterleitung, die Next.js intern als Ausnahme wirft.
|
||||
* Wie in anmelden.ts ueber das digest-Feld statt ueber einen Import aus
|
||||
* next/dist - interne Pfade aendern sich zwischen Nebenversionen.
|
||||
*/
|
||||
function istWeiterleitung(fehler: unknown): boolean {
|
||||
return (
|
||||
typeof fehler === 'object' &&
|
||||
fehler !== null &&
|
||||
'digest' in fehler &&
|
||||
typeof (fehler as { digest?: unknown }).digest === 'string' &&
|
||||
(fehler as { digest: string }).digest.startsWith('NEXT_REDIRECT')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uebersetzt den Fehlercode aus authorize() in einen lesbaren Satz.
|
||||
* "passkey:<grund>" kommt aus PasskeyAnmeldeFehler, alles andere aus dem
|
||||
|
||||
Reference in New Issue
Block a user