Contents

JWT Explained

Intro

Hey future me, I knew you’d come crawling back here to re-understand how JWT works. Allow me to refresh your memory..

Premise

Suppose we’re building a web application called Hot Donuts Near Me. Picture a server with our code and our database. And then there’s a client who wants to interact with our server over HTTP. In this example, we’ll pretend the client is some guy named Bob who’s accessing our web app from Chrome. The picture in your head should be something like this.

Some weeks ago, Bob registered for our application. He signed up with the username bobno1 and password hunter1. When he submitted his password, we encrypted it before storing it into our database table of users. So, somewhere on the server we have a table of users like

username hashed_password
bobno1 hVIg6D0oiE
jamiegrl 5gHi3pfusq
freddyboy 33b2yzPBtE
yolo42 J9fmJLUkhF

Fast-forward to today. Bob goes to our web app, hungry for hot donuts. He tries to access the /find-donuts/ API, but this is a restricted endpoint for registered users only. Of course, bob is a registered user - he just isn’t signed in.

So, he signs in. He fills out a form with his username and password and POSTs it to the server. The server then implements some logic like

inputted_username = "bobno1"
inputted_password = "hunter1"

inputted_password_hashed = hash(inputted_password)
database_password = get_password_for_user(inputted_username)

if database_password == inputted_password_hashed:
   // yep, it's really Bob
else:
  // it's an imposter! ..or maybe Bob mistyped his password

Assuming Bob enters the right username and password, the server can easily confirm that “yes, the person making the request really is Bob”. This process is known as authentication.

So now what? Bob wants to access some protected resources like that /find-donuts/ endpoint he tried earlier. But we shouldn’t need him to re-enter his username and password every single time he makes a request. What can we give to Bob so that he can easily and securely access the protected resources of our app?

Server Side Sessions

Our goal is to come up with an authorization mechanism. That is, we want an easy way for Bob to access our protected resources after he authenticates. (Many people confuse these terms. Authentication is the process of verifying who someone is while authorization is the process of verifying if the person has access to do something.)

One authorization mechanism we can set up involves server side sessions. The process goes like this..

  1. Bob logs in
  2. Us (the server), verifies that the person logging in is indeed bob. (Because, who else would know that username and password??)
  3. We generate a big random unique token like SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  4. In our database, we add this token to a sessions table which looks like this
username session_expires_at token
bobno1 2021-12-30_10:30:05 SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
yolo42 2021-12-25_09:45:52 4fwpMeJf36POk6yJV_adQssw5cSflKxwRJSMeKKF2QT
jamiegrl 2021-12-22_22:03:41 Ok6yJV_adQssw5cSflKxwRJSMeKKF2QT4fwpMeJf36P
  1. We send this token back to Bob in the response of his login request. (We probably give it to him as a cookie.)
  2. Bob tries to access a protect resource like /find-donuts/, but when he makes this request, he also provides the token we gave him.
  3. We (the server) see that someone has made a request to /find-donuts/ and provided a token. We look up the token in our sessions table, see that it exists and that it hasn’t expired, and then we send back the location of hot donuts.

If Bob has to send his access token with his requests, how is this any easier for him than just submitting his username and password with every request?
The token we gave Bob will live in a cookie on his browser. Cookies are automatically sent to the server with every request, so Bob doesn’t have to do any extra work if the token lives in a cookie. Furthermore, Bob wouldn’t want to store his username and password in a cookie because there’s a chance his cookies could get stolen. And, losing an access token is less dangerous than losing a username and password because access tokens are typically set to expire in a few hours or days.

Cool! But there’s a couple drawbacks to this approach..

  1. The server has to do a decent bit of work. It has to create access tokens and insert them into a database. And with every request to a protected resource, it has to search the database to see if the passed-in access token exists and what its corresponding expiration time is.
  2. Suppose we store these access tokens directly on the server. If we add a second server to handle more user requests, then the following problem could occur. Bob logs into server 1, so server 1 stores his access token. Bob then hits server 2, attempting to access /find-donuts/. Server 2 doesn’t know that Bob is logged in because only server 1 was storing his access token. So, server 2 rejects Bob’s request and prompts him to log in.

JWT solves these issues.

JWT

Suppose that after Bob authenticates, we do the following.

  1. We define a header as
{
  "alg": "HS256",
  "typ": "JWT"
}
  1. We define a payload as
{
  "username": "bobno1",
  "name": "Bob",
  "expires": 2021-12-30_10:30:05
}

Then suppose we send this information back to Bob in a cookie. Now when Bob makes a subsequent request to /find-donuts/, we’ll receive this cookie and we can just read it to know that the person making the request is Bob and he’s currently logged in because the session hasn’t expired. No need to look up anything in a database. If we have multiple servers this design still works. This is great!

..except, if Bob figures this out, he can do something very shady. He can modify the username portion of the payload to be jamiegrl, his neighbor’s username who he knows pays for premium access to endpoints that Bob doesn’t have. If Bob does this, how are we (the server) to know that Bob tampered with the original information we sent to him?

This is the interesting, cryptographic part of JWT. Suppose we generate a secret key like eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFt which we store on our server(s). And before we send the header and payload to Bob, we create a signature to send him as well. The way we create the signature is,

  1. We concatenate the header, the payload, and our secret key into a single string.
  2. We hash that string using the algorithm stated in the header, HS256.

This’ll spit back some unique signature like WF0IjoxNTE2MjM5MDIyfQ. So, in addition to sending Bob the header and payload, we also send him this signature. When Bob subsequently makes a request to /find-donuts-premium/ (sending us back the header, payload, and signature) we implement logic like

concatenate the header, payload, and our secret key
hash that string using the algorithm stated in the header
check if the hashed value equals the signature that Bob sent us
confirm these values match

If Bob tampered with the payload and changed "username": "bobno1" to "username": "jamiegrl", then the hashed value that we calculate won’t match the signature that Bob sent us.

This is essentially the design of JWT. It’s a method to send and store data on a client such that, if the client tampers with the data, we’ll know about it. It is not a method to store data on a client such that, the client can’t view the data. In other words JWT makes no attempt to encrypt the header or payload (although, I believe it’s possible to set that up). Rather, it’s a method for verifying the data hasn’t changed.

Details

In practice, there are a few extra steps I didn’t mention above. The first is to base64 url encode the header and payload. The purpose of this is to turn JSON data like this

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Into a nice compact string like this

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ

which is more suitable for sending in the header of HTTP requests. While that big nasty string looks cryptic, it’s not. You can easily decode it. (Try it for yourself if you don’t believe me.)

Secondly, JWT specifies that we should concatenate the base64 url encoded header with the base64 url encoded payload with the signature, separated by periods. So at the end of the day, the token we send and receive looks something like this

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Thirdly, it seems the best way to store a JWT is via the Double Submit Cookies method. Perhaps an article for another day..

Oh by the way, jwt.io is a great place to wrap your head around JWT.