System design

The Anatomy of a JWT

JSON Web Tokens for JWTs for short are omnipresent in 2023. Whatever the authentication method, it’s probable that a JWT is involved. This makes it a good idea to understand what a JWT is underneath the hood.

JWT exists as an “open spec”. This takes form of the IETF or the “Internet Engineering Task Force” and it’s members who can be both individuals and organizations who work together to define new “standards” or “spec” that are “open” and then implemented by anyone who’d wish to implement them, usually in the form of open source code hosted on Github.

The spec can be found here: https://datatracker.ietf.org/doc/html/rfc7519 and exists under the IETF reference RFC7519.

Two Parts Base64

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Go ahead. Scroll horizontally. Do you notice that there are three periods followed by a string of characters? Take the first part: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9” and go into your Dev Tool console and try Base64 decoding it.

const json = atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
const obj = JSON.parse(json);
console.log(obj)
// {alg:"HS256", typ:"JWT"}

Do you see what I see? It’s an object with two fields. One field is named “alg” which is short for algorithm and the other is “typ” which is short for “type”. This part is called the header.

We’ll go more into detail later on what these fields mean.

Next, if we decode the second part we get more fields.

const json = atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ");
const obj = JSON.parse(json);
console.log(obj)
// {iat: 1516239022, name: "John Doe", sub: "1234567890"}

Please note ‘iat’, ‘name’ and ‘sub’. Let’s call this part the payload.

Next for the part after the third period, if we try to decode it we get an error.

const json = atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ");
const obj = JSON.parse(json);
console.log(obj)
// {iat: 1516239022, name: "John Doe", sub: "1234567890"}

“VM185:1 Uncaught DOMException: Failed to execute ‘atob’ on ‘Window’: The string to be decoded is not correctly encoded.”

Do you notice something? The first two parts of the token start with the 3 letters eyJ. The last part starts with the letters Sfl .

The reason being is because if we Base64 decode the letters eyJin Chrome they decode to {" which is the start of a JSON object while Sfl decodes to Iù which is nothing useful to us.

Why? Well, that’s because the last part isn’t actually Base64 encoded. It’s what we call the signature.

Base64? What is Base64?

Most modern cultures count in what we call “Base 10”. Why Base 10? Well because our numerical system is made up of 10 base parts. For example:

|{0,1,2,3,4,5,6,7,8,9}| = 10

You may have also learned in Computer Science Fundamentals what Binary is a Base 2 system. Why? Because |{0,1}| = 2 . And we know that eight 0s and 1s make up a byte which in the ASCII encoding system corresponds to one character.

In the Base64 system there are 64 characters, plus a “padding” character that make up the system for encoding bytes. In Base64 the characters are case sensitive and the “counting system” is made up of capital A-Z, lowercase a-z, 0–9 and two characters + and / along with the “padding character” that doesn’t count as one of the “64 base parts” which is = .

If we were using an end-to-end JavaScript solution that didn’t normalize everything to UTF-8 for the character encoding, our JWT would basically be a UTF-16 string represented as a Base64’d value.

The Header & Payload

As we saw above, the header has two fields in the form of JSON. The first one is the alg or algorithm field. In this case the token is encoded with HS256 . HS256 is actually an amalgam of the methods used to sign the token with the parts being HMAC and SHA256 . In essence HS256 encrypts a value via a secret and then hashes the output. Remember encryption means it could be decrypted and hashing means it cannot as it’s a “one-way cryptographic function”. So the idea here is to encrypt the value and then arbitrarily “scramble” the encrypted output again so it cannot be decrypted.

A full list of encryption algorithms supported by the JWT spec can be found here: https://datatracker.ietf.org/doc/html/rfc7518#section-3.

The payload on the other hand is the part we are usually concerned about as engineers. The payload consists of a list of claims which are literally key/value pairs that we are claiming as apart of our authentication process.

There are only 7 claims officially registered as a part of the JWT spec with the IETF. Implementing these registered claims is optional and a JWT can include any data that can be JSON encoded.

However, the 7 registered claims are:

  • iss — short for issuer; this is usually the domain or hostname of the service that issued the token
  • sub — short for subject which in most cases means user and comes in the form of a user id
  • aud — short for audience or who the token is intended for which usually comes in the form of a service’s name
  • exp — short for expiration; most open source libraries verify that the current date isn’t past the expiration date as a part of the verify step; this comes in the form of a unix time stamp also
  • nbf — short for not before; this unix time stamp means that the token cannot be used before the time listed in this field; this is also implemented in most open source libraries as an automatically verified field
  • iat — short for issued at which comes in the form of a UNIX timestamp
  • jti — short for JWT ID; it’s used to assign a unique ID for token revocation functionality; by default because the JWT is a stateless spec token revocation is functionality implemented by the developers implementing the JWT usually via caching until the time of expiration;

While these are the official fields, you don’t need to implement them and you can implement anything you’d like as apart of the JWT’s claims. One of the commonly implemented “custom claims” is roles. Since JWT already has a field for a user’s ID, we could also list the user’s role in the token.

All Parts Cryptography

The JSON Web Token at its core is designed for authentication. You might ask, “How is Base64ing something secure?”. Well, the answer is it’s not. As you’ve noticed, anyone can decode Base64 values without a key and any experienced engineer would be able to look at it and figure out it’s likely Base64 encoded. JWT being a stateless protocol is basically a client sending a server user data saying, “trust me I’m this user I’m sending you the data for”. Scary.

So what is a JWT designed to protect against anyway? Well the answer is tampering. Let’s say someone got a hold of one of our tokens . Without a signature component that’s verified, an attacker could look at the token and send his own token giving him unlimited access to our backend. This isn’t good.

So how does a JWT actually protect itself if anyone can Base64 decode the token and say, “I’m Kyle, authenticate me as Kyle” without the server really double checking any of this information. Well the answer lies within the signature through the mathematical magic of cryptography and the HS256 algorithm we chose for our JWT example.

Enter HMAC256.

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),
  "<YOUR SECRET>"
)

