DEV Community

Cover image for Validate an OpenID Connect JWT using a public key in JWKS
Axel Navarro for Cloud(x);

Posted on

Validate an OpenID Connect JWT using a public key in JWKS

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. 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 npm package made by Auth0.

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 parameter (e.g. RS256) to sign the JWT, and the kid 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.

const jwt = require('jsonwebtoken');
const payload = jwt.decode('<your_jwt_here>');
console.log(payload.iss);
Enter fullscreen mode Exit fullscreen mode

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! 😉

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);
Enter fullscreen mode Exit fullscreen mode

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.

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);
Enter fullscreen mode Exit fullscreen mode

⚠️ 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.

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;
};
Enter fullscreen mode Exit fullscreen mode

With this little change you're safe now 🔒

Conclusion

Like I mentioned in this post, 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 instead of kid). You can find examples with public.pem files in the 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.

Top comments (0)