Zweiten Faktor zur Grenze machen, nicht nur zur Anzeige
"Der zweite Faktor ist Pflicht" stand im Kopf von auth.ts, durchgesetzt wurde es aber nur im App-Layout. Ein Layout schuetzt, was durch das Layout gerendert wird - Server Actions und Route Handler laufen daran vorbei. Wer eine Passwort-Sitzung hatte, die TOTP-Einrichtung aber nie abgeschlossen hat, kam an die Daten. Die Pruefung steckt jetzt in erfordereBenutzer() selbst und damit ueber erfordereZugang() automatisch in jeder Server Action. Massgeblich ist der Datenbankstand, nicht das Feld im Sitzungstoken - das laesst sich ueber session.update() vom Client beeinflussen. Fuer Route Handler gibt es benutzerFuerRoute(): 401 bzw. 403 statt einer Umleitung. Eine Anmeldeseite als Inhalt einer Word-Datei waere die falsche Antwort. Dabei aufgefallen: Der Dokumentenabruf unter /api/dokumente/datei fragte nur nach der Anmeldung. Auch geschlossen. Ausnahmen sind aufgezaehlt und werden von tests/mfa-grenze.test.ts festgehalten - der Test liest den Quelltext und schlaegt an, sobald eine weitere dazukommt. Refs #38
This commit is contained in:
+68
-4
@@ -46,8 +46,46 @@ export async function aktuellerBenutzer(): Promise<User | null> {
|
||||
return benutzer
|
||||
}
|
||||
|
||||
/** Wie aktuellerBenutzer, leitet aber zum Anmeldeformular um. */
|
||||
/** Hat dieser Benutzer den zweiten Faktor abgeschlossen? */
|
||||
export function zweiterFaktorFertig(benutzer: User): boolean {
|
||||
return Boolean(benutzer.totpAktiviertAm && benutzer.totpSecretVerschluesselt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Der angemeldete Benutzer - mit abgeschlossenem zweitem Faktor.
|
||||
*
|
||||
* Die MFA-Pflicht steckt bewusst HIER und nicht im App-Layout. Ein Layout
|
||||
* schuetzt nur, was durch das Layout gerendert wird; Server Actions und
|
||||
* Route Handler laufen daran vorbei. Wer eine Passwort-Sitzung hat, aber
|
||||
* die TOTP-Einrichtung nie abgeschlossen hat, konnte sie frueher direkt
|
||||
* aufrufen - "MFA ist Pflicht" war damit eine Aussage ueber die
|
||||
* Oberflaeche, keine Sicherheitsgrenze.
|
||||
*
|
||||
* Massgeblich ist der Datenbankstand aus aktuellerBenutzer(), nicht das
|
||||
* Feld im Sitzungstoken: Das laesst sich ueber session.update() vom
|
||||
* Client beeinflussen.
|
||||
*
|
||||
* Die Ausnahmen sind klein und aufgezaehlt - Einrichtung des zweiten
|
||||
* Faktors und Abmelden. Wer eine weitere braucht, nimmt bewusst
|
||||
* erfordereBenutzerOhneZweitenFaktor() und begruendet es dort.
|
||||
*/
|
||||
export async function erfordereBenutzer(zielNachAnmeldung?: string): Promise<User> {
|
||||
const benutzer = await erfordereBenutzerOhneZweitenFaktor(zielNachAnmeldung)
|
||||
if (!zweiterFaktorFertig(benutzer)) {
|
||||
redirect('/zwei-faktor/einrichten')
|
||||
}
|
||||
return benutzer
|
||||
}
|
||||
|
||||
/**
|
||||
* Nur fuer die Handvoll Stellen, die VOR abgeschlossener MFA erreichbar
|
||||
* sein muessen: die Einrichtung selbst und das Abmelden. Jede weitere
|
||||
* Verwendung ist ein Loch in der MFA-Pflicht und braucht eine
|
||||
* Begruendung im Code.
|
||||
*/
|
||||
export async function erfordereBenutzerOhneZweitenFaktor(
|
||||
zielNachAnmeldung?: string,
|
||||
): Promise<User> {
|
||||
const benutzer = await aktuellerBenutzer()
|
||||
if (!benutzer) {
|
||||
const ziel = zielNachAnmeldung ? `?weiter=${encodeURIComponent(zielNachAnmeldung)}` : ''
|
||||
@@ -57,11 +95,37 @@ export async function erfordereBenutzer(zielNachAnmeldung?: string): Promise<Use
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den eingerichteten zweiten Faktor. Wird im App-Layout
|
||||
* aufgerufen: ohne TOTP kommt niemand an Fachdaten.
|
||||
* Fuer Route Handler: Statt umzuleiten wird abgewiesen.
|
||||
*
|
||||
* Ein Redirect ist bei einer Datei- oder Kalenderantwort die falsche
|
||||
* Antwort - der Aufrufer will Daten, nicht eine Anmeldeseite als Inhalt.
|
||||
*/
|
||||
export async function benutzerFuerRoute(): Promise<
|
||||
{ ok: true; benutzer: User } | { ok: false; antwort: Response }
|
||||
> {
|
||||
const benutzer = await aktuellerBenutzer()
|
||||
if (!benutzer) {
|
||||
return { ok: false, antwort: new Response('Nicht angemeldet.', { status: 401 }) }
|
||||
}
|
||||
if (!zweiterFaktorFertig(benutzer)) {
|
||||
return {
|
||||
ok: false,
|
||||
antwort: new Response('Zweiter Faktor ist nicht eingerichtet.', { status: 403 }),
|
||||
}
|
||||
}
|
||||
return { ok: true, benutzer }
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzwingt den eingerichteten zweiten Faktor.
|
||||
*
|
||||
* Bleibt als eigener Aufruf im App-Layout bestehen, obwohl
|
||||
* erfordereBenutzer() das inzwischen selbst tut: Zwei Sperren an
|
||||
* derselben Stelle kosten nichts und die aeussere ist die, die man beim
|
||||
* Lesen des Layouts sieht.
|
||||
*/
|
||||
export async function erfordereZweitenFaktor(benutzer: User): Promise<void> {
|
||||
if (!benutzer.totpAktiviertAm || !benutzer.totpSecretVerschluesselt) {
|
||||
if (!zweiterFaktorFertig(benutzer)) {
|
||||
redirect('/zwei-faktor/einrichten')
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user