Gatekeeper and the ID Token

Hi Keycloak Community

While setting up Keycloak Gatekeeper as an authentication proxy for a SPA, I noticed that it is assuming the ID token is issued for the same audience like the access token. This is not the case when you use different client IDs for the front- and backend.

But first of all let me give you some context:

  • the backend is serving a REST API for the SPA frontend at /api
  • it is also serving the static files of the SPA from / (root)
  • the SPA does not handle any authentication and has no authorization requirements
  • Gatekeeper is used to authenticate the user before serving the SPA, to persist the tokens in an http-only cookie across requests from the SPA to the backend and to add the authentication header to the backend requests
  • this way the frontend does not need any authentication logic and the backend does not need to handle any OAuth flow. It simply has to validate the bearer token of the incoming requests.

This works well if you use a single Keycloak client (let’s name the client “APP”) for Gatekeeper (doing the authentication on behalf of the frontend) AND for the REST API backend (relying on the access token as a proof that the user authenticated and delegated the frontend/Gatekeeper to access the backend on his behalf). This is considered bad practice according to the Keycloak documentation itself.

Anyway, it works with a single client and the following Gatekeeper configuration:

upstream-url: https://backend.local
client-id: APP
client-secret: …
discovery-url: https://keycloak.local/auth/realms/acme
enable-login-handler: true
enable-refresh-tokens: true

It doesn’t work if we want to properly separate the authentication for the frontend (by using the ID token) and authorization for the backend (using the access token) by using two Keycloak clients:

• A confidential client (APP-frontend) for the frontend (the Relying Party in OIDC terms).
• A bearer only client (APP-backend) for the backend (the Resource Server).

In such a scenario the ID token is issued for the frontend (aud = APP-frontend) and the access token for the backend (aud = APP-backend).

The Gatekeeper config would look like this (note the client-id):

upstream-url: https://backend.local
client-id: APP-frontend
client-secret: …
discovery-url: https://keycloak.local/auth/realms/acme
enable-login-handler: true
enable-refresh-tokens: true

And the config for the Keycloak provider used in the backend would look like this:

{
“realm”: “acme”,
“auth-server-url”: “https://keycloak.local/auth”,
“ssl-required”: “external”,
“resource”: “APP-backend”,
“verify-token-audience”: true,
“credentials”: {
“secret”: “…”
}
}

This does not work because Gatekeeper is checking the audience of the access token and not of the ID token. The log output however is saying “unable to verify id token” which is wrong and confusing:

error keycloak-gatekeeper/handlers.go:164 unable to verify the id token {“error”: “oidc: JWT claims invalid: invalid claims, ‘aud’ claim and ‘client_id’ do not match, aud=APP-backend, client_id=APP-frontend”}

Gatekeeper assumes that the same client_id is used for itself and for the downstream service.

When looking at this line of code you can see that it is actually checking the audience of the access token: https://github.com/keycloak/keycloak-gatekeeper/blob/b04d5267e8780ad0a1258bd3bd1c2c7b48d23548/handlers.go#L163

So in short: Gatekeeper requires the ID token to be issued to the same OAuth client like the access token and I am now wondering if the described scenario where the ID token is issued for the frontend and the access token for the backend is not meant to be supported by Gatekeeper or just hasn’t made it into the product yet. It would be nice if you could use it to persist the ID token in a cookie and configure it to check the audience of the ID token.

Kind regards,
Marco

Hi Marco,

I have a similar setup and for me the following steps worked:

I just repeated the steps for APP-frontend and APP-backend.

I have a question regarding your setup for the authentication of the SPA.
I would also like to remove all authentication/keycloak dependencies from the frontend and let Gatekeeper handle everything.

persist the tokens in an http-only cookie across requests from the SPA to the backend and to add the authentication header to the backend requests

How did you manage this without touching the frontend code explicitally?

Are your requests to the backend routed through gatekepper? If so how?

Hope i could help you and you can help me :slight_smile:

Cheers,
John

EDIT: Are you using a single gatekeeper instance for both frontend and backend? I guess my setup is different since I have a seperate Gatekeeper sidecar for every service.

I have the same situation and I did not find a solution yet.
Maybe I can answer your question, at least in my case those are the answers…

How did you manage this without touching the frontend code explicitally?

Configuring a specific client to frontend in KeyCloak management ui, and setting implicit flow, it’s for frontends without backends to have an authentication…

Are your requests to the backend routed through gatekepper? If so how?

There’s a proxy injector that injects gatekeeper as a sidecar, and all request pass through it. I’ve been doing tests with Istio(service mesh) and it does the same thing as mentioned(I’m not quite sure).

Anyways, is it possible to have a frontend with access-typepublic” and “implicit flow” enabled and a back end with access-typebearer-only

If so, how’s the configuration be like? I’m struggling on this scenario.

Best regards,
Galvão