0%

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 -body en de bijbehorende 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 wijzigen, sturen een token: Authorization: Bearer <token> (RFC 6750). Een token is één lange geheime , en het is het hele inloggegeven. Wie het heeft, wie het “draagt” (bears), komt erin, en daar komt de naam vandaan. Er is geen gebruikersnaam om te sturen en geen wachtwoord om te controleren.

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-, gepartitioneerd bij de eerste spatie, het schema 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:

VerzoekAntwoordWat het zegt
POST /books, geen token of een onbekend token401 Unauthorized, met www-authenticate: Bearer“Wie ben je? Stuur een token.”
POST /books, een token dat alleen mag lezen403 Forbidden“Ik weet wie je bent, en nee.”
te veel verzoeken in korte tijd429 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:

  1. Route en , in handle: 404 en 405.

  2. Framing, in read_body, voordat handle draait: 411, 400, 413.

  3. Inloggegevens: 401 en 403.

  4. De boek-id: 404.

  5. 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 -lookup. Een service die één geheim met één verwachte waarde vergelijkt, gebruikt 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 header authorization terug, of None. Er moet precies één authorization- zijn, het schema moet bearer zijn met willekeurige hoofdletters, en het token is de rest, gestript. Een leeg token is None.

  • write_error(request, state) geeft None terug als het verzoek mag schrijven. Zonder token, of met een token dat niet in state["tokens"] staat, geeft hij unauthorized("Bearer") terug: een 401 met www-authenticate: Bearer. Met een bekend token waarvan can_write onwaar is, geeft hij een 403--fout terug. Tokens worden exact vergeleken, hoofdletters inbegrepen.

  • Roep in handle write_error aan voor POST /books, PUT /books/{id} en DELETE /books/{id} nadat route en bekend zijn en voordat de boek-id of de body wordt gecontroleerd. Als die een antwoord teruggeeft, antwoord je daarmee en verander je niets.

  • GET en HEAD hebben 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 om de 401’s, de 403 en de ene schrijfactie die slaagt te zien, en 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.