Journal

EAuth zieht auf eauth.me um, und jede Tür wird gezählt

EAuth hat eine eigene Domain, eauth.me; auth.elchi.dev leitet dorthin weiter. Dazu kamen Limits bei jedem Login, geprüfte Formulare und bestätigte Adressen.

EAuth hat jetzt eine eigene Domain: eauth.me. Der Issuer ist https://eauth.me, Login und Kontoseite liegen auf der Apex-Domain. Seit dem 3. Oktober 2026 beantwortet auth.elchi.dev jede Anfrage mit einem permanenten 308-Redirect auf denselben Pfad auf eauth.me. Der Umzug war der Anlass, alles durchzugehen, was eine Person auf dem Weg hinein berührt: Jede Tür wird jetzt gezählt, Formulare belegen ihre Herkunft, Abmelden braucht einen Nachweis, und keine Anwendung bekommt eine Adresse, die niemand bestätigt hat.

Warum EAuth umgezogen ist

EAuth begann als Login für unsere eigene Konsole und unser Dashboard, also lag elchi.dev als Zuhause nahe. Eine Adresse unter der Website einer Agentur sagt aber: „ein Feature dieser Website“. EAuth ist ein Produkt, auf dem andere Entwickler aufbauen können, und bekommt eine Adresse, die das sagt.

Es gibt auch einen Sicherheitsgrund. Browser schicken Cookies nach Site, und eine Site ist die registrierbare Domain, nicht der Hostname. Auf auth.elchi.dev war jede Seite auf jeder Subdomain von elchi.dev dieselbe Site wie die Seite, die die Login-Session aller Leute hält. Keine davon hat sich falsch verhalten, aber das ist ein grösserer Vertrauenskreis, als ein Login-Dienst haben sollte.

Und es musste vor Passkeys und Single Sign-on passieren. Ein Passkey gehört zu einer Domain, und der Browser bietet ihn nirgends sonst an; der Identity Provider einer Organisation wird einmal mit der Entity-ID und der Antwortadresse von EAuth eingerichtet. Beides lebt von Anfang an auf eauth.me, also muss keines davon wegen einer Adresse noch einmal gemacht werden.

Was es kostet: ein Login mehr für alle, denn die Sessions gehörten zum alten Host. Und jede Integration muss den neuen Issuer nennen. Ein Client vergleicht den Issuer als String, Tokens von eauth.me sagen https://eauth.me, und eine Library, die das prüft, wie sie soll, weist sie ab, bis ihre Einstellung passt.

Keine Übergangszeit, und warum es keine brauchte

Ein geänderter Issuer bricht Integrationen, und unsere AGB versprechen vor einer solchen Änderung eine Ankündigung. Der Plan war, auth.elchi.dev neunzig Tage lang als vollständigen Issuer weiterzuführen. Wir brauchten das nicht: Am Tag des Umzugs gehörte jede bei EAuth registrierte Anwendung Elchi Studios, und niemand ausserhalb der Firma hatte ein Konto. Deshalb leitet die alte Adresse ab dem Tag des Umzugs weiter, und die AGB sagen das in Version 1.7, Ziffer 2.4.

Der Redirect ist ein 308 und kein 301, damit eine Token-Anfrage an die alte Adresse ein POST bleibt und unversehrt ankommt. Die Tokens, die sie bekommt, nennen https://eauth.me. Für unsere eigene Konsole und unser Dashboard war das eine einzige Einstellung, denn jede Oberfläche liest den Issuer aus demselben Wert.

Ich habe es jetzt gemacht, weil es jetzt billig war. Mit ein paar hundert externen Integrationen wäre es ein Migrationsprojekt mit Kündigungsfrist.

Jede Tür wird gezählt

Bis zu diesem Release hatte die API hinter elchi.dev Rate Limits, die Seiten, auf denen Leute ein Passwort eintippen, aber nicht. Die Rechnung, die es dringend machte: Ein zweiter Faktor ist ein sechsstelliger Code, und zu jedem Zeitpunkt werden drei Codes akzeptiert, weil Uhren abweichen. Mit dem Passwort hat ein Skript fünf Minuten für den Code und ohne Limit Tausende Versuche. Das ist kein zweiter Faktor, sondern eine Verzögerung.

