Toestand en identiteit · oefening
Basic-authenticatie
Je login start nog steeds een sessie voor iedereen. Voer het scenario basic van het hulpprogramma uit om te zien hoe ver dat gaat:
python pl_http.py basic
Het stuurt drie eigen logins, elk met Content-Length: 0: één zonder inloggegevens, één voor ana met een verkeerd wachtwoord, en één met haar juiste wachtwoord, beide uit sample_state(). Daarna roept het je login_and_me aan met de juiste gegevens. Zo gaat het verkeerde wachtwoord de deur uit:
client -> server b'POST /login HTTP/1.1\r\nHost: books.test\r\nAuthorization: Basic YW5hOm5vdCB0aGUgcGFzc3dvcmQ=\r\nContent-Length: 0\r\n\r\n' (112 bytes)
Vandaag antwoorden alle vier de logins met HTTP/1.0 200 OK met een verse set-cookie, ook het verkeerde wachtwoord, en je client leest me: {'user': 'guest'}. Deze les laat de login controleren wie er vraagt.
De header
Die regel Authorization is het Basic-schema van Basic, één spatie, en daarna user:password. Decodeer YW5hOm5vdCB0aGUgcGFzc3dvcmQ= en je krijgt ana:not the password. De schemanaam is niet hoofdlettergevoelig, dus basic is hetzelfde schema.
De dubbele punt scheidt de twee delen, en de regels maken een gebruikersnaam met een dubbele punt erin onmogelijk. Een wachtwoord kan wel dubbele punten bevatten: a:b:c is een geldig wachtwoord. Splits dus alleen bij de eerste dubbele punt. str.partition(":") doet precies dat en geeft het deel vóór de eerste dubbele punt terug, de dubbele punt, en alles erna. Als er geen dubbele punt is, is het middelste deel "".
Wat drukt dit af?
print("ana:pa:ss".partition(":"))
Weiger met een challenge
Een login met ontbrekende of verkeerde inloggegevens krijgt 401 Unauthorized. De naam is oud en misleidend: 401 betekent “ik weet niet wie je bent”, niet “je mag niet”. Een 401 moet de client ook vertellen hoe hij kan bewijzen wie hij is, in een header WWW-Authenticate (RFC 9110, sectie 15.5.2). Voor deze server is de challenge de constante REALM, die al in reading_server.py staat:
www-authenticate: Basic realm="reading list", charset="UTF-8"
Basic noemt het schema. De realm is een naam voor het beschermde gebied, die een browser kan tonen als hij om een wachtwoord vraagt, en charset="UTF-8" zegt dat de server de inloggegevens als UTF-8 decodeert. unauthorized(challenge) bouwt dat antwoord: een www-authenticate op challenge.
Er komt POST /login binnen met een bekende gebruiker en een verkeerd wachtwoord. Welk antwoord?
Decodeer strikt, vergelijk daarna
basic_credentials(request) maakt van de header {"user": ..., "password": ...}, of geeft None terug als er niets bruikbaars is:
Neem
request["headers"].get("authorization", []). Alles behalve precies één waarde geeftNone.Strip de waarde en partitioneer hem bij de eerste spatie. Het eerste deel, in kleine letters, moet
basiczijn. Strip de rest, zodat extra spaties na het schema geen kwaad kunnen.Decodeer de rest met
base64.b64decode(..., validate=True), en daarna.decode("utf-8"), binnentry. Les 4 liet zien dat een fout tekenbinascii.Erroropwerpt en dat bytes die geen UTF-8 zijnUnicodeDecodeErroropwerpen; beide zijn soorten, dus één except ValueError:dieNoneteruggeeft, dekt beide, zoals inbody_erroruit hoofdstuk 5.Als er geen dubbele punt in de tekst staat, geef je
Noneterug. Partitioneer anders bij de eerste dubbele punt.
De tak /login vergelijkt daarna: als basic_credentials None teruggeeft, of state["users"].get(user) niet het wachtwoord is, antwoord je met unauthorized(REALM), zonder {"user": user} en de cookie.
Deze server bewaart wachtwoorden als gewone ==, zodat de les over HTTP blijft gaan. Een echte service bewaart van elk wachtwoord alleen een langzame, gezouten hash, en vergelijkt geheimen met hmac.compare_digest, zodat de tijd die een vergelijking kost niets verraadt over hoeveel van een gok klopte. Wachtwoordopslag valt buiten deze cursus.
Zeg wat er misging, niet het geheim
Elke mislukking krijgt dezelfde body, zoals {"error": "credentials are missing or wrong"}. Twee gewoonten zijn belangrijk. Een antwoord herhaalt nooit een inloggegeven, omdat antwoorden in logs en op schermen terechtkomen. En de melding zegt niet of de gebruikersnaam bestaat, omdat “verkeerd wachtwoord” voor de ene naam en “onbekende gebruiker” voor een andere een vreemde vertelt welke namen hij moet proberen.
Welke 401-body is veilig?
Alleen binnen HTTPS
Les 4 liet zien dat base64 niets verbergt. Een Basic-header is het wachtwoord, leesbaar voor iedereen die het verzoek onderweg ziet. Basic-inloggegevens zijn alleen aanvaardbaar binnen HTTPS, waar
De helft van de client
In wire.py geeft basic_header(user, password) de headerwaarde terug: "Basic " gevolgd door base64 van f"{user}:{password}" gecodeerd als UTF-8, met .decode("ascii") weer omgezet in tekst. Daarna stuurt login_and_me hem mee: als user niet None is, geef je headers={"Authorization": basic_header(user, password)} mee aan de urllib.request.Request van de login. Stuur hem alleen bij de login. Daarna zegt de cookie wie de client is.
Hoofdstuk 6 liet één vraag over inloggegevens en content-length en content-type, dus een Basic-header Authorization gaat mee naar welke host de doorverwijzing ook noemt. De cookiejar is voorzichtiger: die bepaalt de regel Cookie opnieuw voor elk verzoek, en stuurt alleen de cookies die bij de nieuwe host en het nieuwe pad passen. Deze server verwijst alleen naar zichzelf door, dus hier lekt niets, maar een client die doorverwijzingen naar elke host volgt terwijl hij een Basic-header stuurt, geeft het wachtwoord aan wie de doorverwijzing ook noemt.
Schrijf in reading_server.py unauthorized en basic_credentials en herschrijf de tak /login zoals hierboven. Schrijf in wire.py basic_header en stuur hem mee vanuit login_and_me. Voer daarna het scenario opnieuw uit:
python pl_http.py basic
De eerste twee logins antwoorden nu met HTTP/1.0 401 Unauthorized, en onder elk drukt het hulpprogramma af:
www-authenticate: Basic realm="reading list", charset="UTF-8"
Het juiste wachtwoord krijgt 200 en een regel set-cookie, en je client leest me: {'user': 'ana'}. Het scenario session uit les 2, dat geen inloggegevens stuurt, krijgt nu ook een 401 voor zijn login.
Klik daarna op Check my work. Logins betekenen nu iets, maar POST, PUT en DELETE op de boeken accepteren nog steeds iedereen. De volgende les beschermt ze.
Opdracht
Controleer bij het inloggen Basic-inloggegevens in reading_server.py, en stuur ze mee vanuit wire.py.
In reading_server.py:
unauthorized(challenge)geeft een 401--fout terug waarvan de header www-authenticatechallengeis.basic_credentials(request)geeft{"user": ..., "password": ...}terug uit een headerauthorization, ofNone. GeefNoneterug tenzij er precies éénauthorization-is waarvan het schema basicis, met willekeurige hoofdletters. Strip de tekst na het schema, decodeer hem metbase64.b64decode(..., validate=True)en daarna als, en geef Noneterug als een van beide stappenopwerpt of de tekst geen dubbele punt bevat. Splits alleen bij de eerste dubbele punt: het wachtwoord kan er meer bevatten. POST /login: alsbasic_credentialsNoneteruggeeft of het wachtwoord niet het wachtwoord van die gebruiker instate["users"]is, antwoord je metunauthorized(REALM)zonderset-cookieen zonder nieuwe sessie. Start anders een sessie voor die gebruiker en antwoord met 200 met{"user": user}en deuit les 2. Geen enkel antwoord herhaalt een wachtwoord of een
-inloggegeven. Formuleer de fout zoals je wilt.
In wire.py:
basic_header(user, password)geeft"Basic "terug, gevolgd door de base64 vanf"{user}:{password}"gecodeerd als UTF-8, als tekst.login_and_mestuurtAuthorization: basic_header(user, password)mee met het loginverzoek alsusernietNoneis, en anders geen headerAuthorization. Het verzoek/medraagt er geen.
Voer python pl_http.py basic uit in de www-authenticate, en daarna een sessie voor ana.
Klik daarna op Check my work. Die roept basic_credentials aan met headers die hij bouwt voor gebruikers en wachtwoorden die je nog niet hebt gezien, ook niet-ASCII, een wachtwoord met dubbele punten, en misvormde headers, en roept handle aan met een eigen state: juiste en verkeerde wachtwoorden, onbekende gebruikers, en ontbrekende of misvormde headers. Hij controleert elke status, het schema en de realm van www-authenticate, de cookie, de sessies, en dat geen enkel antwoord een inloggegeven herhaalt. Hij controleert basic_header tegen een vooraf uitgerekende waarde, en voert login_and_me uit met vervangers voor urllib, zoals in les 3, met juiste en verkeerde inloggegevens. Niets uit je terminal wordt beoordeeld.