Application per tenant - role of API/audience

We are trying to figure out how to properly do tenant isolation using auth0 for our multi-tenant SaaS application.

We determined that it’s best for us to isolate tenants through individual applications per tenant. While implementing this, a few questions arose.

1. The role of the audience and APIs

As mentioned we were planning to separate tenants by applications and therefore would validate that JWT is valid, domain matches and application_id matches. The frontend framework auth0-spa however requires an audience to produce a full JWT. The documentation suggests that the value for audience should be the id of an API created in the dashboard. We don’t see a way of associating an API with an application or a user pool so it seems like a fully independent entity. Because of that, it seems like an arbitrary requirement and we would simply have a global API for all tenants just to satisfy the requirement.

a. Is this correct?
b. Is there some relevance to the API/audience that we don’t see and if yes, can we associate the API somehow with the userpool and the application to make it meaningful for us?

2. Validating the application_id

Our backend service is developed in python using FastAPI. Because of that we are basing our validation logic on this tutorial (and modifying it to be compatible with FastAPI).

This snippet contains logic to validate that the JWT belongs to the correct tenant:

token = get_token_auth_header()

jsonurl = urlopen("https://" + AUTH0_DOMAIN + "/.well-known/jwks.json")
jwks = json.loads(jsonurl.read())

unverified_header = jwt.get_unverified_header(token)
rsa_key = {}
for key in jwks["keys"]:
    if key["kid"] == unverified_header["kid"]:
        rsa_key = {
            "kty": key["kty"],
            "kid": key["kid"],
            "use": key["use"],
            "n": key["n"],
            "e": key["e"]
        }
if rsa_key:
    try:
        payload = jwt.decode(
            token,
            rsa_key,
            algorithms=ALGORITHMS,
            audience=GLOBAL_API_AUDIENCE_ID,
            issuer="https://" + AUTH0_DOMAIN + "/"
        )
        # manually check if `azp` which holds the application id matches the application_id that is configured in the tenants instance
        if payload["azp"] != TENANT_APPLICATION_ID: # (this is the application id)":
            raise AuthApiError("you are trying to access another tenants application")

a. Is this the correct way to do this?
b. Is there maybe a designated way to solve this and maybe even a trusted framework rather than these code snippets?

The audience should reflect the consumer of the token. Your SPA should not consume access tokens. Access tokens should be sent from your SPA (the client) to your backend API (the audience/resource) to verify the request between the them.

Maybe @jesstemporal can help with this. Do we have any FastAPI auth frameworks we recommend?

Hi @dan.woda thanks for the quick response. There may have been a misunderstanding. To clarify we are planning to do the following

  • use auth0-spa in SPA to obtain token
  • send token to backend as part of request
  • for every request → validate token using the python logic above (in the backend)

We are not planning to consume the token in the frontend (SPA) but in order to pass it to the backend we need to read it in the frontend, right?

The audience should reflect the consumer of the token

Based on our findings we would have a single API to represent the same type of service across all tenants. Is there any benefit of having a dedicated API/audience per tenant, given that it is not tied to a user pool anyway?

… to your backend API (the audience/resource) to verify the request between the them.

Is the verification by manually checking the “azp” key as outlined above appropriate? (asking because this is not taken from documentation or example and we just came up with it)

Thanks for your support.

The ID token returned in the request should have the user information necessary for your SPA. This topic adds some detail if you’re interested in learning more.

You can use a single API to serve all of your applications.

Can you give an example of the resources you’re securing with this claim. I am going to ask our field team for their input.

The code above contains the check which is our proposal of answering the question “does this token belong to the tenant that owns this service instance?” This check would run for every request coming into our backend service instances.

Since this is not a designated usage of Auth0 we would like to know:

  • Do you see any concerns with this?
  • Is there a designated way to do this that we missed?

This claim is meant to indicate what application was issued the ID token. It isn’t a strict indication of which organization the user belongs to, but it seems to get the job done.

As for other ways to do this, you could store the tenant/organization in the user’s app_metadata then Create Namespaced Custom Claims in an action or rule to indicate the user’s tenant/organization.

Similarly, you could use a custom claim to indicate which application the user is calling from, although this is similar to using the azp claim.

You could use Auth0 Organizations, although this may be a bit different than your requirements, and requires an enterprise subscription.