Zum Inhalt springen

8. Authentifizierung und Rechte

Zu Zen-Modus wechseln

Die Anmeldung ist die Stelle, an der selbstgebaute Anwendungen am zuverlässigsten brechen. Nicht, weil sie schwer zu programmieren wäre, sondern weil sie an zwanzig kleinen Stellen richtig sein muss und an einer einzigen falschen Stelle wertlos wird. Ein perfekt gehashtes Passwort nützt nichts, wenn die Sitzung nie abläuft. Eine saubere Rollenprüfung nützt nichts, wenn sie nur im Frontend stattfindet.

Dieses Kapitel baut die Anmeldung einmal vollständig, im getrennten Aufbau aus Kapitel 5 und mit der Datenbank aus Kapitel 6.

AuthentifizierungAutorisierung
FrageWer sind Sie?Was dürfen Sie?
Antwort bei Misserfolg401403
Wie ofteinmal pro Sitzungbei jeder Anfrage
Ergebniseine Identitäteine Entscheidung über eine konkrete Handlung

Die dritte Zeile ist die, die im Unterricht am häufigsten untergeht. Die Anmeldung passiert einmal, die Berechtigungsprüfung bei jedem einzelnen Aufruf, für jedes einzelne Objekt. Wer das umdreht und einmalig prüft, ob jemand „Verwalter ist”, und danach alles erlaubt, hat eine Anwendung mit einem einzigen Schloss an der Eingangstür und offenen Türen dahinter.

Passwörter werden nicht im Klartext gespeichert. Sie werden auch nicht „verschlüsselt”, und dieser Fehler ist verbreiteter als der erste: Verschlüsselung ist umkehrbar, das ist ihr Zweck. Wer den Schlüssel hat, hat alle Passwörter, und der Schlüssel liegt auf demselben Server.

Und sie werden nicht mit MD5 oder SHA-1 gehasht, auch nicht mit SHA-256. Diese Verfahren sind schnell, und Schnelligkeit ist bei Passwörtern der Fehler: Eine Grafikkarte prüft Milliarden Kandidaten pro Sekunde. Ein Passwort-Hashverfahren ist absichtlich langsam und absichtlich speicherhungrig.

Argon2id ist die aktuelle Empfehlung, bcrypt die etablierte Alternative. Beide erzeugen den Salt selbst, hängen ihn an den Hash und speichern die Parameter mit, sodass sich später alles aus der gespeicherten Zeichenkette rekonstruieren lässt.

import argon2 from "argon2";
// during registration
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// during sign-in
const matches = await argon2.verify(hash, enteredPassword);

Die Parameter stammen aus den OWASP-Empfehlungen und sind ein Kompromiss zwischen Sicherheit und Serverlast. Wählen Sie sie so groß, dass eine Prüfung auf Ihrem Server ungefähr eine Zehntelsekunde dauert.

Die Passwortregeln, die viele Anwendungen erzwingen, stammen aus den Neunzigerjahren und richten mehr Schaden als Nutzen an. Was tatsächlich hilft:

  • Länge statt Zeichenklassen. Mindestens zwölf Zeichen, und alles erlauben, was jemand tippen kann. Ein erzwungenes Sonderzeichen führt zu Password1!.
  • Abgleich mit bekannten Leaks. Dienste wie Have I Been Pwned lassen sich datensparsam abfragen, ohne das Passwort zu übertragen. Ein Passwort, das in einem Leak steht, ist unabhängig von seiner Komplexität verbrannt.
  • Keine erzwungene Änderung alle 90 Tage. Das erzeugt Sommer2026!, gefolgt von Herbst2026!.
  • Obergrenze nicht zu niedrig. Passphrasen sind gut, und 64 Zeichen sollten durchgehen.

Der Ablauf zum Zurücksetzen ist eine eigene Angriffsfläche und braucht vier Eigenschaften: Der Link enthält ein zufälliges Token mit genügend Entropie, dieses Token wird in der Datenbank gehasht abgelegt (es ist ein Passwort auf Zeit), es gilt kurz, üblicherweise 15 bis 60 Minuten, und es lässt sich genau einmal verwenden. Nach dem Zurücksetzen werden alle bestehenden Sitzungen beendet.

Bei einer serverseitigen Sitzung bekommt der Browser nur eine zufällige Sitzungs-ID in einem Cookie. Alles Weitere liegt auf dem Server, in der Datenbank oder in einem schnellen Speicher.

