JWTs explained with code examples
Tags: security, javascript, beginners, tutorial
Published: 2021-10-30 21:38:07

The Wikipedia description of a JSON Web eToken (JWT) is:

> JSON Web Token is a proposed Internet standard for creating data with optional signature and/or optional encryption 
> whose payload holds JSON that asserts some number of claims. 

However, this definition says a lot without really saying a lot. When I'm trying to understand a concept, I like to play around with relevant libraries. We'll try this out with JWTs using the popular javascript library [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken).

## Creating a JWT 

The first thing the [docs](https://www.npmjs.com/package/jsonwebtoken) mention is that the `sign` function returns a 
JWT, and the only required arguments are some JSON and a string called `secret`.

```js
const jwtLibrary = require('jsonwebtoken');

// The only arguments we need are a secret value and some JSON
const json = {"key": "value", "key2": "value2"}
const secret = "shhhhh";

// Ignore the options for now, we'll check them later
const jwt = jwtLibrary.sign(json, secret);

console.log("JWT:", jwt);
// JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImtleTIiOiJ2YWx1ZTIiLCJpYXQiOjE2MzQxNzgxMTB9.vnXM0oxw05QH1Vs6RsvYp6LaEqFFqZ-NExQMXBgP7Mk
```

This is our first look at a what a JWT looks like.

## Using a JWT

What can we do with this JWT? The library has two other methods, `verify` and `decode`. It lists `verify` first so we'll try that first:

```js
// From previous example
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImtleTIiOiJ2YWx1ZTIiLCJpYXQiOjE2MzQxNzgxMTB9.vnXM0oxw05QH1Vs6RsvYp6LaEqFFqZ-NExQMXBgP7Mk";
const secret = "shhhhh";

// Ignore the options for now, we'll check them later
const verifyResult = jwtLibrary.verify(jwt, secret);

console.log("verifyResult:", verifyResult);
// verifyResult: { key: 'value', key2: 'value2', iat: 1634178110 }
```

It looks like we got back the JSON that we specified above plus an extra entry `iat`. The docs say that `iat` is short for `issued at` and is a unix timestamp of when the JWT was created. 

What happens if we used the wrong secret?

```js
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImtleTIiOiJ2YWx1ZTIiLCJpYXQiOjE2MzQxNzgxMTB9.vnXM0oxw05QH1Vs6RsvYp6LaEqFFqZ-NExQMXBgP7Mk";
const incorrectSecret = "thisiswrong";

const verifyResult = jwtLibrary.verify(jwt, incorrectSecret);
// JsonWebTokenError: invalid signature
```

Unsurprisingly, we get an error. So far, we can determine that a JWT somehow encodes the JSON value that we passed in along with other metadata (`iat`). Later on, we can check that a JWT was created with a specific secret and get back that encoded JSON.

What about the `decode` method?

```js
// From previous example
const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSIsImtleTIiOiJ2YWx1ZTIiLCJpYXQiOjE2MzQxNzgxMTB9.vnXM0oxw05QH1Vs6RsvYp6LaEqFFqZ-NExQMXBgP7Mk";

const decodeResult = jwtLibrary.decode(jwt);
console.log("decodeResult:", decodeResult);
// decodeResult: { key: 'value', key2: 'value2', iat: 1634178110 }
```

This is kind of strange. We didn't pass in the secret, but we still got back the original JSON and `iat`. There's a warning on the method in the docs which gives us a hint about what's going on:

> Warning: This will not verify whether the signature is valid. You should not use this for untrusted messages. 
> You most likely want to use jwt.verify instead.

This tells us something important. The JSON within the JWT is not encrypted. If we store anything sensitive in a JWT, anyone could read it, even if they don't have the secret.

## Where might this be useful?

A quick recap on what we've learned:

 - A JWT can be created with JSON and a secret
 - Anyone can get the JSON out of the JWT, even without the secret
 - We can verify that a JWT was created with a specific secret

One common example is authentication. After a user logs in, we can create a JWT containing metadata about the user, like:

```js
const jwtLibrary = require('jsonwebtoken');
const secret = "shhhhh";

function createJwtForUser(userId) {
    return jwtLibrary.sign({"user_id": userId}, secret);
}
```

Users can send us the JWT, and we can securely know who sent it.

```js
function getUserIdForJwt(jwt) {
    try {
        return jwtLibrary.verify(jwt, secret)["user_id"];
    } catch(err) {
        // Can more gracefully handle errors
        return null;
    }
}
```

All we need is our secret, and we are confident in the returned `user_id`. The only way someone could impersonate a user is if they had our secret (so choose something better than `shhhhh`) or if they stole a valid JWT from someone else (so make sure to keep them safe).

Additionally, we don't need to maintain any state or query any external services to validate the userIds. 

## jsonwebtoken Options

The `sign` function takes in a bunch of options that we have skipped. Let's go back and look at some.

```js
const jwtLibrary = require('jsonwebtoken');

const json = {"whatever we want": "anything"}
const secret = "shhhhh";

// Specify expiresIn for 1h
const jwt = jwtLibrary.sign(json, secret, {expiresIn: '1h'});
const verifyResult = jwtLibrary.verify(jwt, secret);

console.log("verifyResult:", verifyResult)
// verifyResult: { 'whatever we want': 'anything', iat: 1634186608, exp: 1634190208 }
```

After adding `expiresIn`, we can see that a new entry was added to the JSON `exp`. 

`exp` is another unix timestamp, and it's 3600 seconds (1 hour) after the issued time. What happens when the time expires? We can either wait an hour or speed things up by specifying a negative `expiresIn`

```js
// ... same as before
const jwt = jwtLibrary.sign(json, secret, {expiresIn: '-1h'});
const verifyResult = jwtLibrary.verify(jwt, secret);
// TokenExpiredError: jwt expired
```

We get an expected error, because the jwt expired an hour ago. 

Why is `expiresIn` useful? We said before that once we create a JWT we can check that it's valid without doing any external lookups. The issue with this is once a JWT is created, it's valid forever (as long as the secret doesn't change). 

`exp` allows us to bound how long the token is valid for, by encoding that information in the JSON itself.

Note that while this library allows us to specify it in a user-friendly way (`1h`), we could also have just added it directly to the JSON:

```js
const json = {
    "whatever we want": "anything",
    "exp": Math.floor(Date.now() / 1000) - (60 * 60), // 1 hour in the past
}
const secret = "shhhhh";

const jwt = jwtLibrary.sign(json, secret)
const verifyResult = jwtLibrary.verify(jwt, secret);
// TokenExpiredError: jwt expired
```

This is actually how most of the options work. They are a nice way to specify entries (also known as claims) that are added to the JSON. The `issuer` option, for example, adds a claim `iss` to the JSON. 

`iss` is used as an id for whoever created the JWT. The party verifying the JWT can check the `iss` to make sure it came from the source they were expecting:

```js
const json = {"user_id": "8383"}
const secret = "shhhhh";

const jwt = jwtLibrary.sign(json, secret, {"issuer": "@propelauth"})

const verifyNoIssuer = jwtLibrary.verify(jwt, secret);
console.log(verifyNoIssuer);
// { user_id: '8383', iat: 1634178110, iss: '@propelauth' }
// ^ this works because the library only checks the issuer if you ask it to

const verifyCorrectIssuer = jwtLibrary.verify(jwt, secret, {"issuer": "@propelauth"});
console.log(verifyCorrectIssuer);
// { user_id: '8383', iat: 1634178110, iss: '@propelauth' }
// ^ this works because the issuer matches

const verifyIncorrectIssuer = jwtLibrary.verify(jwt, secret, {"issuer": "oops"});
console.log(verifyIncorrectIssuer);
// JsonWebTokenError: jwt issuer invalid. expected: oops
// ^ this fails because the issuer doesn't match
```

A complete list of standard fields is available [here](https://en.wikipedia.org/w/index.php?title=JSON_Web_Token#Standard_fields).
Almost every JWT library will support checking these standard fields.

## What are algorithms?

The last thing to explore in this library is the `algorithms` option. There are quite a few [supported algorithms](https://www.npmjs.com/package/jsonwebtoken#algorithms-supported) in the docs.

The algorithms ultimately control the signing and verification functions. There's a lot we can dig into here, but at a high level, there are two types of algorithms: symmetric and asymmetric.

The default algorithm (`HS256`) is symmetric, meaning the same secret is used for signing and verifying. We saw this above when we passed `shhhhh` into both `sign` and `verify` as the secret. This is often used when a service is verifying the JWTs they issue themselves.

Another common algorithm is `RS256` which is asymmetric. In this case, a private key is used to `sign`, but a public key is used to `verify`. This is often used when the issuer and verifier are different. Anyone with the private key can create valid JWTs, so if a service is only verifying JWTs, they only need the public key.

It is good practice to specify the algorithm you are expecting in the `verify` function:

```js
jwtLibrary.verify(jwt, secret);
// ^ don't do this

jwtLibrary.verify(jwt, secret, { algorithms: ['HS256'] });
// ^ do this
```

Why does this matter? Well, unfortunately `none` is a valid algorithm. There have been security flaws in applications when a person creates a fake token but uses the `none` algorithm (which expects there to be no signature).

Some libraries won't allow `none` at all since it kind of defeats the purpose of `verify`.

## Summing up

You should now have a pretty good grasp on JWTs based on this implementation. If you want to test your understanding,
try reading the docs for a different popular JWT library ([PyJWT](https://pyjwt.readthedocs.io/en/stable/) is a good
choice for python folks) and see if the interfaces make sense.