0%

Hoofdstuk 5

Ondertekende tokens en geheime sleutels

Ondertekend is niet geheim

Je houden nu stand. Een login krijgt een nieuwe id, een inactieve sessie eindigt, en de draagt zijn vlaggen. De schrijfroutes zijn een ander verhaal. Die worden nog steeds geopend door twee die al sinds with Python in sample_state() staan:

"tokens": {
    "tok-ana-writes-7Qx2": {"user": "ana", "can_write": True},
    "tok-ben-reads-K9pz": {"user": "ben", "can_write": False},
},

Er zijn hier twee dingen mis, en samen vormen ze bevinding F6, een rij van SECURITY.md die nog leeg is. De strings staan in de , dus iedereen die reading_server.py kan lezen, heeft beide tokens. En ze werken voor altijd: niets erin, en niets eromheen, loopt ooit af. Een derde nadeel is minder dringend. Om een token te herkennen, moet de server een bijhouden van elk token dat hij ooit heeft uitgegeven.

Een token dat de server niet opslaat

Stel dat het token zelf zei voor wie het is, en een bewijs meedroeg dat de server het schreef. Dan zou de server geen lijst nodig hebben. Hij zou het token lezen, het bewijs controleren, en geloven wat het token zegt.

Hoofdstuk 2 bouwde dat bewijs. Een tag die gemaakt is met hmac en een sleutel die alleen de server heeft, kan door niemand anders gemaakt worden. Een token kan dus uit twee delen bestaan met een punt ertussen: een payload, een klein - dat als -tekst geschreven is zodat het in een header past, en de tag van die tekst. Een die zo haar eigen tag meedraagt, is een ondertekende waarde (signed value).

{"user": "ben", "can_write": false}                  the payload, JSON
                 |
                 |  base64
                 v
eyJ1c2VyIjogImJlbiIsICJjYW5fd3JpdGUiOiBmYWxzZX0=  .  9d64c048...5f5a2fdc
[------------- the payload text --------------]      [---- the tag ----]
  the tag is computed over exactly this text         64 hex characters;
  with the server's key                              only the key holder
                                                     can compute them

Lees het van boven naar beneden. De JSON wordt base64-tekst. De tag dekt die tekst en verder niets. Het token is de tekst, een punt, en de tag. Bouw er een:

Try it

De eerste regel is het token: eyJ1c2VyIjogImJlbiIsICJjYW5fd3JpdGUiOiBmYWxzZX0=, een punt, en een tag die begint met 9d64c048. De tweede regel is 32 64: een sleutel van 32 bytes en een tag van 64 hextekens. base64.urlsafe_b64encode is de base64 die je kent met twee tekens verwisseld, - voor + en _ voor /, zodat de tekst ook veilig is in een .

Twee details in dit blok zijn eenvoudiger dan in het echt. De sleutel is leesbare tekst, zodat het blok kort blijft; een echte sleutel is 32 willekeurige bytes uit secrets.token_bytes, zoals in hoofdstuk 2. En de payloadtekst eindigt op =, en dat is base64-opvulling. De volgende les gebruikt een hulpfunctie die die weglaat, omdat een bearer-token = alleen helemaal aan het eind mag hebben.

Iedereen kan het lezen

Ben heeft dat token. Dit kan hij ermee doen, zonder enige sleutel:

Try it

Het drukt b'{"user": "ben", "can_write": false}' af. Er was geen sleutel nodig, omdat base64 een codering is, en een codering verbergt niets. Dat zag je in HTTP with Python en opnieuw in hoofdstuk 2. De tag bewijst wie de payload schreef. Hij doet niets om de payload privé te houden.

Een ondertekend token is dus geen geheime container. De houder kan het lezen, en iedereen die het onderweg ziet ook. Een wachtwoord, een sleutel, of iets anders wat een vreemde niet mag lezen, gaat nooit in een payload.

Wat drukt dit af?

import base64

print(base64.urlsafe_b64decode("eyJ1c2VyIjogImJlbiJ9"))

Niemand kan het veranderen

Ben zou willen dat can_write waar is. Hij schrijft een nieuwe payload, codeert die, en houdt de oude tag, omdat hij geen nieuwe kan maken. De server doet wat hij altijd doet: hij berekent de tag van de payloadtekst die hij ontving en vergelijkt die met de tag die meekwam.

Try it

Het drukt False af voor het veranderde token en True voor het onaangeroerde. De tag hoort bij precies de bytes die ondertekend zijn. Verander één teken van de payload en de oude tag past niet meer, en zonder de sleutel kan Ben de tag die wel zou passen niet berekenen.

Ben verandert false in true in zijn payload en houdt de oude tag. Waarom weigert de server het token?

Wat het kost

De server houdt geen lijst bij, dus hij heeft niets om door te strepen. Als een kopie van een token lekt, werkt de kopie precies zo lang als het origineel, en uitloggen beëindigt geen van beide, omdat een token nooit een sessie noemt. Niets aan een token kan veranderd worden nadat het is uitgegeven.

Twee dingen kunnen het nog beëindigen. Het eerste is tijd: een token dat zijn eigen verloop meedraagt, werkt niet meer zodra dat moment voorbij is, dus dit hoofdstuk houdt tokens kortlevend. De volgende les voegt het verloop toe. Het tweede is de sleutel: als de server zijn sleutel vervangt, wordt elk token dat de oude sleutel ondertekende in één keer niet meer geverifieerd. Les 5 doet dat met opzet. Een echte dienst kan ook een lijst bijhouden van tokens die hij heeft ingetrokken, en dan komt een deel van de opslag terug die een ondertekend token vermeed.

Het token van ana wordt door iemand anders gekopieerd. De server slaat geen tokens op. Wat maakt een eind aan het gebruik van de kopie?

Je komt dit tegen als JWT

Buiten deze cursus kom je dit idee tegen onder de naam JSON Web Token, of JWT. Een JWT is een ondertekend token in een vast formaat: drie delen met een punt ertussen. Een van de ondertekeningsmethoden, HS256 genaamd, is de die je hier gebruikt. Deze cursus bouwt het idee met de hand en gebruikt geen tokenbibliotheek, en de tokens die je schrijft, zijn geen JWT’s. Als je er een tegenkomt, weet je waar de delen voor dienen, en dat de payload leesbaar is.

Welke hiervan mag in de payload van een token?

Het token in deze blokken heeft dezelfde fout als de vaste strings: het verloopt nooit. De volgende les geeft het een einde, binnen het deel dat de tag dekt.