const cookieStore = await cookies();
cookieStore.set("session", sessionId, {
httpOnly: true, // no access from JavaScript
secure: true, // only over HTTPS
sameSite: "lax", // not sent along on cross-site requests
path: "/",
maxAge: 60 * 60 * 8,
});

Drei Punkte gehören zum Betrieb einer Sitzung dazu.

Zwei Ablaufzeiten. Eine absolute Lebensdauer, nach der neu angemeldet werden muss, und ein Inaktivitätsablauf, der greift, wenn jemand die Anwendung offen liegen lässt. Beide gehören konfiguriert; eine Sitzung ohne Ablauf ist eine dauerhafte Vollmacht.

Neue Sitzungs-ID nach der Anmeldung. Wer sich anmeldet, bekommt eine frische ID. Sonst kann ein Angreifer dem Opfer vorher eine bekannte ID unterschieben und ist nach dessen Anmeldung mit angemeldet. Dieser Angriff heißt Session Fixation und ist mit einer Zeile Code erledigt.

Abmelden, das wirkt. Die Sitzung wird serverseitig gelöscht, und zwar auf Wunsch auch auf allen Geräten. Das ist der größte praktische Vorteil dieses Verfahrens gegenüber Tokens.

Ein JWT besteht aus drei mit Punkten getrennten Teilen: Kopf, Nutzdaten und Signatur, jeweils Base64url-kodiert.

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsInJvbGxlIjoiYWRtaW4ifQ.dBjftJ...
Kopf Nutzdaten Signatur

Der übliche Aufbau kombiniert ein kurzlebiges Zugriffs-Token (5 bis 15 Minuten) mit einem längerlebigen Refresh-Token, mit dem sich ein neues Zugriffs-Token holen lässt. Das Refresh-Token wird bei jeder Verwendung ausgetauscht (Rotation), und wenn ein bereits benutztes Refresh-Token noch einmal auftaucht, ist das ein Hinweis auf Diebstahl; dann wird die ganze Kette ungültig gemacht.

Das eigentliche Problem bei Tokens ist der Rückruf. Ein signiertes Token ist gültig, bis es abläuft, auch wenn Sie den Benutzer inzwischen gesperrt haben, denn zur Prüfung wird nichts nachgeschlagen. Es gibt drei Antworten darauf: sehr kurze Laufzeiten, eine Sperrliste, oder eine Sitzungs-ID im Token, die serverseitig geprüft wird. Die dritte Variante funktioniert gut und führt gedanklich zurück zu den Sitzungen.

Serverseitige SitzungJWT
PrüfungNachschlagen im SpeicherSignatur prüfen, ohne Nachschlagen
Abmeldensofort wirksamerst mit Ablauf oder über Zusatzaufwand
Skalierunggemeinsamer Sitzungsspeicher nötigzustandslos, gut verteilbar
Fremde Clientsumständlichnatürlich
Typisches RisikoSitzungsdiebstahl, CSRFDiebstahl aus dem Speicher, kein Rückruf
Aufwandgeringmittel bis hoch, wenn man es richtig macht

Für ein Jahresprojekt mit einem Browser-Frontend ist die serverseitige Sitzung mit HttpOnly-Cookie die vernünftigere Wahl: weniger Code, sofortiges Abmelden, kein Token in JavaScript. Tokens werden interessant, sobald eine Mobil-App oder ein fremdes System dazukommt.

RBAC ordnet Benutzern Rollen zu und Rollen Rechte. Das reicht für die meisten Projekte. ABAC entscheidet anhand von Eigenschaften, etwa „darf bearbeiten, solange die Buchung in der Zukunft liegt und ihm selbst gehört”. In der Praxis mischt man beides.

Der erste Schritt ist eine Matrix, und die gehört ins Repository:

AktionGastMitgliedWerkstattleitung
Maschinenliste sehenjajaja
Belegung sehenneinjaja
Buchung anlegenneinja, mit Einschulungja
Fremde Buchung ändernneinneinja
Eigene Buchung stornierenneinja, bis 24 h vorherja
Einschulung eintragenneinneinja

Diese Tabelle ist gleichzeitig die Testliste. Für jede Zeile mit „nein” gibt es einen Test, der nachweist, dass es tatsächlich nicht geht.

