Sie befinden sich hier: start » de » Interne Dokumentation » DokuWiki-Erweiterungen (WvdS) » Identity » Konzepte

Konzepte

Wie wkidentity arbeitet: was das Paket führt, woran eine Absicherung hängt und warum die Abläufe so geschnitten sind. Was Sie tun wollen, steht auf Identity; was Sie nachschlagen wollen, in der Technischen Referenz.

Definition

wkidentity führt die Identität eines Kontos über seine ganze Lebensdauer. Zwei Gegenstände, die sich ein Paket teilen, weil sie dieselbe Frage beantworten — wer ist das:

  • Zweiter Faktor. RFC-6238-TOTP (kompatibel mit Microsoft Authenticator, Google Authenticator und anderen Standard-Apps) als Selbstbedienung auf der Profilseite (do=profile), wahlweise ein Einmalcode per E-Mail, dazu eine Pflicht je Namensraum und eine optionale Abfrage beim Anmelden. Ergänzt um eine interne Testseite, die ein beliebiges Base32-Secret gegen die eigene TOTP-Rechnung prüft, ohne dass das Secret den Browser verlässt.
  • Einmalcode-Abläufe. Kennwort-Zurücksetzen, Selbstregistrierung und Einladung durch einen Administrator — gebaut nach dem Grundsatz, dass niemand außer dem Kontoinhaber das Kennwort je kennt. Siehe Einmalcode-Abläufe.

Das Paket ist kein authtype: es ersetzt die Benutzerverwaltung nicht, sondern legt sich vor und neben sie.

Assurance ist eine Sitzungs-, keine Kontoeigenschaft

isEnrolled() bleibt für immer wahr, sobald ein Faktor eingerichtet ist — eine Kontoeigenschaft ohne Zeitbezug. currentAssurance() beantwortet eine andere Frage: was diese Sitzung jetzt bewiesen hat. Ein seit Jahren eingerichtetes Konto kann eine Sitzung haben, die nie einen Step-up durchlaufen hat.

Genau diese beiden wurden in enforceNamespacePolicy() bis zum 8. August 2026 verwechselt — der eigentliche Auslöser der Sicherheits-Härtung. Der ACL-Haken prüfte nur isEnrolled($user); bei deaktiviertem enforce_login (Vorgabe) reichte damit ein einmal eingerichteter Faktor für immer, unabhängig davon, ob die aktuelle Sitzung ihn je bewiesen hatte. enforceNamespacePolicy() löst die geltende Anforderung jetzt über PolicyStore::effectiveRequirement() auf (siehe „Richtlinien-Engine„ unten) und prüft sie gegen echte Sitzungs-Absicherung (helper_plugin_wkidentity_contract::satisfies()); isEnrolled() entscheidet nur noch, ob die Anforderung überhaupt erfüllbar wäre — ein nicht enrolltes Konto wird weiterhin sofort abgewiesen, nie in einen aussichtslosen Step-up geschickt. Eine Ablehnung nennt jetzt einen von zwei Gründen: „nicht enrollt“ (Link zum Enrollment) oder „enrollt, aber diese Sitzung hat es nicht bewiesen„ (Link zu einem echten Step-up, der zur ursprünglichen Seite zurückkehrt, do=wkidentity_stepup_start). Einzelheiten: docs/threat-model.md, Befund F6.
Stufe Bedeutung
NONE keine Sitzung oder anonym
AUTHENTICATED nur Kern-Login, kein zweiter Faktor diese Sitzung
TOTP zweiter Faktor über die Authenticator-App bewiesen
MAIL_OTP zweiter Faktor über den gemailten Code bewiesen

TOTP und MAIL_OTP sind Alternativen, keine Leiter — nur MFA_ANY als Anforderung akzeptiert beides. Ein Konto kann seit der Sicherheits-Härtung beide Faktoren gleichzeitig führen (unabhängig voneinander einrichtbar/entfernbar); eine einzelne Sitzung beweist trotzdem immer nur einen konkreten davon.

