1

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?

Jan Garaj
  • 25,598
  • 3
  • 38
  • 59
user2609980
  • 10,264
  • 15
  • 74
  • 143

2 Answers2

2

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)

Jan Garaj
  • 25,598
  • 3
  • 38
  • 59
  • Thanks. It is the frontend that works however. The back end does not. But you say that the frontend should renew it's own token? Then that token is send along and used in the back end as well? And with no redirects we get a 401 back instead of a redirect? And why do you think setting enable-refresh-tokens=true does not refresh the token in the back end? – user2609980 Oct 22 '19 at 05:11
  • The frontend initial page is protected , then all needed information is retrieved via backend calls where the user identity needs to be known – user2609980 Oct 22 '19 at 06:46
  • Would it not be fine to just refresh the token in the backend? Shouldn't that also work? I don't understand why it says "no session state found" when the store is enabled. – user2609980 Oct 22 '19 at 06:47
  • PKCE is used for mobile apps or SPAs without a backend right? We do have a backend. I don't see why we should implement code flow with PKCE. Are there security risks with the backend token renewal, if it would work? – user2609980 Oct 22 '19 at 07:27
  • 1
    I'm saying that frontend will use PKCE and it will handle token renewal AND backend (API) will be protected by Gatekeeper, which actually only verifies access token. – Jan Garaj Oct 22 '19 at 09:43
  • I see. Thanks. So putting a Gatekeeper in front of the webserver that serves the HTML SPA code is not a good practice? But is it a security risk? I don't think so. I mean if the backend API does refresh it's token the /graphql backend calls will keep working for that session right? But indeed at the moment the Gatekeeper only verifies the access token and is unable to refresh even when saying `enable-refresh-tokens=true`. – user2609980 Oct 22 '19 at 09:49
  • Non working automatic refresh tokens are fixed in this PR: https://github.com/keycloak/keycloak-gatekeeper/pull/484. If refresh tokens work on basis of the SESSION of the user and are immediately obtained in the Gatekeeper I see no security issues with this approach. – user2609980 Oct 22 '19 at 15:29
  • I see you suggest to use PKCE in the frontend? I feel uncomfortable because I don't understand. We have the backend client under control, it's a confidential client. If you look at [this blog post with OAuth recommendation](https://medium.com/@robert.broeckelmann/when-to-use-which-oauth2-grants-and-oidc-flows-ec6a5c00d864) we therefore fall in the authorization grant flow. I don't see a way how a token an be intercepted. Only the authorization code can be obtained, but that is immediately exchanged for an access token by the Gatekeeper. – user2609980 Oct 23 '19 at 08:12
  • I recommend to check some doc/blogspost how to secure SPA with OIDC first. Of course you can use whatever you want, because you can implement in hackish way. But you need to be skilled hacker, who knows consequences. – Jan Garaj Oct 23 '19 at 10:00
  • Thanks. What works is putting a Gatekeeper in front of the SPA and in front of the backend API, that have the same encryption key. Both Gatekeepers have `enable-refresh-tokens=true` (at first it was only the backend, and then you get "no session state found"). Now the refresh tokens are encrypted in the `kc-state` cookie and are reused by the backend and frontend. I think this is a safer solution than using an implicit or PKCE flow. Those implicit flows seem more appropriate when calling a public API or when the SPA frontend is public? For us the frontend is also behind the login portal. – user2609980 Oct 24 '19 at 10:33
  • Thanks for all your suggestions! – user2609980 Oct 24 '19 at 10:35
0

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.

user2609980
  • 10,264
  • 15
  • 74
  • 143