Die Prüfung gehört in die Dienstschicht aus Kapitel 6, nicht in die Route Handler und erst recht nicht in die Komponenten. Der Grund ist einfach: In den Handler schreibt man sie beim vierten Endpunkt nicht mehr hinein, und in der Komponente ist sie kein Schutz.

export async function updateBooking(id: number, data: BookingUpdate, user: User) {
const booking = await repo.findBooking(id);
if (!booking) throw new NotFound();
if (booking.memberId !== user.id && !user.roles.includes("manager")) {
throw new NotFound(); // deliberately not Forbidden
}
// ...
}

Eine Rollenprüfung allein genügt nicht. Die verbreitetste Schwachstelle in Webanwendungen ist, dass ein angemeldeter Benutzer mit der richtigen Rolle einfach eine fremde ID in die URL schreibt und fremde Daten bekommt. Der Name dafür ist IDOR, Insecure Direct Object Reference, und die Prüfung dagegen lautet nicht „darf dieser Benutzer Buchungen ändern”, sondern „darf dieser Benutzer diese Buchung ändern”.

Im Code oben steht deshalb NotFound statt NichtErlaubt. Ein 403 auf eine fremde ID bestätigt, dass es diese Buchung gibt; ein 404 verrät nichts. Diese Entscheidung ist eine Abwägung, weil 404 die Fehlersuche erschwert, und sie gehört ins Entscheidungsprotokoll.

Probieren Sie das an Ihrem eigenen Projekt aus, bevor Sie es absichern: Legen Sie zwei Konten an, buchen Sie mit dem einen, und rufen Sie mit dem anderen dieselbe ID auf.

Eine ausgeblendete Schaltfläche, eine Weiterleitung auf die Anmeldeseite, eine Route mit Rollenprüfung im Frontend: All das ist Bedienkomfort. Der Beweis dauert eine Zeile:

Terminal-Fenster
curl -i https://api.projekt.example.at/bookings/812 \
-X PATCH -H "Content-Type: application/json" -d '{"end":"2026-03-12T18:00:00Z"}'

Wenn hier etwas anderes als 401 oder 403 kommt, ist der Endpunkt offen, unabhängig davon, wie gut die Oberfläche aussieht. Genau dieser Aufruf gehört als Nachweis in die Abgabe.

Wo die Anmeldung lebt, hängt an der Entscheidung aus Kapitel 5.

Im BFF-Modell hält der Frontend-Server die Sitzung, der Browser hat ein HttpOnly-Cookie, und das Token für die API bleibt serverseitig. Damit ist das Diebstahlrisiko aus JavaScript ausgeschlossen, und es bleibt die CSRF-Frage, die in Kapitel 9 behandelt wird.

Im Bearer-Modell hält der Client das Token und schickt es im Authorization-Header. CSRF entfällt weitgehend, weil nichts automatisch mitgeschickt wird, dafür ist jede XSS-Lücke ein Sitzungsverlust.

Für nachgelagerte Dienste gilt eine eigene Regel: Wenn Ihre Anwendung mit dem CMS oder einem fremden Dienst spricht, bekommt sie dafür eine eigene Identität, also ein Dienstkonto mit eigenen Rechten. Das Benutzertoken einfach weiterzureichen gibt dem nachgelagerten Dienst genau die Rechte des Benutzers, und meist auch die, die er nicht braucht.

Statt selbst Passwörter zu verwalten, kann man die Anmeldung an einen Anbieter delegieren. OAuth 2.0 regelt dabei die Erlaubnisvergabe, OpenID Connect setzt die Identität obendrauf.

Vier Beteiligte sind im Spiel: der Benutzer, Ihre Anwendung als Client, der Autorisierungsserver beim Anbieter und die geschützte Ressource. Der heute empfohlene Ablauf ist der Authorization Code Flow mit PKCE:

sequenceDiagram
  participant B as Browser
  participant A as Ihre Anwendung
  participant I as Identitätsanbieter
  B->>A: Anmelden mit fremdem Konto
  A->>B: Weiterleitung zum Anbieter, mit code_challenge
  B->>I: Anmeldung und Zustimmung
  I->>B: Weiterleitung zurück, mit Autorisierungscode
  B->>A: Autorisierungscode
  A->>I: Code plus code_verifier
  I->>A: ID-Token und Zugriffstoken
  A->>B: eigene Sitzung setzen