Jetzt wird jede Stelle, die ein Credential annimmt, doppelt gezählt: nach der Adresse, von der eine Anfrage kommt, und nach dem Konto, das sie nennt.

Tür Limit
Login-Versuche von einer Adresse, jede Methode 30 pro Minute
Falsche Passwörter, ein Konto von einer Adresse 5 in 15 Minuten
Falsche Passwörter, ein Konto von überall 20 in 15 Minuten
Fehlgeschlagene Logins von einer Adresse 50 pro Stunde
Falsche Codes für den zweiten Faktor, ein Konto 5 in 15 Minuten, 20 pro Tag
Neue Konten von einer Adresse 10 pro Stunde
/authorize, pro Adresse 60 pro Minute
/token, pro Anwendung standardmässig 120 pro Minute, in der Konsole bis 600

Bei zwanzig falschen Codes pro Tag braucht jemand, der das Passwort hat und sonst nichts, im Schnitt etwa 45 Jahre, um den Code zu erraten.

Vier Details zählen mehr als die Zahlen.

Jeder Versuch wird gezählt, bevor er geprüft wird. Die naheliegende Sperre prüft die Sperre, dann das Passwort, und zählt einen Fehlschlag danach. Anfragen, die im selben Moment kommen, passieren alle den ersten Schritt, also bekam ein Skript, das hundert Versuche auf einmal abfeuerte, hundert Versuche. Ein Review der ersten Version fand genau das. Jetzt nimmt jeder Versuch in einem atomaren Schritt seinen Platz in der Zählung ein, bevor irgendetwas geprüft wird, und bekommt ihn nur zurück, wenn er richtig war.

Die Limits schlagen geschlossen fehl. Antwortet der Speicher, der sie zählt, nicht, verweigert das Login und sagt das, statt alles durchzulassen. Die einzige Ausnahme ist ein lockeres Limit von 600 Anfragen pro Minute und Adresse für den Rest des Hosts, darunter Discovery und Keys: Dort gibt es nichts zu erraten, also soll ein Ausfall des Zählers das nicht lahmlegen.

Eine Sperre lässt sich nicht gegen den Besitzer einsetzen. Sperrt man ein Konto nach ein paar falschen Passwörtern, kann jeder dessen Besitzer aussperren: fünf falsche Versuche jede Viertelstunde, für immer. Ein Browser, der ein Login in ein Konto abgeschlossen hat, trägt deshalb 90 Tage lang ein Cookie und behält ein kleines eigenes Kontingent, während das Konto für alle anderen gesperrt ist. Wer rät, kann das Konto für Fremde verlangsamen, nicht für seinen Besitzer.

IPv6 wird nach seinem /64 gezählt, denn eine Verbindung kann darin jede beliebige Absenderadresse wählen.

Der Token-Endpoint zählt die Anfragen einer Anwendung erst, nachdem sich der Client authentifiziert hat, sonst könnte jeder das Kontingent eines anderen aufbrauchen. Ein Browser oder eine Mobile-App hat kein Secret, deshalb wird das Kontingent dort pro Anwendung und Adresse gezählt: Ein Fremder, der es aufbraucht, braucht nur sein eigenes auf.

Formulare belegen ihre Herkunft, das Abmelden auch

Der Zustimmungsdialog und die Kontoseite verliessen sich auf die SameSite-Regeln des Browsers, einen Standard mit einer Geschichte von Ausnahmen, damit andere Sites nicht an sie posten konnten. Jedes Formular dort trägt jetzt ein an die Session gebundenes Token und muss von eauth.me selbst abgeschickt werden.

Abmelden geschah bisher bei jedem GET /logout, also konnte jede Seite jeden mit einem Link abmelden: der erste Schritt, um jemanden dazu zu bringen, sich auf einer Seite Ihrer Wahl neu anzumelden. Das Abmelden folgt jetzt OpenID Connect RP-Initiated Logout und steht in der Discovery als end_session_endpoint. Die Session endet, wenn die Anwendung ein id_token_hint schickt, das die angemeldete Person nennt und während dieser Session ausgestellt wurde. Ohne das fragt EAuth zuerst.