Eine bewiesene Stufe läuft jetzt ab. Die Einstellung assurance_ttl (Vorgabe 3600 Sekunden) bestimmt, wie lange ein abgeschlossener Step-up — oder ein beim Login bewiesener Faktor — als Absicherung dieser Sitzung zählt, bevor er auf AUTHENTICATED zurückfällt (nicht auf NONE: die Anmeldung selbst bleibt bestehen, nur der zusätzliche Faktornachweis verfällt). Das ersetzt die frühere Festlegung „keine Stufe verfällt innerhalb einer Sitzung“.

Step-up mit geprüftem Rücksprungziel

Bislang nicht existent: jede Weiterleitung in LoginGate/action.php führte bedingungslos auf $conf['start'], nie zurück zum ursprünglichen Ziel. Neu: require() leitet bei Bedarf auf do=wkidentity_stepup um — dieselbe Formularschale wie do=wkidentity_verify (Wiederverwendung von verifyCode()/verifyEmailChallenge(), keine neue Kryptographie) — und kehrt nach einem korrekten Code zum geprüften Ziel zurück.

Das Rücksprungziel ist niemals eine rohe URL. Angenommen wird ausschließlich eine strukturierte Referenz — eine über cleanID() bereinigte Seiten-Id, oder eine bei wkfluentui tatsächlich vorhandene Contract-v1-Beitrags-Id. Alles andere fällt auf $conf['start'] zurück, denselben sicheren Vorgabewert, den jede bestehende Weiterleitung in diesem Plugin schon nutzt. Diese Disziplin spiegelt bewusst, wie DokuWiki-Kern sein eigenes Anmeldeformular-Rücksprungziel auflöst — nie eine vom Aufrufer gewählte URL.

Richtlinien-Engine: Namespace, Gruppe, Benutzer und eine dokumentierte Konfliktauflösung

Seit der Sicherheits-Härtung berücksichtigt PolicyStore vier unabhängige Achsen statt nur Namespace: namespace (Präfix-Vergleich wie conf/acl.auth.php), group (jede Gruppe der aktuellen Sitzung), user (genauer Login) sowie plugin/module/view/command/ adminAction (genaue Id, vom aufrufenden Plugin selbst benannt). Eine Anfrage kann mehreren Achsen gleichzeitig unterliegen — PolicyStore::effectiveRequirement($scope, $user, $groups) faltet jeden zutreffenden Eintrag über PolicyStore::combine() zusammen.

combine() behandelt die vier Anforderungswerte als kleinen Verband, keine Zahlenskala — genau weil TOTP und MAIL_OTP Alternativen sind, keine Leiter. AUTHENTICATED ist das neutrale Element (verändert die andere Seite nicht); MFA_ANY liegt unterhalb beider konkreten Faktoren (jeder von ihnen erfüllt „irgendein MFA„ bereits); TOTP und MAIL_OTP sind ansonsten unvergleichbar. Jede definierte Kombination ergibt die strengere der beiden Eingaben — nie schwächer als eine von ihnen. Genau das macht „eine Richtlinie kann die eigene Mindestanforderung eines Plugins nur anheben, nie stillschweigend abschwächen“ beweisbar statt bloß versprochen.

Die eine undefinierte Zelle ist ein echter Konflikt: ein Namespace-Eintrag verlangt TOTP, während ein Gruppen- oder Benutzereintrag MAIL_OTP verlangt — zwei verschiedene konkrete Faktoren, und keine einzelne currentAssurance()-Stufe kann beide zugleich erfüllen. effectiveRequirement() löst das nach Spezifität: gefaltet wird von unspezifisch nach spezifisch (Namespace/direkter Scope, dann jede passende Gruppe, dann der Benutzer — und, eine Ebene höher in satisfies()/require(), die eigene, explizite Laufzeit-Anforderung des aufrufenden Plugins zuletzt, als spezifischste Stimme überhaupt). Der Konflikt selbst wird über Trace::note() protokolliert — eine sich selbst widersprechende Richtlinie verdient die Aufmerksamkeit eines Betreibers, keinen stillen Münzwurf.

