0%

Chapter 5

Signed Tokens and Secret Keys

Signed Is Not Secret

Your hold now. A login gets a new id, an idle session ends, and the carries its flags. The write routes are another matter. They are still opened by two that have sat in sample_state() since with Python:

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

Two things are wrong here, and together they are finding F6, a row of SECURITY.md that is still empty. The strings are in the source, so everyone who can read reading_server.py holds both tokens. And they work forever: nothing in them, and nothing about them, ever ends. A third cost is less urgent. To recognize a token, the server has to keep a of every token it ever handed out.

A token the server does not store

Suppose the token itself said who it is for, and carried proof that the server wrote it. Then the server would need no list. It would read the token, check the proof, and believe what the token says.

Chapter 2 built that proof. A tag made with hmac and a key that only the server holds can be made by nobody else. So a token can be two parts with a dot between them: a payload, which is a small written as text so that it fits in a header, and the tag of that text. A that carries its own tag like this is a 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

Read it from the top. The JSON becomes base64 text. The tag covers that text and nothing else. The token is the text, a dot, and the tag. Build one:

Try it

The first line is the token: eyJ1c2VyIjogImJlbiIsICJjYW5fd3JpdGUiOiBmYWxzZX0=, a dot, and a tag that starts with 9d64c048. The second line is 32 64: a key of 32 bytes and a tag of 64 hex characters. base64.urlsafe_b64encode is the base64 you know with two characters swapped, - for + and _ for /, so the text is also safe in a .

Two details in this block are simpler than the real thing. The key is readable text so that the block stays short; a real key is 32 random bytes from secrets.token_bytes, as in Chapter 2. And the payload text ends with =, which is base64 padding. The next lesson uses a helper that drops it, because a bearer token may have = only at its very end.

Anyone can read it

Ben holds that token. Here is what he can do with it and no key at all:

Try it

It prints b'{"user": "ben", "can_write": false}'. No key was needed, because base64 is an encoding, and an encoding hides nothing. You saw that in HTTP with Python and again in Chapter 2. The tag proves who wrote the payload. It does nothing to keep the payload private.

So a signed token is not a secret container. The holder can read it, and so can anyone who sees it on its way. A password, a key, or anything else a stranger should not read never goes into a payload.

What does this print?

import base64

print(base64.urlsafe_b64decode("eyJ1c2VyIjogImJlbiJ9"))

Nobody can change it

Ben would like can_write to be true. He writes a new payload, encodes it, and keeps the old tag, since he cannot make a new one. The server does what it always does: it computes the tag of the payload text it received and compares that with the tag that came along.

Try it

It prints False for the changed token and True for the untouched one. The tag belongs to the exact bytes that were signed. Change one character of the payload and the old tag no longer fits, and without the key Ben cannot compute the one that would.

Ben changes false to true in his payload and keeps the old tag. Why does the server refuse the token?

What it costs

The server keeps no list, so it has nothing to cross out. If a copy of a token leaks, the copy works exactly as long as the original, and logging out does not end either of them, because a token never mentions a session. Nothing about one token can be changed after it is issued.

Two things can still end it. The first is time: a token that carries its own expiry stops working when that moment passes, so this chapter keeps tokens short-lived. The next lesson adds the expiry. The second is the key: when the server replaces its key, every token the old key signed stops verifying at once. Lesson 5 does that on purpose. A real service may also keep a list of tokens it has withdrawn, which brings back some of the storage a signed token avoided.

Ana's token is copied by someone else. The server stores no tokens. What ends the copy's use?

You will meet this as JWT

Outside this course you will meet this idea under the name JSON Web Token, or JWT. A JWT is a signed token in a fixed format: three parts with a dot between them. One of its signing , called HS256, is the you are using here. This course builds the idea by hand and uses no token library, and the tokens you write are not JWTs. When you meet one, you will know what its parts are for, and that its payload is readable.

Which of these may go into a token's payload?

The token in these blocks has the same flaw as the fixed strings: it never expires. The next lesson gives it an end, inside the part the tag covers.