Keine Anwendung bekommt eine unbestätigte Adresse

email_verified war in jedem ID-Token, das EAuth vor diesem Release ausgestellt hat, false. Das stimmte, denn nichts hatte je eine Adresse geprüft, und es war nutzlos. Die Registrierung schickt jetzt einen Link, der einmal funktioniert, 24 Stunden lang. Wer ihn öffnet, bestätigt die Adresse.

Der strengere Teil ist neu seit dem 3. Oktober: Keine Anwendung bekommt ein Konto, dessen Adresse nicht bestätigt ist. Wer sich registriert, wird auf dem Weg zurück zur Anwendung von einer Seite angehalten, die einen neuen Link schickt, und der Link führt dorthin zurück, wo das Login stehen blieb. Mit prompt=none bekommt die Anwendung stattdessen interaction_required. In einem Token aus einem Login ist email_verified also immer true. Für unsere eigene Konsole gilt dieselbe Regel.

Eine unbestätigte Adresse ist ein String, den jeder eintippen könnte, und wer damit angemeldet ist, bekäme als Fremder Einladungen, die für dieses Postfach gedacht sind. Ein Zurücksetzen des Passworts, Single Sign-on über den Identity Provider einer Organisation oder ein als bestätigt markierter Import zählen ebenfalls; jedes andere importierte Konto wird beim ersten Login gefragt. Der Preis ist ein Schritt mehr für jemanden, der eine Anwendung zum ersten Mal ausprobiert. Ich finde, das lohnt sich.

Derselbe Mechanismus bringt das Zurücksetzen des Passworts, wofür man uns bisher schreiben musste. Die Anfrage sagt dasselbe, ob ein Konto existiert oder nicht, und ein Link funktioniert einmal, eine Stunde lang, und nur der neuste. Wer ihn öffnet, sieht ein Formular und verbraucht nichts, denn auch Mailscanner öffnen Links. Das Setzen des neuen Passworts meldet jedes Gerät und jede Anwendung ab.

Die Links bestehen aus 256 zufälligen Bits und werden nur als Digest gespeichert, wie jedes andere Credential hier. Mit einer Kopie der Datenbank lässt sich nichts bestätigen und nichts zurücksetzen.

Was noch nicht fertig ist

Passkeys funktionieren neben Passwörtern, und eine Organisation kann ihre Mitglieder über ihren eigenen Identity Provider mit SAML anmelden lassen. Was noch fehlt: Die Mails, die EAuth verschickt, gibt es nur auf Englisch. Ein unabhängiges Sicherheitsaudit gab es nicht; das oben erwähnte Review war unser eigenes. Die gemessene Verfügbarkeit kommt auf status.elchi.dev, sobald ein ganzer Monat gemessen ist, nicht vorher. Das Login mit der Schweizer E-ID ist in Vorbereitung, aber noch nichts, was eine Anwendung nutzen kann.

Wer auf EAuth aufbaut, gibt eine Sache von Hand an: den Issuer, https://eauth.me. Alles andere kommt aus der Discovery.

Quellen

  1. RFC 9700: Best Current Practice for OAuth 2.0 Security, IETF, gelesen am
  2. OpenID Connect RP-Initiated Logout 1.0, OpenID Foundation, gelesen am
  3. OpenID Connect Core 1.0, Standard Claims, OpenID Foundation, gelesen am
  4. RFC 6238: TOTP, Time-Based One-Time Password Algorithm, IETF, gelesen am
  5. RFC 9110: HTTP Semantics, 308 Permanent Redirect, IETF, gelesen am
  6. Web Authentication: An API for accessing Public Key Credentials, Level 3, W3C, gelesen am
  7. Authentication Cheat Sheet, OWASP, gelesen am
  8. Site (glossary), MDN, gelesen am

Geschrieben von Samuel Krauss, Founder. Abgelegt unter eauth, security.

Übersetzt aus dem Englischen. Zum englischen Original

Alle Beiträge Dieser Beitrag als Markdown Atom-Feed