DEV Community

Cover image for Building a verify JWT function in TypeScript
Klee Thomas
Klee Thomas

Posted on • Edited on

Building a verify JWT function in TypeScript

Verifying an RS256 signed JWT

JSON Web Tokens (JWT) are used as a way to verify the identity of the caller of an API.
The best way to verify a JWT is to use a verification library.
I wanted to have a look at some of what those libraries are doing under the hood by putting together a function that will return if a given token is valid. In this blog I'll go through what I have done to get a validation function working.

To simplify things assume:

  • That the signing algorithm is RS256 all others are considered invalid.
  • That the public keys are available on a JWKS url provided to the function eg https://klee-test.au.auth0.com/.well-known/jwks.json
  • I only want to know if the token was signed by a key available at the above url. I wont be checking if the token has expired, if the scopes or other claims are valid.

Break up the token

A JWT is made up to 3 parts. The first thing to do in validating the token is to break this apart.

  • The header - Meta information about the token. This is a JSON string tha has been base 64 encoded.
  • The Body - Claims that the token is asserting. This is a JSON string tha has been base 64 encoded.
  • The Signature - Used to verify the integrity of the token.

The token string is made up of the three sections concatenated with a . character. The first thing to do is to split the 3 sections.

  const [rawHead, rawBody, signature] = jwt.split(".");
Enter fullscreen mode Exit fullscreen mode

Check the Algorithm

To check the token has been signed with the expected algorithm the head needs to be readable.

Create a way to decode the base 64 encode string into TypeScript objects.

function decodeAndJsonParse<T>(base64: string): T {
  // Decode the JSON string from Base 64
  const json = new Buffer(base64, "base64").toString("ascii");
  // Return the parsed object
  return JSON.parse(json);
}
Enter fullscreen mode Exit fullscreen mode

Read the head section of the JWT into a known type { alg: string; kid: string }.

  const parsedHead = decodeAndJsonParse<{ alg: string; kid: string }>(rawHead);
Enter fullscreen mode Exit fullscreen mode

Check that the alg property is the algorithm that was used to sign the token. This example is only going to support the RS256 signing method. If the algorithm is anything else reject the key.

  if (parsedHead.alg !== "RS256") {
    return false;
  }
Enter fullscreen mode Exit fullscreen mode

Get the key

The next step in validating that the token was signed with a known private key is to fetch the public key. The standard for sharing these are to provide a JSON Web Key Set (JWKS) endpoint. The return value from this endpoint for RS256 keys matches this type definition:

Set up a TypeScript type for the key

type JWKS = {
  keys: JWK[];
};

type JWK = {
  alg: string;
  kty: string;
  use: string;
  n: string;
  e: string;
  kid: string;
  x5t: string;
  x5c: string[];
};

Enter fullscreen mode Exit fullscreen mode

Important properties for this example are:
alg: The Algorithm that was used to sign the key.
n: The public key modulus (Base64urlUInt encoded)
e: The public key exponent. (Base64urlUInt-encoded)
kid: The key identifier.
The spec that outlines the use of all the parameters can be found in RFC7518;

Fetch the key

Use node-fetch to get the JWKS keys from the provided endpoint.

  // Get the key
  const jwksResponse = await fetch(jwksEndpoint);
  // Read the JSON response as a JWKS type
  const jwks: JWKS = await jwksResponse.json() as JWKS;
Enter fullscreen mode Exit fullscreen mode

Check the key

Check that there is a key in the set with a kid that matches the kid from the tokens head.

  // Find the key that matches the token
  const jwk = jwks.keys.find((key) => key.kid === parsedHead.kid);

  // Check that a key was found and that it's the correct algorithm
  if (!jwk || jwk.alg !== "RS256") {
    return false;
  }
Enter fullscreen mode Exit fullscreen mode

Convert the JWK to pem

The JWK is a JSON object that contains the components of the public key. To validate the JWT the components need to be converted into a PEM format key.
To do this pass the n and e parameters of the JWK to NodeRSA with the "components-public" flag. Then export the key into a PEM key string.

  // Make an instance of Node RSA from the JWK public key components
  const key = new NodeRSA(
    {
      n: Buffer.from(jwk.n),
      e: Buffer.from(jwk.e),
    },
    "components-public"
  );
  // Export the key into the desired formats
  const pem = key.exportKey("pkcs8-public-pem");
Enter fullscreen mode Exit fullscreen mode

Use Crypto to verify the token

Node provides a built in Crypto library that can be used to validate the token.

Setup the Crypto verify object

Start by creating an instance of the Verify class from the Crypto package by using the createVerify factory method with the "RSA-SHA256" to specify the algorithm to use.

  // Create a verify object that can be used to verify the token
  const verifyObject = Crypto.createVerify("RSA-SHA256");
Enter fullscreen mode Exit fullscreen mode

Add the token head and body to the verify object.

The verify object can be written to as a stream. Write in the head and the body section of the JWT with a "." in the middle. Effectively taking the JWT without the signature.

  // Write the base64 encoded head the . character and the base64 encoded body to the stream.
  verifyObject.write(rawHead + "." + rawBody);
  // Close the stream
  verifyObject.end();
Enter fullscreen mode Exit fullscreen mode

Normalise the base64 signature

This is the tricky part. The signature is Base64URL encoded but it needs to be Base64 encoded. Node is able to take care of this by passing it in and out of a buffer. If this isn't done the signature will always be invalid.

  // Important! Normalise the base64 encoding by reading the signature in and writing it out.
  const base64Signature = Buffer.from(signature, "base64").toString("base64");
Enter fullscreen mode Exit fullscreen mode

Validate the signature

The final step is to use the verifyObject to validate that the JWT's signature was created using the private key that is paired with the public key retrieved from the JWKS endpoint.

This is done by passing the PEM formatted public key, the signature and the format into the verify method on the verifyObject

  // validate that the signature is correct.
  const signatureIsValid = verifyObject.verify(pem, base64Signature, "base64");
Enter fullscreen mode Exit fullscreen mode

Don't do this

There are lots of great libraries available to validate tokens, take care of formatting issues and they are probably more reliable and robust than what I've put together here. If you're looking for a library to use try the listing on jwt.io.


Code for this blog can be found on my GitHub


Cover image from unsplash

Top comments (2)

Collapse
 
theupsider profile image
David Fischer

I actually made a npm package for this. npmjs.com/package/jwt-quick
Still room to improve, but works with types and supports a couple of algorithms.

Collapse
 
kleeut profile image
Klee Thomas

Nice. I advocate hard for always using a library to do this kind of thing. I wanted to explore what happens under the hood, but in any production code I'd always leave auth stuff to a library.