How to code a JWT debugger in JavaScript

If you’re a developer then you’ve probably pasted a JWT into jwt.io to have a peek into the payload or validate a secret. As part of my attempt to create a dev utility tool — mentioned in Coding a JSON formatted — the next logical step after creating a base64 encoder/decode is to create a JWT debugger. That’s the plan of this blog.

If you’re interested in what my JWT debugger currently looks like (It’s still a work in progress):

What is a jwt

To create a JWT debugger it’ll probably help if we first understand what the JWT comprises of. A JWT is a way for us to transmit information securely across the interwebs. The data within the payload can be trusted as JWTs are signed using a secret using the HMAC algorithm or a public/private key (RSA or ECDSA), in this blog I’m going to go over exclusively HMAC for simplicity.

The structure of a JWT is split into 3 parts, separated by the “.”:

header.payload.signature

Header

The header is base64 encoded, once decoded it’ll look something like:

{
  "alg": "HS256",
  "typ": "JWT"
}

In node you would decode it like the following:

Buffer.from(header, "base64");

The header usually just contains the two parts shown in the above JSON, the algorithm type and the type of token.

Payload

The payload is once again base64 encoded and looks like the following:

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

There are 3 types of claims: Registered, public and private.

  • Registered: a set of predefined, recommended claims. Above you can see there are two registered claims “sub” and “iat” meaning subject and issued at respectively.
  • Public custom claims that are required to be collision resistant, you’d usually see these properties prefixed by an identifier.
  • Private are not required to be collision resistant.

More information on JWT payloads can be found here.

Signature

The signature of a JWT follows this format:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

So it takes the encoded header and the encoded payload concatenates them with “.” and then signs with our secret using the hmac algorithm. This signature allows us to prove that the JWT wasn’t tampered with along the way, we can be certain it’s coming from the expected client; if the claims in the body were altered then the signature would be different.

Coding a JWT debugger

Well, now we know what a JWT is, let’s start writing some code. My application is written in vue3 using Electron, so I won’t go into details about rendering and two-way binding, I’ll extract the meat of what’s being done — it’s pretty simple.

Let’s start with an example JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.YIsLjk6gmcAPXnZNe8M57ibu0IzpTykXcb6CT9P0dVY

With the secret: codeheir.com

The first thing we need to do is split it up into its respective sections, and decode the header and the payload. Contrary to the example I showed above, I’m going to be using the base64url node package, which just makes the encoded strings safe to be used in a URL. You can take a look at what it’s doing with the strings here.

An example of what it might look like is as follows:

function decodedHeader() {
  const header = input.value.split(".")[0];
  return base64url.decode(header);
}

And the payload:

function decodedPayload() {
  const payload = input.value.split(".")[1];
  return base64url.decode(payload);
}

Displaying those gives the following:

Next, the secret. Let’s say we’ve just pasted in our JWT and we want to verify the JWT hasn’t been tampered with. We need to verify the signature:

function verifySignature() {
  const toast = useToast();
  let headerAndPayload =
    input.value.split(".")[0] + "." + input.value.split(".")[1];

  let secret = signingKey.value;
  const signatureBase64 = crypto
    .createHmac(selectedOption.value.alg, secret)
    .update(headerAndPayload)
    .digest("base64");
  const signatureBase64Url = base64url.fromBase64(signatureBase64);
  if (signatureBase64Url === input.value.split(".")[2]) {
    toast.success("Valid signature");
  } else {
    toast.error("Invalid signature");
  }
}

We’re concatenating the header and the payload with a dot as explained above. We’re then signing the secret codeheir.com using the node crypto library and the sha256 algorithm.

Next, let’s say we want to go the other way — creating the encoded header, payload and signature.

Creating the header is just a case of encoding with the selected algorithm type:

function createHeader() {
  return base64url.encode(`{"typ":"JWT","alg":"${selectedOption.value}"}`) // so value would be hs256 in our example
}

Payload is exactly the same, just encode it as you’d expect.

function createPayload() {
  return base64url.encode(payload) 
}

And creating the signature is similar to when we validated the key:

function createSignature(signingKey) {
  if (isSecretEncoded.value) {
    signingKey = base64url.decode(signingKey);
  }

  const headerAndPayload = createHeader() + '.' + createPayload();
  let signature = crypto
    .createHmac(selectedOption.value.alg, signingKey)
    .update(headerAndPayload)
    .digest("base64");
     signature = base64url.fromBase64(signature);
  return signature;
}

And that’s pretty much all there is to it, if you liked this blog then please sign up for my newsletter and join an awesome community!

One comment

Leave a Reply