Keycloak and aud claim usage as additional authentication layer

By | April 14, 2025

OpenFGA and Keycloak configuration

Some time ago, we integrated OpenFGA with Keycloak for our AuthN/AuthZ implementation. OpenFGA can interpret the token’s “aud” claim when making authentication/authorization decisions.

The “aud” claim specifies the intended recipient(s) of the token:

The “aud” (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT must identify itself with a value in the audience claim.

in accordance with:

https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3

OpenFGA’s gets “aud” claim and then checks this value:

The authentication logic described here – https://github.com/openfga/openfga/blob/73dceae92683474c2d788c9b4467abe2d2e3b3db/internal/authn/oidc/oidc.go#L78

And authorization logic – https://github.com/openfga/openfga/blob/73dceae92683474c2d788c9b4467abe2d2e3b3db/internal/authz/authz.go#L223

Additional details you can find in the OpenFGA official documentation – https://openfga.dev/docs/getting-started/setup-openfga/configure-openfga#oidc

On the Keycloak side, the configuration of the OIDC client for AuthN/AuthZ related to OpenFGA looks as follows.

1-st. Create a separate OIDC client, openfga-test, in our case

2-nd. Create a client scope with the name “openfga-audience” for example, with the “none” Assigned Type (that doesn’t propagate this client scope to all OIDC clients) and assign it explicitly to the “openfga-test” OIDC client

Keycloak / client scope configuration with the "none" assigned type

In the openfga-audience client scope

Keycloak / client scope mapper configuration

add a custom mapper that will add a hardcoded aud claim to the JWT token

Keycloak / client scope and mapper configuration

3-rd. Assign this client scope to your OIDC client in OIDC client configuration.

Our resultant JWT token now has aud claim equal to “openfga”, see below

{
    "exp": 1739368919,
    "iat": 1739368619,
    "jti": "db10a698-222a-44ad-b788-806a3333437c",
    "aud": "openfga",
    "sub": "cf9fb982-0650-4749-83c5-c1b63d1a8f1a",
    "typ": "Bearer",
    "azp": "openfga-test",
    "acr": "1",
    "allowed-origins": [
      "/*"
    ],
    "scope": "email profile",
    "email_verified": false,
    "preferred_username": "service-account-openfga-test",
    "session_state": "1bc88d45-4b12-4d73-be18-8c8fdb7a874f",
    "client_id": "openfga-test"
  }

Consequences

Finally, our simplified authorization architecture might look like the following:

  • we explicitly assign the aud claim using a custom client scope and a specific mapper that handles this.
  • this claim is automatically assigned only to specific OIDC clients—we prevent its propagation to all clients by setting the “Assigned type” to none.
  • only the owner of the OIDC client, security engineers, or GitHub code with specific approval are allowed to assign this claim to the client.
  • it would not be possible to interact with OpenFGA using tokens that do not include our aud claim.

Conceptually, the “aud” claim can be used to implement whitelisting between microservices, services, or applications—determining which service or app, holding a given token (and its “aud” claim), is allowed to authenticate with another. If you automate and persist this logic in a centralized location, you can even visualize and streamline the decision-making process, such as determining which service, app, or user is allowed to authenticate with others. However, I recommend using Distributed Authorization with OPA and Rego policies for this purpose, and reserving the use of the “aud” claim for specific cases—such as when your mobile application needs to authenticate different other mobile applications that are associated with distinct OIDC clients.

Leave a Reply