Handling (OAuth) refresh tokens can be quite complicated as there are a lot of parameters influencing the actual behaviour. In this article we show some best practices and how to test your actual configuration.
One might think that configuring the proper refresh token timeout for your application is performed simply by setting some specific value in you realm.
Alas, it is actually much more complicated, as there are at least four variables which you have to bear in mind:
- SSO Session Idle (ssoSessionIdleTimeout): If a user is inactive for longer than this period, the user session is invalidated.
- SSO Session Max (ssoSessionMaxLifespan): The maximum time before a user session expires finally.
- Client Session Idle (clientSessionIdleTimeout) and Client Session Max (clientSessionMaxLifespan): Basically the same for client sessions.
Timeout pitfalls
To make things no more complicated than necessary, we will look at the SSO session timeouts only and ignore clientSessionIdleTimeout and clientSessionMaxLifespan by setting them to 0.
As this more detailed discussion states, the value of ssoSessionIdleTimeout is effective only if it is smaller than ssoSessionMaxLifespan.
In our example we want to make sure that tokens need to be refreshed after 30 minutes and have a maximum life time of 10 hours. This looks like this in the realm definition file (JSON):
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
...
Note that KeyCloak will add an offset of two minutes to the timeout value. For example, when you have the timeout set to 30 minutes, it will be effectively 32 minutes before the session expires. This is due to some possible networking issues.
Testing timeouts
The next challenge is to write an integration test to ensure the expected behaviour. We do so by using the REST API client provided by KeyCloak. Given that a configured KC server and a backend server are running the test might look like this:
...
// Substract the 2 Minutes delay added by KC, i.e. the Refresh Token is valid for 5 seconds effectively
setSSOSessionIdleTimeout(-115)
login() // HTTP call
callProtectedBackendResource()
sleep(Duration.ofSeconds(5))
// Value stored during login
val refreshToken = ...
assertThat(refreshToken
.isExpired).isTrue()
assertThrows<Exception> {
callProtectedBackendResource() }
Helper methods
private fun setSSOSessionIdleTimeout(timeout: Int) {
val keycloak = Keycloak.getInstance(serverUrl, realm, username, password, clientId)
val realmResource = keycloak.realm(REALM)
val realm = realmResource.toRepresentation()
realm.ssoSessionIdleTimeout = timeout
realmResource.update(realm)
}