Ein Namespace, der nur in der älteren required_namespaces-Liste benannt ist (die vor required_scopes entstand und keine eigene Anforderungsstufe trägt), löst weiterhin — zurückgestuft auf MFA_ANY — auf: die naheliegendste treue Lesart dessen, was diese Liste immer bedeutet hat („ein beliebiger enrollter Faktor genügt„), jetzt geprüft gegen echte Sitzungs-Absicherung statt bloße Kontoeinschreibung.

Workflow (Enrollment)

Das Enrollment ist bewusst ein Zwei-Schritt-Vorgang und kein Instant-On, damit ein Nutzer 2FA nie mit einem Secret „aktiviert“, das seine Authenticator-App tatsächlich nie gescannt hat:

  1. Start — Button „Einrichten“ auf do=profile (nur sichtbar, wenn noch nicht enrollt). Erzeugt ein neues Zufalls-Secret (auth_randombytes(), 160 Bit) und legt es nur in der Session ab — noch nichts wird auf Platte geschrieben.
  2. Bestätigen — die Seite rendert QR-Code + manuelles Secret + ein Eingabefeld für den 6-stelligen Code. Erst ein korrekter Code (gegen das Pending-Secret geprüft, ±1 Zeitschritt Toleranz) persistiert das Secret verschlüsselt (saveSecret()) und löscht den Pending-Eintrag aus der Session. Ein falscher Code lässt das Pending-Secret unangetastet — der Nutzer kann denselben QR-Code erneut scannen/eingeben.
  3. Status/Deaktivieren — sobald enrollt, zeigt die Profilseite nur noch Status „aktiv“ + eine Checkbox „Deaktivieren“, die zusammen mit dem regulären Profil-„Speichern“-Button das Secret entfernt (removeSecret()).
Bekanntes DokuWiki-Gotcha, hier gelöst: doku.php ruft session_write_close() vor act_dispatch() auf. Ein naiver $_SESSION[…] = …-Write in einem ACTION_ACT_PREPROCESS-Hook aktualisiert dadurch nur das In-Memory-Array des aktuellen Requests und landet nie auf Platte — der nächste Request (der Bestätigen-Submit) sieht kein Pending-Secret mehr, jeder Code wäre „falsch“. Fix: jeder Session-Write für das Pending-Secret kapselt sich selbst erneut in @session_start(); …; session_write_close(); (dasselbe Idiom, das DokuWiki-Core in auth.phps updateprofile() und common.phps send_redirect() verwendet). Ohne dieses Reopen-Idiom bricht die Bestätigung environment-unabhängig — das Symptom trat identisch auf MicroApache und IIS auf.

Die interne Testseite (do=wkidentity_debug, Link im Profil optional über die Einstellung show_debug_link) ist vom echten Enrollment komplett entkoppelt: Sie berechnet TOTP-Codes zu einem beliebig eingefügten Secret rein im Browser (Web Crypto HMAC-SHA1) und schickt nie etwas an den Server — gedacht, um die Kompatibilität mit Authenticator-Apps zu prüfen, nicht um echte Nutzer-Secrets anzuzeigen.

Workflow (Email-Enrollment)

Zweiter Reiter im selben 2FA-Bereich, nur sichtbar wenn die Einstellung allow_email_method aktiv ist (Default aus, siehe „Konfiguration“). Derselbe Zwei-Schritt-Grundsatz wie bei TOTP: „Per Email einrichten„ schickt einen 6-stelligen Code an die aktuell im Profil hinterlegte Adresse (sendEmailChallenge(), gesalzener Hash — password_hash(), seit der Sicherheits-Härtung, zuvor ungesalzenes SHA-256 — mit 10 Minuten Ablauf server-seitig gespeichert, nie im Klartext); erst ein korrekt eingegebener Code aktiviert die Methode (saveEmailMethod()). Seit der Sicherheits-Härtung kann ein Benutzer beide Methoden gleichzeitig führen — TOTP und Email —, unabhängig voneinander eingerichtet und entfernt (hasFactor()/enrolledFactors()/ removeFactor()); eine einzelne Anmeldung oder ein Step-up akzeptiert dabei, welcher der eingerichteten Faktoren auch immer zum eingegebenen Code passt.

