Use Keycloak Gatekeeper in front of backend API

3.3k views Asked by At

On a single page app (SPA) that runs on DOMAIN calls to DOMAIN/graphql are rerouted to the backend. Both the frontend and backend are secured via a Keycloak Gatekeeper instance.

The idea is that the frontend and backend share the kc-access token.

Now, the access token expires in the backend Gatekeeper. If the SPA is refreshed in the browser the frontend is rerouted to Keycloak and a fresh access token is required. But if there's no refresh, the POST requests to DOMAIN/graphql fail with a 307 status code when the token has expired. The browser does not know how to handle this. The browser logging gives an "{"error":"RESTEASY003065: Cannot consume content type"}". If the content-type header of the POST is removed the error is "no client_id provided", while the client_id is included in the query string.

Redirecting a POST request to Keycloak would probably not be the best solution. Cleaner would be if the backend refreshes it's access token itself.

This is what we tried by adding a session state store to the backend's Gatekeeper. We are using the following configuration:

- --discovery-url=DISCOVERY_URL
- --client-id=CLIENT_ID
- --client-secret=****
- --enable-refresh-tokens=true
- --encryption-key=0123456789012345
- --store-url=boltdb:///boltdb
- --listen=0.0.0.0:3001
- --verbose=true
- --redirection-url=REDIRECTION_URL
- --upstream-url=http://127.0.0.1:3000

This does create a /boltdb file in the Gatekeeper, but it does not seem to be used since the file does not change.

The backend's Gatekeeper gives the following logging:

|1.5716729131430433e+09|debug|keycloak-gatekeeper/session.go:51|found the user identity|{"id": "b5b659cd-148e-4f23-bf2f-28e6f207f6c7", "name": "piet", "email": "", "roles": "offline_access,dashboard_viewer,uma_authorization,account:manage-account,account:manage-account-links,account:view-profile", "groups": ""}|
|1.5716729131462774e+09|info|keycloak-gatekeeper/middleware.go:154|accces token for user has expired, attemping to refresh the token|{"client_ip": "****", "email": ""}|
|1.5716729131463811e+09|error|keycloak-gatekeeper/middleware.go:161|unable to find a refresh token for user|{"client_ip": "**", "email": "", "error": "no session state found"}|

So we are "unable to find a refresh token for user" because there is "no session state found" according to the logging.

Anybody any idea how to enable token refresh?

2

There are 2 answers

0
user2609980 On BEST ANSWER

By also setting enable-refresh-tokens=true with the same encryption key in the Gatekeeper on the frontend the design works.

The user retrieves the frontend and is redirected to Keycloak. There an authorization code is obtained. This authorization code is exchanged by the frontend Gatekeeper for an access and refresh token that are put in a cookie on the frontend. When the backend is called with an expired access token the refresh token is decrypted and used to get a new access token.

The refresh token can expire or be invalidated. When a 401 is returend the frontend should refresh the page so the user is redirected to Keycloak.

More secure would be to store the tokens not in the frontend cookies, but in a shared store.

11
Jan Garaj On

It doesn't look like a good design. Keycloak Gatekeeper uses grant code flow, which is not the best flow for SPA as you have discovered (it seems to be very hackish to read user identity provided by Gatekeeper in SPA case).

SPA uses Code Flow with PKCE or Implicit Flow and these flows use silent token renewal (and not refresh token). IMHO the best option will be to use the same client id in the frontend (SPA) and in the backend (e.g. API). But frontend will be protected by Code Flow with PKCE and it will handle own token renewal. Only backend will be protected by Gatekeeper (+ --no-redirects setting makes sense for API protection)