-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Description
Before reporting an issue
- I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.
Area
oidc
Describe the bug
This is for migration from RH-SSO 7.6 to Keycloak 24.0.
After they migrated their production environment to Keycloak 24.0.z, they attempted to refresh a token for an offline session migrated from RH-SSO, and the following was returned as the token response.
{
"access_token": "...",
"expires_in": -1586880441,
"refresh_expires_in": -1586880441,
"refresh_token": "...",
"token_type": "Bearer",
"not-before-policy": 0,
"session_state": "...",
"scope": "... offline_access"
}
As you can see, the expires_in value is negative, which is invalid.
The value is derived from the exp claim of the access token, and the issued access token has an exp value of 157680000 (1974-12-31 00:00:00 UTC), which is also invalid.
{
"exp": 157680000,
"iat": 1744560441,
"auth_time": 1709734954,
"typ": "Bearer",
...
The exp value of the access token is, roughly speaking, calculated as (session start time) + offlineSessionMaxLifespan.
In this realm, offlineSessionMaxLifespan is set to 157680000 seconds (5 years), so it appears the result is from 0 + offlineSessionMaxLifespan.
In other words, for some reason, the session start time is being treated as 0.
The following is the code that likely considers the session start time to be 0:
keycloak/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java
Lines 36 to 40 in 81aa588
| default int getStarted() { | |
| String started = getNote(STARTED_AT_NOTE); | |
| // Fallback to 0 if "started" note is not available. This can happen for the offline sessions migrated from old version where "startedAt" note was not yet available | |
| return started == null ? 0 : Integer.parseInt(started); | |
| } |
For recently created sessions, it seems that a note called startedAt is added to the offline session. However, in the customer's case, the session was created with an older version, so it appears that the startedAt note is not included [1].
Therefore, clientSession.getStarted() in the following code becomes 0, leading SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp() to return 0 + offlineSessionMaxLifespan, which caused the access token's exp to be 157680000.
keycloak/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
Lines 1039 to 1042 in b9bd644
| long sessionExpires = SessionExpirationUtils.calculateClientSessionMaxLifespanTimestamp( | |
| userSession.isOffline() || offlineTokenRequested, userSession.isRememberMe(), | |
| TimeUnit.SECONDS.toMillis(clientSession.getStarted()), TimeUnit.SECONDS.toMillis(userSession.getStarted()), | |
| realm, client); |
Version
24.0.z
Regression
- The issue is a regression
Anything else?
Consideration
Is it possible that if clientSession.getStarted() is 0, then we use the iat of the offlineToken, which was sent to the endpoint? We should make sure to set startedAt, so that it is correctly set in the DB for the next time.
Alternative is to update all client sessions at startup and set started_at in case that it is not set. But not sure if still possible and it can take long time as bulk updates could be long and people can have millions of sessions in the DB...
Testing note
There is AbstractMigrationTest.testOfflineTokenLogin() for testing migration of offline-token from the older version. Currently it is tested from Keycloak 19 or earlier, which I am not 100% sure if can be used to simulate the issue (as the issue is for migration from RH-SSO 7.6, which is Keycloak 18), but maybe yes. Hopefully we can maybe add calling the "introspection endpoint" to that testOfflineTokenLogin() to doublecheck if access-token has correct exp times (as introspection for such access-token would probably fail).