Validate an OpenID Connect JWT using a public key in JWKS
Tags: javascript, node, security, webdev
Published: 2023-03-30 14:45:36

---
title: Validate an OpenID Connect JWT using a public key in JWKS
published: true
description: How to validate a JSON web token using the public keys of the issuer
tags: 'javascript, node, security, webdev'
cover_image: 'https://raw.githubusercontent.com/cloudx-labs/posts/main/posts/navarroaxel/assets/oidc-jwks-cover.jpg'
id: 1339462
---

You may have used OpenID Connect in the Front-end, where an IDP (IDentity Provider) authenticates a user, gives you a bunch of tokens in the browser and then you can add the `Authorization` header to your HTTP requests to your own Back-End because you trust this IDP. But what happens when you can't find where you can verify the `id_token` (or `access_token`) using some endpoint in the IDP?

Well, I found how an [OpenID Connect `id_token` should be validated](https://curity.io/resources/learn/validating-an-id-token/). It wasn't straightforward in my case: I had to do a lot of research to validate my `id_token`. Let's see how to make this easy using Node.js and the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) npm package made by [Auth0](https://auth0.com).

## Understanding JWT to know how to validate it

A JSON Web Token (JWT) is a string built with 2 JSON objects encoded in `base64` and a signature; these parts are joined by a period (`.`) with the following structure: `<header>.<payload>.<signature>`.

💡 This is why a JWT always starts with `ey`, because it is the result of encoding `{"` using `base64`, which is the beginning of any JSON.

In the `header` part we can find which signature algorithm was used in the [`alg`](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.1) parameter (e.g. _RS256_) to sign the JWT, and the [`kid`](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) parameter tells which _Key ID_ from the JSON Web Key Set (JWKS) was used for a given token.

🧠 Remember that when the JWT `header` has a _Key ID_ (`kid`), JWKS is used.

And here is where the problem starts. Where do I find the JWKS to get the public key so we can verify the integrity of this token? 😫

## The issuer

The issuer is the one who created and signed the JWT, and we can know this by checking the value `iss` in the payload of our JWT - using `jwt.decode` from `jsonwebtoken`.

```javascript
const jwt = require('jsonwebtoken');
const payload = jwt.decode('<your_jwt_here>');
console.log(payload.iss);
```

Alternatively, you can paste your JWT into <https://jwt.io> (don't worry, it's a safe website from Auth0), and `iss` is there too!

> In OpenID Connect, the issuer should be a URL, but it could just be the name of the IDP. In that case you should read more docs to find where the JWKS URI is. 😭

For our example we can use <https://sandrino.auth0.com> as an issuer, so you can know the OpenID configuration using the well-know URI <https://sandrino.auth0.com/.well-known/openid-configuration>, and in the `jwks_uri` attribute is where you can find the JWKS for our issuer. You can check this same value in another issuer <https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration>, the `jwks_uri` is there too! 😉

```javascript
const jwt = require('jsonwebtoken');

const printJwksUri = async (issuer) => {
  const response = await fetch(`${issuer}/.well-known/openid-configuration`);
  const {jwks_uri} = await response.json();
  console.log(jwks_uri);
};

const token = '<your_jwt>';
const {iss} = jwt.decode(token);
printJwksUri(iss);
```

We have the JWKS URI programmatically in Node.js! 🥳

## Verifying the token

Now that we know enough about JWKS, we can write a Node.js code to validate an OpenID token that discovers the JWKS URI if you don't know where it is.

```javascript
const {promisify} = require('node:util');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const fetchJwksUri = async (issuer) => {
  const response = await fetch(`${issuer}/.well-known/openid-configuration`);
  const {jwks_uri} = await response.json();
  return jwks_uri;
};

const getKey = (jwksUri) => (header, callback) => {
  const client = jwksClient({jwksUri});
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      return callback(err);
    }
    callback(null, key.publicKey || key.rsaPublicKey);
  });
};

/**
 * Verify an OpenID Connect ID Token
 * @param {string} token - The JWT Token to verify
 */
const verify = async token => {
  const {iss: issuer} = jwt.decode(token);
  const jwksUri = await fetchJwksUri(issuer);
  return promisify(jwt.verify)(token, getKey(jwksUri));
};

const token = '<your_jwt>';
verify(token)
  .then(() => console.log('Token verified successfully.'))
  .catch(console.error);
```

⚠️ Did you notice that I didn't check who is the issuer? This code will accept any of them, even a malicious one. 😨 To control this just accept issuers from an allowed list.

```javascript
const allowedIssuers = [
  'https://login.microsoftonline.com/common/v2.0',
  'https://sandrino.auth0.com',
];

const fetchJwksUri = async (issuer) => {
  if (!allowedIssuers.includes(issuer)) {
    throw new Error(`The issuer ${issuer} is not trusted here!`);
  }
  const response = await fetch(`${issuer}/.well-known/openid-configuration`);
  const {jwks_uri} = await response.json();
  return jwks_uri;
};
```

With this little change you're safe now 🔒

## Conclusion

Like I mentioned in [this post](https://dev.to/cloudx/build-a-discoverable-change-password-form-3f9m), these well-known URIs are a standard method to get information for specific features, and issuers should implement the  `/.well-known/openid-configuration` to integrate it easier to your authentication flow.

🧠 OpenID Connect is a safe way to authenticate users, but you always have to verify the token's integrity in the Back-End side and check if it was created by a trusted issuer.

🔑 Remember that this scenario only works with JWKS (when the certificate is pre-distributed to the clients the JWT `header` has [`x5t`](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7) instead of `kid`). You can find examples with `public.pem` files in the [`jsonwebtoken`](https://github.com/auth0/node-jsonwebtoken) package.

🪙 Bonus tip if you use Autenticar service from the Argentinian government, a.k.a AFIP Clave Fiscal, you can use this code to validate the `id_token` with the JWKS.