Login-Erzwingung

Umgesetzt über LoginGate.php und den Kern-Hook AUTH_LOGIN_CHECK (inc/auth.php, vor der eigentlichen Session-Finalisierung) — der einzige Zeitpunkt im gesamten Request-Zyklus, an dem sich ein zusätzlicher Faktor vor den fertigen Login schieben lässt, da Cores auth_login() Passwort- Prüfung und Session-Aufbau sonst in einem Rutsch erledigt.

Ablauf: korrektes Passwort + Nutzer enrollt + Einstellung enforce_login aktiv → Login wird angehalten, ein Pending-Zustand (Nutzer, Passwort, eingerichtete Methoden, Ablauf, Fehlversuchszähler) landet für maximal login_attempt_window Sekunden in der Session (Reopen-Idiom, siehe oben), und die Seite do=wkidentity_verify verlangt den passenden Code (bei zwei eingerichteten Methoden wird gegen beide geprüft, siehe „Assurance ist eine Sitzungs-, keine Kontoeigenschaft“ oben). Bei Erfolg wird der echte Core-Login (auth_login()) selbst ausgelöst — keine eigene Session-/Cookie-Logik, volle Wiederverwendung — und zusätzlich die Sitzungs-Absicherung vermerkt (StepUp::recordAssurance()): ein beim Login bewiesener Faktor zählt seither genauso wie ein per Step-up bewiesener, statt eine sofortige, überflüssige erneute Abfrage zu erzwingen. Bei login_max_attempts Fehlversuchen wird der Pending-Zustand verworfen; der Nutzer muss erneut mit Passwort beginnen.

Nicht enrollte Nutzer sind vollständig unberührtgateLogin() greift nur bei bereits enrollten Nutzern ein; wer nie 2FA eingerichtet hat, meldet sich unverändert einstufig an, auch bei aktivem enforce_login.

enforce_login überschreibt Kernverhalten von auth_login() — das riskanteste Stück Code in diesem Plugin. Deshalb Default aus, und als einfacher onoff-Schalter im normalen Konfigurationsmanager erreichbar (nicht nur im eigenen Admin-Panel) — ein Bug hier ließe sich sonst im schlimmsten Fall nicht mehr schnell genug abschalten.

Einmalcode-Abläufe

Drei Vorgänge, ein Mechanismus: Kennwort-Zurücksetzen, Selbstregistrierung und Einladung durch einen Administrator. Alle drei enden damit, dass der Kontoinhaber sein Kennwort selbst eingibt — es wird nie erzeugt, nie versendet und nie angezeigt.

Ein- und ausgeschaltet über otp_flows (Vorgabe an).

Die beiden Kern-Einsichten

Ohne sie liest sich die Einstellungsliste wie fünf beliebige Zeitwerte:

  1. Der Code ist nicht der Vorgang. Ein Code lebt otp_ttl Sekunden (Vorgabe 1800), der Vorgang selbst reset_ttl / register_ttl / invite_ttl (Vorgabe 1 Tag / 2 Tage / 7 Tage). Deshalb kann ein neuer Code angefordert werden, ohne von vorn zu beginnen — die verschickte E-Mail darf verloren gehen, der Vorgang überlebt sie. Ein Code in einem Postfach ist ein Schlüssel in einer Tür, also ist er kurzlebig; der Vorgang ist es nicht.
  2. Was Raten begrenzt, ist das Versuchsbudget, nicht die Codelänge. otp_attempts (Vorgabe 5) zählt je Vorgang, und ein neuer Code füllt es nicht auf. Wer den Nachschub-Knopf drückt, bekommt keinen zweiten Satz Versuche. otp_resend_gap (Vorgabe 60 s) begrenzt zusätzlich, wie oft überhaupt nachgeschickt wird.

Was das Formular über ein Konto verrät

Nichts. requestReset() antwortet gleich, ob es das Konto gibt oder nicht — sonst wäre das Zurücksetz-Formular ein Mitgliedschafts-Prüfer, in den jeder beliebige Namen eintippen kann.

