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