sequenceDiagram
participant U as Nutzer im Browser
participant A as Client App
participant S as Authorization Server
A->>A: erzeugt code_verifier (geheim) und code_challenge (Hash davon)
A->>S: Weiterleitung mit code_challenge
U->>S: Login und Zustimmung
S->>A: Weiterleitung mit authorization code (kurzlebig, einmalig)
A->>S: Token-Anfrage mit code UND code_verifier
S->>S: hasht den verifier und vergleicht mit der gespeicherten challenge
S->>A: Access Token und Refresh Token
Identität und Kryptografie-Grundlagen: AuthN vs. AuthZ, Sessions, Tokens, JWT, OAuth2 mit PKCE, Passwort-Hashing
Track Konzepte · Security · ca. 70 Min.
Worum es geht
Fast jede Anwendung, die du als Freelancer baust, hat einen Login. Fast jeder Sicherheitsvorfall in solchen Anwendungen hat mit Identität zu tun: ein gefälschtes Token wird akzeptiert, ein abgefangener Code wird eingelöst, eine Passwort-Datenbank wird geleakt und die Passwörter sind in Minuten geknackt. Die gute Nachricht: Die Bausteine sind klein, und du kannst sie in Python nachbauen, um sie zu verstehen. (Im Projekt baust du sie nicht selbst, dazu mehr in der Falle.)
Am Ende der Lektion kannst du Authentifizierung (authentication, AuthN) und Autorisierung (authorization, AuthZ) trennen, Sessions und Tokens gegeneinander abwägen, ein JWT (JSON Web Token) mit HS256 selbst prüfen und seine Fallstricke benennen, den Authorization Code Flow mit PKCE (Proof Key for Code Exchange) durchrechnen, ein Passwort mit Salt und Iterationen speichern und Refresh-Token-Rotation mit Reuse Detection (Wiederverwendungs-Erkennung) bauen. Der Prüfstein der Lektion: Warum Authorization Code mit PKCE statt Implicit Flow?
Plane ehrlich 70 Minuten ein: etwa 30 Minuten Lesen, 40 Minuten Übungen (fünf Stück). Eine gute Pause ist nach Übung 2.
Alles läuft im Browser mit hmac, hashlib, base64, json und secrets. Es gibt keine echten Server. Kleine Hinweise vorab:
- Die Beispiele sind Lernmodelle. Produktionscode nimmt geprüfte Bibliotheken.
- Das Passwort-Hashing hier nutzt nur 1000 Iterationen, damit es im Browser schnell bleibt. Das ist ein Demo-Wert, kein Empfehlungswert.
Von JS/TS her gedacht
| Konzept | JS/TS | Python (hier) |
|---|---|---|
| Signatur (HMAC) | crypto.createHmac("sha256", key) |
hmac.new(key, daten, hashlib.sha256) |
| Konstantzeit-Vergleich | crypto.timingSafeEqual(a, b) |
hmac.compare_digest(a, b) |
| base64url | Buffer.from(x).toString("base64url") |
base64.urlsafe_b64encode(x) plus = abschneiden |
| Sichere Zufallszahl | crypto.randomBytes(32) |
secrets.token_bytes(32), als URL-taugliche Zeichenkette secrets.token_urlsafe(32) (ergibt 43 Zeichen) |
| JWT in der Praxis | Bibliothek jsonwebtoken, jose |
Bibliothek PyJWT, authlib |
| Passwort-Hashing in der Praxis | bcrypt, argon2 (npm) |
argon2-cffi, bcrypt |
Der Beweis, dass das Konzept sprachunabhängig ist: Dasselbe JWT, in Node gebaut.
const crypto = require("crypto");
const b64url = (b) => Buffer.from(b).toString("base64url");
const kopf = b64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
const inhalt = b64url(JSON.stringify({ sub: "anna", rolle: "leser", exp: 1700000600 }));
const sig = crypto.createHmac("sha256", "geheimnis-nur-auf-dem-server").update(`${kopf}.${inhalt}`).digest();
console.log(`${kopf}.${inhalt}.${b64url(sig)}`);Ausgabe:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbm5hIiwicm9sbGUiOiJsZXNlciIsImV4cCI6MTcwMDAwMDYwMH0.f8Qs6lTw1sSvXjEqeStLqKCnUsvPmz5V7VzHUgqmU78
Gleich siehst du, dass Python exakt dieselbe Zeichenkette erzeugt.
Konzept
Schritt 1: Wer bist du, und was darfst du?
Zwei Fragen, die oft vermischt werden:
| Authentifizierung (AuthN) | Autorisierung (AuthZ) | |
|---|---|---|
| Frage | Wer bist du? | Was darfst du hier tun? |
| Beweis | Passwort, Passkey, Einmalcode, Token | Rolle, Rechte, Besitz der Ressource |
| Wann | Beim Login, dann über Session oder Token | Bei jedem Request an eine Ressource |
| HTTP bei Fehler | 401 (nicht angemeldet oder Beweis ungültig) | 403 (angemeldet, aber nicht erlaubt) |
Beispiel: Anna ist angemeldet (AuthN erfolgreich). Sie ruft GET /rechnungen/4711 auf. Das Backend muss prüfen, ob Rechnung 4711 Anna gehört (AuthZ). Sonst kann jeder angemeldete Nutzer fremde Rechnungen lesen, nur indem er die Nummer ändert. Diese Lücke heißt Broken Access Control und gehört zu den Schwerpunkten der OWASP Top 10 (Quelle: Grundlagen, Schicht 8, Angriffe; Platz in der aktuellen Liste: bitte prüfen).
Daraus folgt eine Regel, die auch den Prüfstein der Schicht beantwortet: Eingabevalidierung im Frontend reicht nicht, weil der Angreifer das Frontend nicht benutzen muss. Er schickt Requests direkt mit curl. Autorisierung gehört darum ins Backend, in jeden Request, nie nur in versteckte Buttons.
Schritt 2: Sessions oder Tokens
Nach dem Login muss sich der Server merken, wer du bist. Zwei Wege:
- Session (serverseitiger Zustand): Der Server legt einen Eintrag an (
session_id -> user) und schickt nur die zufällige Session-ID als Cookie. Bei jedem Request schlägt er die ID nach. - Token (clientseitiger Zustand): Der Server schickt ein selbstbeschreibendes Token (z. B. ein JWT) mit der Identität darin, signiert. Bei jedem Request prüft er nur die Signatur, ohne Nachschlagen.
| Session | Token (JWT) | |
|---|---|---|
| Wo steht der Zustand? | Im Server (Store, z. B. Datenbank oder Redis) | Im Token beim Client |
| Sofort sperrbar (Logout, Mitarbeiter entlassen)? | Ja, Eintrag löschen | Nein, nur bis zum Ablauf (exp), außer du baust wieder einen Store dazu |
| Skalierung auf viele Server | Gemeinsamer Store nötig | Jeder Server prüft allein mit Secret oder Public Key |
| Drittparteien (andere Services, Mobile Apps) | Umständlich | Passt gut |
| Größe pro Request | Klein (eine ID) | Größer (Daten plus Signatur) |
Beides ist legitim. Für ein klassisches Web-Backend mit Browser ist die Session mit HttpOnly-, Secure- und SameSite-Cookie oft die einfachere und sicherere Wahl. Tokens lohnen sich, wenn mehrere Services oder Clients ohne gemeinsamen Store prüfen sollen. Das ist ein klassischer Trade-off (Übung 5).
Schritt 3: Aufbau eines JWT, durchgerechnet
Ein JWT besteht aus drei Teilen, getrennt durch Punkte: kopf.inhalt.signatur. Kopf (Header) und Inhalt (Payload) sind JSON, jeweils in base64url kodiert (wie base64, aber mit - und _ statt + und / und ohne = am Ende). Die Signatur ist bei HS256 ein HMAC-SHA256 (Hash-based Message Authentication Code) über den Text kopf.inhalt, mit einem Secret, das nur der Server kennt.
Ausgabe:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbm5hIiwicm9sbGUiOiJsZXNlciIsImV4cCI6MTcwMDAwMDYwMH0.f8Qs6lTw1sSvXjEqeStLqKCnUsvPmz5V7VzHUgqmU78
b'{"alg":"HS256","typ":"JWT"}'
{'sub': 'anna', 'rolle': 'leser', 'exp': 1700000600}
Signatur in Bytes: 32
Das ist dieselbe Zeichenkette wie in Node. Drei Dinge fallen auf:
- Der Inhalt ist nicht geheim. Jeder kann ihn dekodieren, ohne das Secret. Ein JWT ist signiert (Manipulation fällt auf), aber nicht verschlüsselt. Lege keine Passwörter oder vertrauliche Daten hinein.
sub(subject, Nutzerkennung) undexp(expiration time, Ablaufzeitpunkt als Unix-Sekunden) sind registrierte Claims (Felder im Payload).expist der Grund, warum ein gestohlenes Token nicht ewig lebt.- Der Header sagt, welchen Algorithmus der Aussteller benutzt hat. Das ist der Ursprung der berüchtigtsten Falle (nächster Schritt).
Schritt 4: Ein JWT richtig prüfen (und was alles schiefgeht)
Wer ein Token prüft, geht immer in dieser Reihenfolge vor. Erst wenn alle Schritte bestanden sind, darf er dem Inhalt trauen:
- Form: Genau drei Teile, jeder dekodierbar, Header und Payload sind JSON. Sonst: ungültig.
- Algorithmus aus der Allowlist: Dein Server legt fest, welcher Algorithmus akzeptiert wird (hier nur
HS256), nicht das Token. Alles andere: ungültig. - Signatur neu berechnen über
kopf.inhaltmit deinem Secret und mit dem erhaltenen Wert konstantzeitig vergleichen (hmac.compare_digest). - Erst jetzt den Payload auswerten:
expprüfen. Das Token ist gültig, solangejetzt < exp. Beijetzt == expist es schon abgelaufen. In der Praxis kommeniss(issuer, Aussteller) undaud(audience, Empfänger) dazu.
Fünf typische Fälle für das Token aus Schritt 3 (jetzt = 1700000000), und wo sie scheitern:
| Fall | Was der Angreifer oder Zufall tut | Scheitert in Schritt |
|---|---|---|
| Original | nichts | gültig, Payload wird zurückgegeben |
| Payload manipuliert | "rolle": "admin" einsetzen, alte Signatur behalten |
3 (Signatur passt nicht mehr zum Text) |
alg: none |
Header auf none setzen, Signatur weglassen |
2 (nicht in der Allowlist) |
| Abgelaufen | Token von gestern wird wiederverwendet | 4 (exp liegt in der Vergangenheit) |
| Müll | abc oder kaputtes base64 |
1 (Form) |
Warum ist alg: none gefährlich? Der Standard kennt einen Algorithmus none (“ungesichert”). Eine Bibliothek, die den Algorithmus aus dem Header übernimmt, prüft dann nichts: Der Angreifer schreibt sich ein beliebiges Token mit none und beliebigem Payload. Das ist ein Klassiker. Die Lehre: Vertraue nie einem Feld im Token, um zu entscheiden, wie du dem Token vertraust.
Warum konstantzeitig vergleichen? Ein normaler == auf Strings bricht beim ersten abweichenden Zeichen ab. Wer die Antwortzeit sehr genau misst, kann theoretisch Zeichen für Zeichen die richtige Signatur erraten (Timing-Angriff). hmac.compare_digest braucht unabhängig vom Inhalt gleich lang. Praktisch ist das schwer auszunutzen, aber es kostet nichts, also tust du es immer.
Eine Kleinigkeit zu HS256 gegen RS256: Bei HS256 (symmetrisch, ein gemeinsames Secret) kann jeder, der prüfen kann, auch Tokens ausstellen. Bei RS256 oder ES256 (asymmetrisch) signiert der Aussteller mit einem privaten Schlüssel, und alle anderen Services prüfen nur mit dem öffentlichen Schlüssel. Ein gestohlener Prüf-Service kann dann keine Tokens fälschen. Darum nutzen verteilte Systeme meist asymmetrische Signaturen.
Schritt 6: Passwörter speichern
Du speicherst nie das Passwort, sondern einen Hash. Aber nicht irgendeinen:
Ausgabe:
99ca9d959b395b4cee37275607a2feaa195887d3642c808c394cf916f0e5d2c2
gleich bei zweitem Nutzer: True
934ec7d5314afe86ee3ffcb746558c517cba1d5c76193a003d35db2d18acd3b9
40b9071739a0ff26f9cee5e16937475564a13e9d4a8f0480488cf6c67c3a0108
9fad8646b8608947a51617b7c0861c9bfec340112bf73cb3d70958c1d89d09a2
pbkdf2_sha256$1000$000102030405060708090a0b0c0d0e0f$934ec7d5314afe86ee3ffcb746558c517cba1d5c76193a003d35db2d18acd3b9
Drei Probleme eines einfachen sha256(passwort), jeweils mit ihrem Gegenmittel:
- Gleiches Passwort, gleicher Hash (Zeile 1 und 2). Wer die Datenbank liest, sieht sofort, wer dasselbe Passwort hat, und kann fertige Tabellen (Rainbow Tables) benutzen. Gegenmittel: Salt, ein zufälliger Wert pro Nutzer (Zeile 3 und 4: gleiches Passwort, anderer Salt, ganz anderer Hash). Der Salt ist nicht geheim, er wird neben dem Hash gespeichert.
- Zu schnell. SHA-256 ist für Geschwindigkeit gebaut. Ein Angreifer mit gestohlener Datenbank probiert Milliarden Kandidaten. Gegenmittel: ein absichtlich langsamer Hash. PBKDF2 wiederholt die Berechnung
iterationenmal (Zeile 5: schon eine Iteration mehr ergibt einen völlig anderen Wert). Mit 1000 Iterationen kostet jeder Rateversuch den Angreifer das 1000-Fache. Die Iterationszahl muss mit gespeichert werden, damit man sie später erhöhen kann. - Format: Die letzte Zeile zeigt das übliche Speicherformat
verfahren$iterationen$salt$hash. Beim Prüfen liest du Iterationen und Salt aus dem gespeicherten String, rechnest neu und vergleichst konstantzeitig.
Hinweis zur Praxis: hashlib.pbkdf2_hmac gibt exakt denselben Wert wie die Hilfsfunktion oben (lokal verifiziert), ist aber im Browser nicht verfügbar. In echten Projekten nimmst du Argon2id oder bcrypt aus einer geprüften Bibliothek. Sie sind zusätzlich speicherhungrig (Argon2) oder anders gebaut, sodass auch Grafikkarten-Rechenfarmen kaum Vorteile haben. Konkrete Parameter (Iterationen, Speicher) richtest du nach aktuellen Empfehlungen aus (z. B. OWASP, bitte prüfen), nicht nach diesem Browser-Beispiel.
Schritt 7: Token-Lebenszyklus, Refresh-Token-Rotation
Ein Access Token ist kurzlebig (Minuten), damit ein gestohlenes Token bald wertlos ist. Damit der Nutzer sich nicht ständig neu einloggt, gibt es zusätzlich ein Refresh Token: länger gültig, wird nur am Token-Endpunkt eingelöst und liefert ein neues Access Token. Das Refresh Token ist damit das wertvollste Ziel.
Gegen Diebstahl hilft Refresh-Token-Rotation: Jedes Refresh Token ist einmalig. Beim Einlösen bekommst du ein neues Refresh Token, das alte ist verbraucht. Und die Pointe, die Reuse Detection: Taucht ein bereits verbrauchtes Token wieder auf, hat es jemand kopiert. Der Server weiß nicht, wer der Dieb ist, also sperrt er die ganze Kette (Token Family), und der Nutzer muss sich neu anmelden.
Das Grundmuster “einmal benutzbar” kennst du aus dem Authorization Code (einmalig einlösbar). In Python:
Ausgabe:
anna None None
Beim Refresh-Token kommt zur Einmaligkeit die Familie dazu. Ein durchgerechneter Ablauf (Anna meldet sich an, ein Dieb besitzt eine Kopie von rt2):
| Schritt | Wer | Aktion | Ergebnis | Zustand danach |
|---|---|---|---|---|
| 1 | Anna | Login | rt1 |
rt1 offen |
| 2 | Anna | Refresh mit rt1 |
rt2 |
rt1 verbraucht, rt2 offen |
| 3 | Anna | Refresh mit rt2 |
rt3 |
rt2 verbraucht, rt3 offen |
| 4 | Dieb | Refresh mit rt2 (alt, kopiert) |
abgelehnt, Reuse erkannt | ganze Familie gesperrt |
| 5 | Anna | Refresh mit rt3 |
abgelehnt | sie muss sich neu anmelden |
Schritt 5 wirkt hart: Anna ist unschuldig und fliegt trotzdem raus. Das ist der Handel. Der Server kann Annas Gerät und den Dieb nicht unterscheiden, und die sichere Seite ist, beide auszusperren. Spielt der Dieb zuerst (er löst rt3 ein, bevor Anna es tut), trifft es genau andersherum: Annas späterer Versuch mit rt3 löst den Alarm aus und sperrt auch das neue Token des Diebs.
Schritt 8: Kurzüberblick ohne Übung
Drei Themen aus der Quelle, die du kennen musst, aber hier nicht selbst baust:
| Thema | Kernaussage |
|---|---|
| Symmetrisch vs. asymmetrisch | Symmetrisch: ein Schlüssel zum Ver- und Entschlüsseln bzw. Signieren (schnell, Schlüsselaustausch ist das Problem). Asymmetrisch: Schlüsselpaar, privat zum Signieren oder Entschlüsseln, öffentlich zum Prüfen oder Verschlüsseln. |
| AEAD (authenticated encryption with associated data) | Verschlüsselung, die Manipulation bemerkt (z. B. AES-GCM, ChaCha20-Poly1305). Verschlüsseln ohne Integritätsprüfung ist ein Fehler. |
| Key Management und Secrets | Schlüssel und Secrets gehören nicht ins Repository, sondern in einen Secret Store oder ein KMS (Key Management Service), mit Rotation. Envelope Encryption: Daten mit einem Datenschlüssel verschlüsseln, den Datenschlüssel mit einem Hauptschlüssel. Least Privilege: jeder Dienst bekommt nur die Rechte, die er braucht. |
Falle
- Dem Token vertrauen, wie es behauptet.
algaus dem Header übernehmen,expnicht prüfen, den Payload auswerten, bevor die Signatur geprüft ist. - JWT als Datenspeicher für Geheimnisse. Der Payload ist lesbar, nicht verschlüsselt.
- Langlebige JWTs ohne Widerruf. Ein 24-Stunden-Token lässt sich nicht sperren. Kurze Lebensdauer plus Refresh-Token, oder gleich Sessions.
- Passwörter mit schnellem Hash oder ohne Salt speichern. Dazu gehört der Irrtum, “SHA-256 ist doch sicher”. Der Hash ist sicher, aber für Passwörter viel zu schnell.
- Implicit Flow oder Code ohne PKCE in öffentlichen Clients (Single-Page-Apps, Mobile Apps).
- Kryptografie selbst bauen. Diese Lektion baut Teile nach, um sie zu verstehen. Im Projekt nutzt du geprüfte Bibliotheken für JWT, Passwort-Hashing und Verschlüsselung. Eigene Konstruktionen haben fast immer Lücken, die du erst bemerkst, wenn es zu spät ist.
- Reuse Detection ohne Bedacht auf Parallelität. Zwei Tabs, die gleichzeitig dasselbe Refresh Token einlösen, sehen aus wie ein Diebstahl. Produktionssysteme lassen darum oft eine kurze Schonfrist zu (allgemeines Fachwissen).
Übungen
Übung 1: JWT selbst prüfen (ca. 10 Min.)
Schreibe verifiere(token, secret, jetzt). Die Funktion gibt das Payload als dict zurück, wenn das Token gültig ist, sonst None. Sie darf nie eine Exception werfen, auch nicht bei Müll als Eingabe. Regeln:
- Genau drei Teile, Header und Payload sind JSON.
- Akzeptiert wird ausschließlich
HS256(Allowlist, der Server entscheidet). - Signatur über
kopf.inhaltmitsecret(bytes) neu berechnen und mithmac.compare_digestvergleichen. - Ein Token ohne
expist ungültig. Es ist gültig, solangejetzt < exp(jetztin Unix-Sekunden).
Vorhanden sind b64url(bytes) und b64url_zurueck(text) aus dem Text.
Packe alles, was bei kaputten Eingaben schiefgehen kann (Aufteilen, Dekodieren, JSON), in einen gemeinsamen Schutz. Und überlege, in welcher Reihenfolge du Algorithmus, Signatur und Ablauf prüfst: Welchem Teil darfst du erst nach der Signaturprüfung trauen?
import hashlib, hmac, json
def verifiere(token, secret, jetzt):
try:
kopf, inhalt, signatur = token.split(".")
header = json.loads(b64url_zurueck(kopf))
if header.get("alg") != "HS256":
return None
erwartet = hmac.new(secret, f"{kopf}.{inhalt}".encode(), hashlib.sha256).digest()
if not hmac.compare_digest(erwartet, b64url_zurueck(signatur)):
return None
payload = json.loads(b64url_zurueck(inhalt))
if "exp" not in payload or not jetzt < payload["exp"]:
return None
return payload
except Exception:
return None
verifiereDie Reihenfolge ist Form, Algorithmus aus der Allowlist, Signatur, erst dann exp. Der Algorithmus wird mit dem Wert im Server-Code verglichen, nicht aus dem Token übernommen. compare_digest verhindert Timing-Angriffe, und das try/except fängt kaputte Eingaben ab.
Übung 2: PKCE selbst bauen (ca. 8 Min.)
Schreibe drei Funktionen:
neuer_verifier(): ein kryptografisch sicherercode_verifiermitsecrets(nichtrandom), 43 bis 128 Zeichen, nur ausA-Z a-z 0-9 - . _ ~.challenge_s256(verifier):base64url(SHA256(verifier))ohne=.token_endpunkt(gespeicherte_challenge, verifier): gibtTruezurück, wenn der Code mit diesem Verifier eingelöst werden darf, sonstFalse. Fehlt der Verifier (Noneoder leer), ist esFalse. Vergleiche konstantzeitig.
secrets hat eine Funktion, die schon URL-taugliche Zeichen liefert. Beim Hash: Du brauchst die rohen Bytes, nicht den Hex-String. Und was macht urlsafe_b64encode am Ende, das PKCE nicht will?
import base64, hashlib, hmac, secrets
def neuer_verifier():
return secrets.token_urlsafe(32)
def challenge_s256(verifier):
digest = hashlib.sha256(verifier.encode("ascii")).digest()
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
def token_endpunkt(gespeicherte_challenge, verifier):
if not verifier:
return False
return hmac.compare_digest(challenge_s256(verifier), gespeicherte_challenge)
neuer_verifier, challenge_s256, token_endpunktsecrets.token_urlsafe(32) liefert 43 Zeichen aus A-Z a-z 0-9 - _. Die Challenge ist der Hash in base64url ohne Padding. Der Token-Endpunkt rechnet selbst nach und lässt Anfragen ohne Verifier gar nicht erst durch.
Übung 3: Passwort speichern und prüfen (ca. 8 Min.)
Schreibe hash_passwort(passwort, iterationen=1000, salt=None) und pruefe_passwort(passwort, gespeichert).
hash_passwortgibtpbkdf2_sha256$<iterationen>$<salt_hex>$<hash_hex>zurück. Ohnesalt(bytes) erzeugst du 16 zufällige Byte mitsecrets. Das Passwort ist einstrund wird als UTF-8 kodiert.pruefe_passwortliest Iterationen und Salt aus dem gespeicherten String, rechnet neu und vergleicht mithmac.compare_digest. Für kaputte oder fremde Formate gibt sieFalsezurück, ohne Exception.- Die Hilfsfunktion
pbkdf2(passwort_bytes, salt_bytes, iterationen)gibt 32 Byte zurück (aus dem Text, hier vorhanden).
Beim Prüfen darfst du dich auf nichts verlassen, was im Code steht und sich später ändern kann: Woher kommen die Parameter für die Neuberechnung? Und was passiert beim Zerlegen eines kaputten Strings?
import hmac, secrets
def hash_passwort(passwort, iterationen=1000, salt=None):
if salt is None:
salt = secrets.token_bytes(16)
h = pbkdf2(passwort.encode("utf-8"), salt, iterationen)
return f"pbkdf2_sha256${iterationen}${salt.hex()}${h.hex()}"
def pruefe_passwort(passwort, gespeichert):
try:
verfahren, iterationen, salt_hex, hash_hex = gespeichert.split("$")
if verfahren != "pbkdf2_sha256":
return False
neu = pbkdf2(passwort.encode("utf-8"), bytes.fromhex(salt_hex), int(iterationen))
return hmac.compare_digest(neu, bytes.fromhex(hash_hex))
except Exception:
return False
hash_passwort, pruefe_passwortDer Salt ist pro Nutzer zufällig und wird mit gespeichert. Die Iterationen stehen im String, damit alte Hashes auch nach einer Erhöhung der Standardzahl prüfbar bleiben. compare_digest vergleicht in konstanter Zeit, das try/except macht kaputte Datensätze zu einem harmlosen False.
Übung 4: Refresh-Token-Rotation mit Reuse Detection (ca. 8 Min.)
Ergänze refresh(self, token). Vorgegeben sind der Speicher self.tokens (Token zu Eintrag mit familie und benutzt), die Menge self.gesperrt (gesperrte Familien) und _ausstellen(familie), das ein neues Token erzeugt. Regeln:
- Unbekanntes Token:
None. - Token einer gesperrten Familie:
None. - Token ist schon benutzt (Reuse): die ganze Familie sperren und
Nonezurückgeben. - Sonst: Token als benutzt markieren und ein neues Token derselben Familie zurückgeben.
Die Reihenfolge der Fragen zählt: Erst klären, ob das Token überhaupt existiert und seine Familie noch lebt, dann, ob es schon benutzt wurde. Und überlege, was nach einem Reuse mit den noch unbenutzten Tokens derselben Familie passieren soll.
class AuthServer:
def __init__(self):
self.tokens = {}
self.gesperrt = set()
self.n = 0
def _ausstellen(self, familie):
self.n += 1
token = f"rt{self.n}"
self.tokens[token] = {"familie": familie, "benutzt": False}
return token
def login(self, user):
return self._ausstellen("familie_" + user)
def refresh(self, token):
eintrag = self.tokens.get(token)
if eintrag is None:
return None
familie = eintrag["familie"]
if familie in self.gesperrt:
return None
if eintrag["benutzt"]:
self.gesperrt.add(familie)
return None
eintrag["benutzt"] = True
return self._ausstellen(familie)
AuthServerReuse sperrt die Familie, und weil jeder Aufruf zuerst self.gesperrt prüft, sind danach auch die noch unbenutzten Tokens der Familie wertlos. Andere Familien sind nicht betroffen.
Übung 5: Sessions oder Tokens, und wer scheitert bei PKCE (ca. 6 Min.)
Zwei Fälle, je mit genau einer besten Antwort aus den genannten Randbedingungen. Gib ein Tupel (a, b) mit zwei Buchstaben zurück.
Fall 1. Eine interne Verwaltungsanwendung: ein einziges Backend mit einer gemeinsamen Datenbank, 150 Mitarbeitende, nur Browser, keine Drittanbieter oder Mobile Apps. Scheidet jemand aus, muss sein Zugang binnen Sekunden gesperrt sein. Welche Lösung passt?
- A: JWT mit 24 Stunden Gültigkeit im Cookie, das der Server nur auf die Signatur prüft, ohne Sperrliste.
- B: Zufällige Session-ID im HttpOnly-Cookie, Zustand in der Datenbank, Sperren durch Löschen des Eintrags.
- C: JWT mit 15 Minuten Gültigkeit und Refresh Token, das der Server nur auf die Signatur prüft, ohne Sperrliste.
- D: API-Schlüssel pro Mitarbeiter, einmal vergeben, im Browser gespeichert, ohne Ablauf oder Rotation.
Fall 2. Eine Single-Page-App (öffentlicher Client, kein Client Secret möglich) meldet Nutzer über einen Identity Provider an. Auf demselben Gerät liest eine bösartige App die Weiterleitungs-URL mit. Welche Aussage stimmt?
- A: Der Implicit Flow genügt: Das Token steht nur im URL-Fragment und wird nie an einen Server gesendet.
- B: Der Code ohne PKCE genügt: Ein Code ist nur einmal einlösbar, also bleibt die Mitleserin wirkungslos.
- C: Mit PKCE ist der Code verschlüsselt: Nur die App besitzt den Schlüssel, um ihn zu lesen und einzulösen.
- D: Mit PKCE fehlt der Mitleserin der Verifier, den der Token-Endpunkt gegen die Challenge prüft.
Fall 1: Welche Randbedingung lässt sich mit einem reinen Signaturcheck prinzipiell nicht erfüllen? Fall 2: Was reist durch die Weiterleitung, und was reist nur auf direktem Weg zum Server?
antwort = ("B", "D")
antwortFall 1: Sofortiges Sperren braucht serverseitigen Zustand, den man löschen kann, und ein einziges Backend mit gemeinsamer Datenbank hat damit kein Skalierungsproblem. Fall 2: Der Verifier reist nie über die Weiterleitung, der Token-Endpunkt rechnet beim Einlösen nach, und ohne den richtigen Verifier scheitert die Einlösung.
Merksatz
Vertraue einem Token erst, nachdem dein Server Algorithmus, Signatur und Ablauf geprüft hat, schütze Codes mit PKCE statt Implicit Flow, speichere Passwörter nur mit Salt und langsamem Hash, rotiere Refresh Tokens mit Reuse Detection, und baue Kryptografie nie selbst.
Prüfstein
- Warum Authorization Code mit PKCE statt Implicit Flow?
- Warum reicht Eingabevalidierung im Frontend nicht, und wo gehört Autorisierung hin?
- Ein Kollege schlägt vor, bei einem JWT einfach den
algaus dem Header zu verwenden. Was kann dann passieren?
Quelle: quellen/konzeptuebersicht-software-grundlagen.md, Abschnitt “8. Security und Qualität” (Identität: Authentifizierung vs. Autorisierung, Sessions vs. Tokens, OAuth2/OIDC, JWT mit seinen Fallstricken; Kryptografie-Grundlagen: Hashing, symmetrisch vs. asymmetrisch, Signaturen; Hygiene: Secrets-Management, Least Privilege; Prüfstein zur Frontend-Validierung); quellen/konzeptuebersicht-software-fortgeschritten.docx, Schicht 8 (Authentifizierung vertieft: OAuth2 Authorization Code mit PKCE, OIDC, Refresh-Token-Rotation, Token-Lebenszyklen; Kryptografie in der Praxis: Passwort-Hashing Argon2 und bcrypt, AEAD, Key Management, nichts selbst bauen; Prüfstein zu PKCE statt Implicit Flow).
Über die Quelle hinaus (allgemeines Fachwissen): Aufbau und Prüfreihenfolge eines JWT, alg: none, Timing-Angriffe, HS256 gegen RS256, Rollen und Ablauf von OAuth2 und PKCE (Verifier, Challenge, Methode S256, Länge 43 bis 128 Zeichen), die Schwächen des Implicit Flow (Stand der Empfehlung in OAuth 2.0 Security Best Current Practice und OAuth 2.1: bitte prüfen), Salt, Iterationen und Speicherformat beim Passwort-Hashing (konkrete Parameter für PBKDF2, Argon2 und bcrypt: bitte prüfen), die Funktionsweise der Reuse Detection und die Schonfrist bei Parallelität, Cookie-Flags HttpOnly, Secure, SameSite. Die Hashes, Token, Längen und Ausgaben stammen aus ausgeführtem Python (3.13 lokal), die PBKDF2-Hilfsfunktion wurde lokal gegen hashlib.pbkdf2_hmac und im Browser (Pyodide) auf Funktion geprüft, das Node-Beispiel mit Node 20 ausgeführt. In Pyodide fehlen hashlib.pbkdf2_hmac und hashlib.scrypt (im Browser getestet), darum die Hilfsfunktion.