Der Punkt an diesem Ablauf: Das eigentliche Token geht nie durch den Browser, sondern wird von Server zu Server geholt. Der code_verifier stellt sicher, dass ein abgefangener Autorisierungscode allein nichts nützt.

Für Ihr Projekt ist das eine optionale Erweiterung („Anmelden mit GitHub”), keine Pflicht. Die Vertiefung folgt in der 5. Klasse, dort dann mit Datenschutzfragen und der Abwägung, ob man einem fremden Anbieter die Anmeldung überlassen will. Ebenfalls als Ausblick: die Zwei-Faktor-Anmeldung über TOTP, bei der zusätzlich ein zeitbasierter Code aus einer Authenticator-App verlangt wird.

  1. Krypto oder Sitzungsverwaltung selbst bauen, weil es nur ein Schulprojekt ist. Der Fehler fällt nicht auf und macht alles wertlos.
  2. Passwörter mit SHA-256 hashen. Schnell ist hier die falsche Eigenschaft.
  3. JWT im localStorage plus eine XSS-Lücke. Die Sitzung ist damit kopierbar und nicht rückrufbar.
  4. Unterschiedliche Meldungen oder Antwortzeiten für unbekannte Kennung und falsches Passwort.
  5. Rollenprüfung nur im Frontend. Die API bleibt offen, und curl weiß das.
  6. Keine objektbezogene Prüfung. Eine fremde ID in der URL liefert fremde Daten, und niemand merkt es.
  7. Sitzungs-ID nach der Anmeldung beibehalten. Session Fixation, mit einer Zeile vermeidbar.
  8. Kein Ablauf, kein Abmelden. Eine Sitzung ohne Ende ist eine Vollmacht ohne Ende.
  9. Benutzertoken an nachgelagerte Dienste weiterreichen, statt dem Dienst eine eigene Identität zu geben.
  • Erklären: Authentifizierung und Autorisierung unterscheiden und begründen, warum die zweite bei jeder Anfrage stattfindet.
  • Erklären: begründen, warum Passwörter gehasht und nicht verschlüsselt werden und warum ein schnelles Hashverfahren ungeeignet ist.
  • Anwenden: Passwörter mit Argon2id oder bcrypt korrekt speichern und prüfen.
  • Anwenden: Registrierung, Anmeldung, Abmeldung und Zurücksetzen mit Einmal-Token umsetzen.
  • Erklären: erklären, warum ein JWT lesbar ist, und daraus ableiten, was nicht hineingehört.
  • Analysieren: Sitzungs- und Token-Verfahren hinsichtlich Abmeldung, Skalierung, XSS und CSRF vergleichen und eine begründete Wahl treffen.
  • Erschaffen: eine Rollenmatrix für das eigene Projekt aufstellen und in Tests überführen.
  • Anwenden: Rollen- und Objektrechte zentral in der Dienstschicht durchsetzen.
  • Analysieren: eine IDOR-Lücke im eigenen Projekt nachweisen und schließen.
  • Beurteilen: nachweisen, dass ein geschützter Endpunkt auch ohne Frontend nicht erreichbar ist.
  • Erklären: den Authorization Code Flow mit PKCE in Grundzügen erklären.
  • Registrierung, Anmeldung, Abmeldung und Passwort-Zurücksetzen funktionieren, Passwörter sind mit Argon2id oder bcrypt gespeichert.
  • Sitzungen haben Ablauf und lassen sich beenden; nach der Anmeldung wird die Sitzungs-ID gewechselt.
  • Die Rollenmatrix liegt in docs/architektur.md, mindestens zwei Rollen sind umgesetzt.
  • Objektbezogene Rechte sind implementiert, ein IDOR-Versuch ist als Protokoll dokumentiert (vorher erfolgreich, nachher nicht).
  • Für jede „nein”-Zelle der Matrix existiert ein Test.
  • Ein curl-Aufruf gegen einen geschützten Endpunkt ohne Anmeldung liegt als Nachweis bei.
  • Aufgabe - Anmeldung von Grund auf (Hashing, Sitzung, Abmeldung)
  • Aufgabe - Sitzung gegen JWT: dieselbe Anwendung in zwei Varianten
  • Aufgabe - IDOR: fremde Daten über die ID abrufen und die Lücke schließen
  • Aufgabe - Rollenmatrix für das eigene Projekt umsetzen und testen