Hasura JWKS: JWT w/ Multiple Auth0 tenants

Hasura JWKS: JWT w/ Multiple Auth0 tenants

TL;DR - By default, Hasura does not accept multiple JWKS URLs. Thus, write a lambda or a simple HTTP API to combine the JWKS keys of multiple tenants into a single JWKS endpoint. (Bonus: Buggy Auth0 fingerprint implementation)

Prelude

Before starting, please make sure to add the Auth0 Rule as mentioned in this support document. Test to make sure the Hasura Claim is present in the JWT issued by Auth0.

The Auth0 JWKS Bug

Thanks to a pretty weird bug in Auth0, if you have an old signing key generated on/before April 2020, the key has an invalid x5t fingerprint. Thus, Hasura will not work if you point it to a JWKS endpoint containing the old x5t fingerprints.

Thankfully, Auth0 has issued a fix, and all you need to do is rotate your signing keys.

Observing the JWKS response

The JWKS URL of your Auth0 Tenant will respond resembling the type declaration stated below.

(Hint: https://..auth0.com/.well-known/jwks.json )

export interface Key {
    alg: string;
    kty: string;
    use: string;
    n: string;
    e: string;
    kid: string;
    x5t: string;
    x5c: string[];
}

export interface RootObject {
    keys: Key[];
}

Thus, we just need to fetch the JWKS Key objects from all our tenants and merge them into a single keys array. Let's do it.

Building the Lambda

Let's list down all our JWKS endpoints in an array. Make sure to replace it with your Auth0 Domains.

const jwksUrls = [
    'https://foo.au.auth0.com/.well-known/jwks.json',
    'https://bar.au.auth0.com/.well-known/jwks.json',
    'https://baz.au.auth0.com/.well-known/jwks.json',
];

Next, time to parallelly download all the JWKs for merging. We'll be using axios.

const axios = require("axios");

const responses = await Promise.all(jwksUrls.map((url) => axios.get(url)));
const keySets = responses.map(response => response.data.keys)

Okay, we have to take a break here. Recall the Auth0 x5t bug? Yeah. It turns out, even if you rotate the key, for the sake of other microservices in your system, you may decide to keep the previous key still valid. Otherwise, the previously issued tokens would become invalid and your users wouldn't stay "logged in" anymore.

There is a tiny workaround. It is to remove the invalid keys with the invalid x5t signatures from the keySets. However, this does mean that your new Hasura system will only work with newly issued JWT tokens signed with your shiny new key. Thus, we will replace that last line with,

const keySets = responses.map(response => response.data.keys.filter(key => key.kid !== key.x5t));

Because in the faulty implementation, the kid and x5t are the same.

The final code for the Lambda

const axios = require("axios");

exports.handler = async (event) => {
    const jwksUrls = [
        'https://foo.au.auth0.com/.well-known/jwks.json',
        'https://bar.au.auth0.com/.well-known/jwks.json',
        'https://baz.au.auth0.com/.well-known/jwks.json',
    ];

    const responses = await Promise.all(jwksUrls.map((url) => axios.get(url)));
    const keySets = responses.map(response => response.data.keys.filter(key => key.kid !== key.x5t));
    const keys = [].concat(...keySets);

    const response = {
        keys,
    };

    return response;
};

Pointing Hasura to the JWKs endpoint

You may choose to create a lambda or host this function any way depending on your preference. Finally, once you have the endpoint to the JWKS merging API, simply point Hasura to it. Update the HASURA_GRAPHQL_JWT_SECRET environment variable to,

{
  "type":"RS256",
  "jwk_url": "",
  "claims_format": "stringified_json"
}

The end!

Β