Aus demselben Grund ist die Formulierung auf dem Bildschirm für alle dieselbe: sie leitet sich nur aus wikiweitem Zustand ab, nie aus dem eingegebenen Namen. Eine Wortwahl, die dem einzelnen Konto folgte, verriete, welche Namen einen Authentifikator halten.

Der Weg über einen Administrator

Für den Fall, den kein Formular lösen kann — totp ist gesetzt und das Konto hat keinen Authentifikator, oder die E-Mail kommt nachweislich nicht an:

  • Über die Einladungs-Verwaltung (Technische Referenz) oder die Kommandozeile stellt ein Administrator einen Code aus. Dieser Weg öffnet absichtlich einen Vorgang mit gemailtem Code, unabhängig von recovery_factor — er ist der Ausweg, und ein Ausweg, der derselben Richtlinie unterläge wie der Weg hinein, wäre keiner.
  • Der Code wird dem Administrator genau einmal angezeigt, in der Antwort auf die Anforderung, und nirgends abgelegt, wo er ein zweites Mal erscheinen könnte.
  • Der Mindestabstand für Nachschub entfällt hier: eine Rettung eine Minute warten zu lassen, nachdem der Gerettete sich vertippt hat, ist das Gegenteil ihres Zwecks.
  • Ein Code berechtigt zum Setzen eines Kennworts und offenbart keines. Ein Administrator hält damit keine Zugangsdaten in der Hand.

Sicherheit

  • At-Rest-Verschlüsselung — Secrets liegen nie im Klartext auf Platte (Technische Referenz: Speicherung); Email-Codes liegen nur gesalzen gehasht (password_hash(), seit der Sicherheits-Härtung).
  • TOTP-Wiederverwendung ausgeschlossen (seit der Sicherheits-Härtung) — ein einmal akzeptierter Zeitschritt wird für dasselbe Login nicht ein zweites Mal angenommen, über jeden Aufrufer von verifyCode() hinweg (Login, Step-up, Enrollment-Bestätigung, Kennwort-Wiederherstellung).
  • CSRF — jeder Submit auf do=profile, im Admin-Panel und auf do=wkidentity_verify prüft checkSecurityToken() (CWE-352).
  • XSS — alle ausgegebenen Werte (Secret, URI, Login) via hsc() (CWE-79).
  • Zwei-Schritt-Enrollment — verhindert „Aktivieren“ ohne nachgewiesenen Scan/Empfang (siehe Workflow), für beide Methoden.
  • Login-Erzwingung als Kill-Switchenforce_login ist ein einfacher, vom eigenen Admin-Panel unabhängiger onoff-Schalter, sofort abschaltbar.
  • Rate-Limiting — sessiongebundener Fehlversuchszähler auf do=wkidentity_verify (kein IP-Tracking, bewusst einfach gehalten).
  • Debug-Tool — nur für angemeldete Nutzer erreichbar; Secret-Berechnung rein clientseitig, nichts wird je an den Server gesendet.
  • Erstes AUTH_ACL_CHECK-Plugin in diesem Repository — verschärft nur zusätzlich, mutiert niemals conf/acl.auth.php.
  • Kein Kennwort verlässt je den Server — die Einmalcode-Abläufe erzeugen und versenden keines; der Kontoinhaber tippt sein Kennwort selbst, und die Mindestlänge wird serverseitig geprüft.
  • Codes nur gehasht — auch in den Vorgangsdateien; ein Administrator kann einen Code ausstellen, aber keinen nachschlagen.
  • Keine Kontoauskunft — das Zurücksetz-Formular antwortet gleich, ob es das Konto gibt oder nicht, und seine Wortwahl leitet sich nie aus dem eingegebenen Namen ab.
  • Kein Schreibvorgang auf GET — jeder zustandsändernde Zweig der Abläufe verlangt POST und Sicherheitstoken; ein GET zeichnet nur ein Formular. Ein Zurücksetzen, das ein Crawler durch Folgen eines Links auslösen könnte, wäre eines, das jeder für jeden auslösen kann.

Verwandte Themen

de/wiki/dwe/wkidentity/concepts.txt · Zuletzt geändert: von 0.0.0.0