Zustand und Identität · Übung
Bearer-Tokens und die Grenze bei 403
Eine Anmeldung weiß jetzt, wer ein Benutzer ist. Den Büchern ist das noch egal: POST, PUT und DELETE nehmen weiterhin jeden an. Starte das Szenario tokens des Hilfsprogramms:
python pl_http.py tokens
Es sendet viermal dasselbe POST /books, mit einem Content-Length: ohne Zugangsdaten, mit einem Token, das niemand ausgegeben hat, mit einem Token, das nur lesen darf, und mit einem Token, das schreiben darf. Dann löscht es Buch 3 mit dem Token des Schreibers. Heute antworten alle vier POSTs mit HTTP/1.0 201 Created, mit location: /books/3, /books/4, /books/5 und /books/6: vier neue Bücher, drei davon von Clients, die nichts hätten hinzufügen können sollen.
Ein Token statt eines Passworts
Clients, die die Authorization: Bearer <token> (RFC 6750). Ein Token ist ein einziger langer geheimer
Tokens haben zwei Vorteile gegenüber dem Senden eines Passworts bei jedem Schreibzugriff. Das Passwort bleibt zu Hause: Nur die Anmeldung sendet es je. Und ein Token kann für sich allein eingeschränkt und zurückgezogen werden: Dieses darf nur lesen, jenes funktioniert ab morgen nicht mehr, ohne dass ein Passwort geändert wird. Hier werden Tokens im Voraus ausgegeben, wie die API-Schlüssel, die ein Dienst seinen Benutzern gibt, und sample_state() enthält zwei:
tokens = {
"tok-ana-writes-7Qx2": {"user": "ana", "can_write": True},
"tok-ben-reads-K9pz": {"user": "ben", "can_write": False},
}
Wie ein echter Dienst Tokens ausgibt, oft über ein Protokoll namens OAuth, liegt außerhalb dieses Kurses.
bearer_token(request) liest den Header so, wie es basic_credentials getan hat: genau ein authorization-bearer in beliebiger Schreibweise und der Rest ohne Leerzeichen am Rand. Wenn das Verfahren falsch oder der Rest leer ist, gibt es kein Token, und es gibt None zurück.
Was gibt das aus?
print("Bearer tok".partition(" "))
Das Token kommt als ' tok' zurück, mit dem zusätzlichen Leerzeichen vorne, also entferne es, bevor du nachschlägst. Bei Tokens selbst zählt die Groß- und Kleinschreibung: TOK-ana-writes-7Qx2 ist nicht dasselbe Token und bekommt keine Sonderbehandlung.
401, 403, 429
Drei Status lehnen eine Anfrage ab, weil es darauf ankommt, wer sie gesendet hat oder wann:
| Anfrage | Antwort | Was sie sagt |
|---|---|---|
POST /books, kein Token oder ein unbekanntes | 401 Unauthorized, mit www-authenticate: Bearer | „Wer bist du? Sende ein Token.“ |
POST /books, ein Token, das nur lesen darf | 403 Forbidden | „Ich weiß, wer du bist, und nein.“ |
| zu viele Anfragen in kurzer Zeit | 429 Too Many Requests | „Nicht jetzt. Versuch es später noch einmal.“ |
401 bedeutet, dass der Server nicht weiß, wer du bist: Es gab keine Zugangsdaten oder welche, die er nicht erkennt. Sie trägt eine Challenge, die sagt, wie man sich authentifiziert, hier den bloßen Namen des Verfahrens, www-authenticate: Bearer, gebaut mit deinem unauthorized("Bearer"). 403 bedeutet, dass der Server genau weiß, wer du bist, und trotzdem ablehnt. Dasselbe Token noch einmal zu senden, ändert die Antwort nicht, also braucht sie keine Challenge. 429, die du in Python in der Praxis kennengelernt hast, betrifft das Timing, nicht die Identität: Dieselbe Anfrage kann später gelingen. Wenn man sie auseinanderhält, weiß ein Client, was er als Nächstes tun soll: Zugangsdaten besorgen, aufgeben oder warten.
POST /books kommt ohne Header authorization an. Welche Antwort?
Ein bekanntes Token, das nur lesen darf, sendet DELETE /books/7. Welcher Status?
write_error(request, state) trifft diese Entscheidung. Hol das Token mit bearer_token; wenn es eins gibt, schlag es mit state["tokens"].get(token) nach. Kein Token oder kein Eintrag: Gib unauthorized("Bearer") zurück. Ein Eintrag, dessen can_write falsch ist: Gib einen 403-JSON-Fehler zurück. Andernfalls gib None zurück: Diese Anfrage darf schreiben.
Wohin die Prüfung kommt
SERVICE_CONTRACT.md legt die Reihenfolge jeder Prüfung bei einem Schreibzugriff fest, und das Token nimmt den dritten Platz ein:
Route und
, in handle: 404 und 405.Rahmung, in
read_body, bevorhandleläuft: 411, 400, 413.Zugangsdaten: 401 und 403.
Die Buch-ID: 404.
Der Body: 415, 400, 422.
Zugangsdaten vor der Buch-ID bedeuten, dass ein Client ohne gültiges Token nichts darüber erfährt, welche Bücher existieren: PUT /books/99 bekommt dieselbe 401, ob Buch 99 existiert oder nicht. Zugangsdaten vor dem Body bedeuten, dass der Schreibzugriff eines Fremden abgelehnt wird, ohne geparst zu werden. Ein abgelehnter Schreibzugriff ändert nichts am Zustand. Ein Preis dieser Reihenfolge: Die Rahmung kommt zuerst, also kann ein Client ohne Token den Server trotzdem bis zu 10000 Bytes lesen lassen, bevor er abgelehnt wird. Der Vertrag sagt das.
PUT /books/99, wobei kein Buch 99 existiert, kommt ohne Token an. Welcher Status?
In handle ruft der POST-Zweig von /books zuerst write_error auf und prüft den Body nur, wenn es None zurückgibt. Im Zweig für /books/{id} ruft ein PUT oder DELETE write_error auf, bevor die Buch-ID geprüft wird. GET und HEAD brauchen kein Token, und eine Methode, die ein Pfad nicht erlaubt, bekommt weiterhin 405.
Deine Aufgabe
Schreib in reading_server.py bearer_token(request) und write_error(request, state) und ruf write_error wie beschrieben vor jedem der drei Schreibzugriffe auf. Starte dann das Szenario noch einmal:
python pl_http.py tokens
Die ersten beiden POSTs antworten jetzt mit HTTP/1.0 401 Unauthorized, jede mit dieser Zeile darunter:
www-authenticate: Bearer
Das Token des Lesers bekommt HTTP/1.0 403 Forbidden, und nur das POST des Schreibers legt ein Buch an, location: /books/3, das das DELETE dann mit HTTP/1.0 204 No Content entfernt. Führ auch python -m pytest -q aus: Deine Tests bestehen weiterhin, weil jeder Schreibzugriff in tests/test_server.py das Beispiel-Token des Schreibers sendet.
Dieser Server findet ein Token über einen hmac.compare_digest, das gleich lange dauert, egal ob sich das erste oder das letzte Zeichen unterscheidet; Angriffe, die diese Zeit messen, liegen außerhalb dieses Kurses.
Drück dann Check my work. Jeder Schreibzugriff ist jetzt geschützt, und die nächste Lektion, das Projekt des Kapitels, lässt eine Session enden.
Aufgabe
Schütze in reading_server.py jeden Schreibzugriff mit einem Bearer-Token.
bearer_token(request)gibt das Token aus einem Headerauthorizationzurück, oderNone. Es muss genau einenauthorization-geben, sein Verfahren muss bearerin beliebiger Schreibweise sein, und das Token ist der Rest, ohne Leerzeichen am Rand. Ein leeres Token istNone.write_error(request, state)gibtNonezurück, wenn die Anfrage schreiben darf. Ohne Token oder mit einem Token, das nicht instate["tokens"]steht, gibt esunauthorized("Bearer")zurück: eine 401 mitwww-authenticate: Bearer. Mit einem bekannten Token, dessencan_writefalsch ist, gibt es einen 403--Fehler zurück. Tokens werden exakt verglichen, einschließlich Groß- und Kleinschreibung. Ruf in
handlewrite_errorfürPOST /books,PUT /books/{id}undDELETE /books/{id}auf, nachdem Route undfeststehen und bevor die Buch-ID oder der Body geprüft wird. Wenn es eine Antwort zurückgibt, antworte damit und ändere nichts. GETundHEADbrauchen kein Token, und eine Methode, die ein Pfad nicht annimmt, bekommt weiterhin 405.Keine Antwort wiederholt ein Token. Formuliere die Fehler, wie du willst.
Führ python pl_http.py tokens im python -m pytest -q, um zu sehen, dass deine Tests weiterhin bestehen.
Drück dann Check my work. Es ruft bearer_token mit Headern auf, die du noch nicht gesehen hast, und handle mit einem eigenen Zustand und Tokens, die du noch nicht gesehen hast: POST, PUT und DELETE ohne Header, mit einem unbekannten Token, mit dem Token des Schreibers in anderer Schreibweise, mit einem fehlerhaften Header, mit dem Token des Lesers und mit dem Token des Schreibers. Es prüft jeden Status, das Verfahren in www-authenticate, dass der Zustand nach jeder 401 und 403 unverändert ist, die Reihenfolge gegenüber einer fehlenden Buch-ID und einem schlechten Body, dass Lesezugriffe kein Token brauchen und dass keine Antwort ein Token wiederholt. Nichts aus deinem Terminal wird bewertet.