Toestand en identiteit · oefening
Bearer-tokens en de grens van 403
Een login weet nu wie een gebruiker is. De boeken trekken zich daar nog niets van aan: POST, PUT en DELETE accepteren nog steeds iedereen. Voer het scenario tokens van het hulpprogramma uit:
python pl_http.py tokens
Het stuurt vier keer dezelfde POST /books, met een Content-Length: zonder inloggegevens, met een token dat niemand heeft uitgegeven, met een token dat alleen mag lezen, en met een token dat mag schrijven. Daarna verwijdert het boek 3 met het token van de schrijver. Vandaag antwoorden alle vier de POSTs met HTTP/1.0 201 Created, met location: /books/3, /books/4, /books/5 en /books/6: vier nieuwe boeken, waarvan drie van clients die niets hadden mogen kunnen toevoegen.
Een token in plaats van een wachtwoord
Clients die de Authorization: Bearer <token> (RFC 6750). Een token is één lange geheime
Tokens hebben twee voordelen ten opzichte van een wachtwoord bij elke schrijfactie meesturen. Het wachtwoord blijft thuis: alleen de login stuurt het ooit. En een token kan op zichzelf worden beperkt en ingetrokken: dit token mag alleen lezen, dat token werkt morgen niet meer, zonder dat er een wachtwoord verandert. Hier worden tokens vooraf uitgegeven, zoals de API-sleutels die een service aan zijn gebruikers geeft, en sample_state() bevat er twee:
tokens = {
"tok-ana-writes-7Qx2": {"user": "ana", "can_write": True},
"tok-ben-reads-K9pz": {"user": "ben", "can_write": False},
}
Hoe een echte service tokens uitgeeft, vaak via een protocol dat OAuth heet, valt buiten deze cursus.
bearer_token(request) leest de header zoals basic_credentials deed: precies één authorization-bearer met willekeurige hoofdletters, en de rest gestript. Als het schema verkeerd is of de rest leeg, is er geen token en geeft de None terug.
Wat drukt dit af?
print("Bearer tok".partition(" "))
Het token komt terug als ' tok', met de extra spatie ervoor, dus strip het voordat je het opzoekt. Tokens zelf zijn hoofdlettergevoelig: TOK-ana-writes-7Qx2 is niet hetzelfde token, en krijgt geen speciale behandeling.
401, 403, 429
Drie statussen weigeren een verzoek vanwege wie het stuurde, of wanneer:
| Verzoek | Antwoord | Wat het zegt |
|---|---|---|
POST /books, geen token of een onbekend token | 401 Unauthorized, met www-authenticate: Bearer | “Wie ben je? Stuur een token.” |
POST /books, een token dat alleen mag lezen | 403 Forbidden | “Ik weet wie je bent, en nee.” |
| te veel verzoeken in korte tijd | 429 Too Many Requests | “Nu niet. Probeer het later opnieuw.” |
401 betekent dat de server niet weet wie je bent: er was geen inloggegeven, of een dat hij niet herkent. Het draagt een challenge die zegt hoe je je kunt authenticeren, hier de kale schemanaam, www-authenticate: Bearer, gebouwd met je unauthorized("Bearer"). 403 betekent dat de server precies weet wie je bent en toch weigert. Hetzelfde token opnieuw sturen verandert het antwoord niet, dus er is geen challenge nodig. 429, die je in Python in Practice tegenkwam, gaat over timing, niet over identiteit: hetzelfde verzoek kan later slagen. Ze uit elkaar houden vertelt een client wat hij moet doen: een inloggegeven halen, opgeven, of wachten.
Er komt POST /books binnen zonder header authorization. Welk antwoord?
Een bekend token dat alleen mag lezen, stuurt DELETE /books/7. Welke status?
write_error(request, state) neemt die beslissing. Haal het token op met bearer_token; als er een is, zoek je het op met state["tokens"].get(token). Geen token of geen entry: geef unauthorized("Bearer") terug. Een entry waarvan can_write onwaar is: geef een 403-JSON-fout terug. Geef anders None terug: dit verzoek mag schrijven.
Waar de controle komt
SERVICE_CONTRACT.md legt de volgorde van elke controle op een schrijfactie vast, en het token neemt de derde plaats in:
Route en
, in handle: 404 en 405.Framing, in
read_body, voordathandledraait: 411, 400, 413.Inloggegevens: 401 en 403.
De boek-id: 404.
De body: 415, 400, 422.
Inloggegevens vóór de boek-id betekent dat een client zonder geldig token niets te weten komt over welke boeken er bestaan: PUT /books/99 krijgt dezelfde 401, of boek 99 nu bestaat of niet. Inloggegevens vóór de body betekent dat de schrijfactie van een vreemde wordt geweigerd zonder te worden geparset. Een geweigerde schrijfactie verandert niets aan de state. Eén nadeel van deze volgorde: framing komt eerst, dus een client zonder token kan de server nog steeds tot 10000 bytes laten lezen voordat hij wordt geweigerd. Het contract zegt dat ook.
PUT /books/99, terwijl er geen boek 99 bestaat, komt binnen zonder token. Welke status?
In handle roept de POST-tak van /books eerst write_error aan, en controleert hij de body alleen als die None teruggeeft. In de tak /books/{id} roept een PUT of DELETE write_error aan voordat de boek-id wordt gecontroleerd. GET en HEAD hebben geen token nodig, en een methode die een pad niet toestaat, krijgt nog steeds 405.
Jouw opdracht
Schrijf in reading_server.py bearer_token(request) en write_error(request, state), en roep write_error aan vóór elk van de drie schrijfacties zoals beschreven. Voer daarna het scenario opnieuw uit:
python pl_http.py tokens
De eerste twee POSTs antwoorden nu met HTTP/1.0 401 Unauthorized, elk met deze regel eronder:
www-authenticate: Bearer
Het token van de lezer krijgt HTTP/1.0 403 Forbidden, en alleen de POST van de schrijver maakt een boek aan, location: /books/3, dat de DELETE daarna verwijdert met HTTP/1.0 204 No Content. Voer ook python -m pytest -q uit: je tests slagen nog steeds, omdat elke schrijfactie in tests/test_server.py het voorbeeldtoken van de schrijver meestuurt.
Deze server vindt een token met een hmac.compare_digest, dat even lang duurt of nu het eerste of het laatste teken verschilt; aanvallen die die tijd meten, vallen buiten deze cursus.
Klik daarna op Check my work. Elke schrijfactie wordt nu bewaakt, en de volgende les, het project van het hoofdstuk, laat een sessie eindigen.
Opdracht
Bescherm in reading_server.py elke schrijfactie met een bearer-token.
bearer_token(request)geeft het token uit een headerauthorizationterug, ofNone. Er moet precies éénauthorization-zijn, het schema moet bearerzijn met willekeurige hoofdletters, en het token is de rest, gestript. Een leeg token isNone.write_error(request, state)geeftNoneterug als het verzoek mag schrijven. Zonder token, of met een token dat niet instate["tokens"]staat, geeft hijunauthorized("Bearer")terug: een 401 metwww-authenticate: Bearer. Met een bekend token waarvancan_writeonwaar is, geeft hij een 403--fout terug. Tokens worden exact vergeleken, hoofdletters inbegrepen. Roep in
handlewrite_erroraan voorPOST /books,PUT /books/{id}enDELETE /books/{id}nadat route enbekend zijn en voordat de boek-id of de body wordt gecontroleerd. Als die een antwoord teruggeeft, antwoord je daarmee en verander je niets. GETenHEADhebben geen token nodig, en een methode die een pad niet accepteert, krijgt nog steeds 405.Geen enkel antwoord herhaalt een token. Formuleer de fouten zoals je wilt.
Voer python pl_http.py tokens uit in de python -m pytest -q om te zien dat je tests nog steeds slagen.
Klik daarna op Check my work. Die roept bearer_token aan met headers die je nog niet hebt gezien, en handle met een eigen state en tokens die je nog niet hebt gezien: POST, PUT en DELETE zonder header, met een onbekend token, met het token van de schrijver in andere hoofdletters, met een misvormde header, met het token van de lezer, en met het token van de schrijver. Hij controleert elke status, het schema van www-authenticate, dat de state na elke 401 en 403 ongewijzigd is, de volgorde tegenover een ontbrekende boek-id en een foute body, dat leesacties geen token nodig hebben, en dat geen enkel antwoord een token herhaalt. Niets uit je terminal wordt beoordeeld.