What this is about
An access token is valid only for a short time. In Keycloak, the default is 5 minutes. So that no one has to sign in again every few minutes, the BFF gets a new one in time. For this, it sends the refresh token to Keycloak. This is called a refresh.
Why are access tokens so short? An access token is only checked locally on every request. No one asks Keycloak. So a revoked permission only disappears when a new token is issued. Short tokens keep this gap small. See Why revoking permissions takes effect with a delay.
The timeline
gantt
title One access token (default 5 minutes)
dateFormat mm:ss
axisFormat %M:%S
section Token 1
valid :t1, 00:00, 5m
Buffer, refresh due :crit, r1, 03:30, 90s
section Token 2
valid after the refresh :t2, 03:30, 5m
From 90 seconds before expiry, the token counts as “expiring soon”. The next session request then triggers the refresh. The user interface requests the session every minute and every time the tab comes back to the foreground. So the refresh almost always happens within the buffer.
How the refresh works
-
1Browser→BFFrequests
/api/auth/session(every minute and on focus) -
2BFFDoes the access token expire in less than 90 seconds?no: session returned unchanged
-
3BFF→Keycloak
POST /tokenwithgrant_type=refresh_token, client ID and client secret -
4Keycloak→BFFnew access token, usually also a new refresh token and ID token
-
5BFFwrites the new tokens into the session cookieResult: Session continues, no one notices anything
Keycloak issues a new refresh token on every refresh. If the realm is set up so that a refresh token is valid only once, the old one becomes invalid. This is called rotation.
The outcomes
When: Keycloak responds with new tokens.
The BFF takes over the access, refresh and ID token and clears any error field in the session. If no new refresh token comes along, it keeps the old one.
Result: Session continues.
When: Keycloak rejects, usually with invalid_grant. The refresh token has expired, the Keycloak session was ended, or the account was suspended.
-
1BFFsets the error
RefreshAccessTokenErrorin the session -
2BFF→BrowserAPI calls of the BFF now respond with 401
-
3Frontendsees the error and redirects to
/login -
4Browser→Keycloaknew login. If there is no session at Keycloak anymore, Keycloak asks for the password
Result: The old session is over. The new login overwrites the cookie.
When: Keycloak responds with 5xx or not at all.
The BFF keeps the current token and sets no error. The next session request tries again. The 90-second buffer leaves time for this before the token really expires.
Result: Short Keycloak outages go unnoticed. If the outage lasts longer than the token, the APIs respond with 401.
When: Keycloak responds with 5xx or not at all.
The hub treats every failed refresh like a rejection. It removes the access token and sets RefreshAccessTokenError.
Result: The user interface starts a new login. As long as Keycloak is not reachable, it does not succeed.
Special behavior in the hub
The hub also renews before every API call. If the access token has already expired (with a 30-second safety margin), the BFF first gets a new one. The new token applies only to this call. It goes into the cookie on the next session request.
If Keycloak rotates the refresh token, two simultaneous calls must not renew twice with the same refresh token. The second one would get invalid_grant. So the hub remembers for 60 seconds which renewals are running or have just run, and lets parallel calls wait for the same result.
And API clients without a user interface?
Your own client that calls CDMS directly does the same: renew with the refresh token before expiry. If it gets 401 with invalid_token from the API, the token has expired: renew it and repeat the request. See Access without a token.