From 11b032f9cd5b336c1002ccb19e4977b1699f8ffa Mon Sep 17 00:00:00 2001 From: rmartinc Date: Mon, 5 May 2025 10:03:54 +0200 Subject: [PATCH] Return user session started time when client note is missing for offline Closes #39021 Signed-off-by: rmartinc --- .../AuthenticatedClientSessionModel.java | 8 ++- .../testsuite/oauth/OfflineTokenTest.java | 53 ++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java index 9ac322ee77b..8677b99e4d6 100644 --- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -39,8 +39,12 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode 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); + if (started == null) { + // Note can be null for offline sessions migrated from old version where "startedAt" note was not yet available + // Fallback to user session started for offline or 0 + return getUserSession().isOffline() ? getUserSessionStarted() : 0; + } + return Integer.parseInt(started); } default int getUserSessionStarted() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java index c15d535550e..1939b8774c2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java @@ -101,7 +101,6 @@ import static org.keycloak.testsuite.util.oauth.OAuthClient.APP_ROOT; import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Marek Posolda @@ -975,6 +974,22 @@ public class OfflineTokenTest extends AbstractKeycloakTest { }, Integer.class); } + private void removeClientSessionStartedAtNote(final String userSessionId, final String clientId, final String clientSessionId) { + testingClient.server().run(session -> { + RealmModel realmModel = session.realms().getRealmByName("test"); + session.getContext().setRealm(realmModel); + ClientModel clientModel = realmModel.getClientByClientId(clientId); + UserSessionModel userSession = session.sessions().getOfflineUserSession(realmModel, userSessionId); + if (userSession != null) { + AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(clientModel.getId()); + if (clientSession != null) { + clientSession.removeNote(AuthenticatedClientSessionModel.STARTED_AT_NOTE); + clientSession.removeNote(AuthenticatedClientSessionModel.USER_SESSION_STARTED_AT_NOTE); + } + } + }); + } + private void testOfflineSessionExpiration(int idleTime, int maxLifespan, int offsetHalf, int offset) { int prev[] = null; getTestingClient().testing().setTestingInfinispanTimeService(); @@ -1510,4 +1525,40 @@ public class OfflineTokenTest extends AbstractKeycloakTest { } } + @Test + public void offlineRefreshWhenNoStartedAtClientNote() throws Exception { + int prevOfflineSession[] = null; + try { + prevOfflineSession = changeOfflineSessionSettings(true, 3600, 3600, 0, 0); + + // login to obtain a refresh token + oauth.scope("openid " + OAuth2Constants.OFFLINE_ACCESS); + oauth.client("offline-client", "secret1"); + oauth.redirectUri(offlineClientAppUri); + oauth.doLogin("test-user@localhost", "password"); + String code = oauth.parseLoginResponse().getCode(); + AccessTokenResponse response = oauth.doAccessTokenRequest(code); + + EventRepresentation loginEvent = events.expectLogin() + .client("offline-client") + .detail(Details.REDIRECT_URI, offlineClientAppUri) + .assertEvent(); + + // remove the started notes that can be missed in previous versions + String clientSessionId = getOfflineClientSessionUuid(loginEvent.getSessionId(), loginEvent.getClientId()); + removeClientSessionStartedAtNote(loginEvent.getSessionId(), loginEvent.getClientId(), clientSessionId); + + // check refresh is successful + response = oauth.doRefreshTokenRequest(response.getRefreshToken()); + assertEquals(200, response.getStatusCode()); + assertTrue("Invalid ExpiresIn", 0 < response.getRefreshExpiresIn() && response.getRefreshExpiresIn() <= 3600); + + // check refresh a second time + response = oauth.doRefreshTokenRequest(response.getRefreshToken()); + assertEquals(200, response.getStatusCode()); + assertTrue("Invalid ExpiresIn", 0 < response.getRefreshExpiresIn() && response.getRefreshExpiresIn() <= 3600); + } finally { + changeOfflineSessionSettings(false, prevOfflineSession[0], prevOfflineSession[1], prevOfflineSession[2], prevOfflineSession[3]); + } + } }