As you can see, to generate the signature, we encode our header with our declared algorithm and the type field declaring that this is a JWT, the period delimiter between JWT segments and the a Base64’d concatenated into a singular string with your secret as the seed to encrypt off of and later hash.

Since you keep your secret a secret stored on your server, and nobody but you has access to it, we’re free to leave these fields for the world to see. When we verify the JWT on our backend with an open source JWT library, the verify function runs through the following steps:

  • Encrypt and hash header + payload with our secret and
  • Verify that the issuer matches the issuer we tell it to verify against
  • Verify that the token isn’t expired
  • Verify that the token isn’t used before it’s “not before” date
  • Verify anything else we ask it to including any custom claims

Since our library hashes with our secret that should be securely injected to our server and no human should know what that secret is; we can test that the token was issued by us and remains untampered with because if someone changes the field or signs a token with a different secret pretending to be us the hash that our server computes on the verification step will differ from the hash from the malicious token we’re given and access won’t be granted.

What disclaimers have I left out until this point?

For starters, while a JWT has a good level of security being transmitted in a away that the payload is easily decodable it’s unwise to do so. There exists a spec to encrypt JWTs and you should do so always. We don’t know what attacks lurk around the corner and encryption is one more safeguard against future exploits.

Also, there are plenty of methodologies to sign a token. HS256 has a couple known vulnerabilities that are easily foilable if you’re verifying the JWT’s signature properly but you shouldn’t count on that alone. Not many engineers know the “anatomy of a JWT” and hence this article was birthed into existence. If you don’t understand the spec at a deep level (and really even if you think you do), you should probably choose another common sighing method like RS256 which uses a public/private keypair like that inside an x.509 certificate used with HTTPS or a PEM used to SSH into an EC2 instance instead. Auth0 has a great post about this https://auth0.com/blog/rs256-vs-hs256-whats-the-difference/.

Finally, the JWT spec has many more parts to this. JWKS for example is apart of the overarching spec and is something worth researching and understanding. JWKS works with RSA based cyphers (e.g. RS256 ) to provide a convenient way to host your public keys for JWT verification among other services you might have.