offline token issuance can cause violation of PRIMARY KEY constraint CONSTRAINT_OFFL_CL_SES_PK3 (#14658)

closes #13706
This commit is contained in:
Marek Posolda 2022-10-03 12:54:12 +02:00 committed by GitHub
parent 390c7485c7
commit fb24c86a3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 13 deletions

View file

@ -90,26 +90,44 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
PersistentAuthenticatedClientSessionAdapter adapter = new PersistentAuthenticatedClientSessionAdapter(session, clientSession);
PersistentClientSessionModel model = adapter.getUpdatedModel();
PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
String userSessionId = clientSession.getUserSession().getId();
String clientId;
String clientStorageProvider;
String externalClientId;
StorageId clientStorageId = new StorageId(clientSession.getClient().getId());
if (clientStorageId.isLocal()) {
entity.setClientId(clientStorageId.getId());
entity.setClientStorageProvider(PersistentClientSessionEntity.LOCAL);
entity.setExternalClientId(PersistentClientSessionEntity.LOCAL);
clientId = clientStorageId.getId();
clientStorageProvider = PersistentClientSessionEntity.LOCAL;
externalClientId = PersistentClientSessionEntity.LOCAL;
} else {
entity.setClientId(PersistentClientSessionEntity.EXTERNAL);
entity.setClientStorageProvider(clientStorageId.getProviderId());
entity.setExternalClientId(clientStorageId.getExternalId());
clientId = PersistentClientSessionEntity.EXTERNAL;
clientStorageProvider = clientStorageId.getProviderId();
externalClientId = clientStorageId.getExternalId();
}
entity.setTimestamp(clientSession.getTimestamp());
String offlineStr = offlineToString(offline);
boolean exists = false;
PersistentClientSessionEntity entity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(userSessionId, clientId, clientStorageProvider, externalClientId, offlineStr));
if (entity != null) {
// client session can already exist in some circumstances (EG. in case it was already present, but expired in the infinispan, but not yet expired in the DB)
exists = true;
} else {
entity = new PersistentClientSessionEntity();
entity.setUserSessionId(userSessionId);
entity.setClientId(clientId);
entity.setClientStorageProvider(clientStorageProvider);
entity.setExternalClientId(externalClientId);
entity.setOffline(offlineStr);
entity.setUserSessionId(clientSession.getUserSession().getId());
}
entity.setTimestamp(clientSession.getTimestamp());
entity.setData(model.getData());
if (!exists) {
em.persist(entity);
em.flush();
}
}
@Override
public void removeUserSession(String userSessionId, boolean offline) {

View file

@ -57,6 +57,7 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
import org.keycloak.testsuite.auth.page.AuthRealm;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
@ -743,6 +744,64 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
testOfflineSessionExpiration(IDLE_LIFESPAN, MAX_LIFESPAN, IDLE_LIFESPAN + SessionTimeoutHelper.IDLE_TIMEOUT_WINDOW_SECONDS + 60);
}
// Issue 13706
@Test
public void offlineTokenReauthenticationWhenOfflinClientSessionExpired() throws Exception {
// expect that offline session expired by idle timeout
final int MAX_LIFESPAN = 360000;
final int IDLE_LIFESPAN = 900;
getTestingClient().testing().setTestingInfinispanTimeService();
int prev[] = null;
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(adminClient.realm("test")).setSsoSessionIdleTimeout(900).update()) {
// Step 1 - offline login with "offline-client"
prev = changeOfflineSessionSettings(true, MAX_LIFESPAN, IDLE_LIFESPAN);
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
assertOfflineToken(tokenResponse);
// Step 2 - set some offset to refresh SSO session and offline user session. But use different client, so that we don't refresh offlineClientSession of client "offline-client"
setTimeOffset(800);
oauth.clientId("test-app");
oauth.redirectUri(APP_ROOT + "/auth");
oauth.openLoginForm();
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
tokenResponse = oauth.doAccessTokenRequest(code, "password");
assertOfflineToken(tokenResponse);
// Step 3 - set bigger time offset and login with the original client "offline-token". Login should be successful and offline client session for "offline-client" should be re-created now
setTimeOffset(900 + SessionTimeoutHelper.PERIODIC_CLEANER_IDLE_TIMEOUT_WINDOW_SECONDS + 20);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.openLoginForm();
code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
assertOfflineToken(tokenResponse);
} finally {
getTestingClient().testing().revertTestingInfinispanTimeService();
changeOfflineSessionSettings(false, prev[0], prev[1]);
}
}
// Asserts that refresh token in the tokenResponse is offlineToken. Return parsed offline token
private RefreshToken assertOfflineToken(OAuthClient.AccessTokenResponse tokenResponse) {
Assert.assertEquals(200, tokenResponse.getStatusCode());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.parseRefreshToken(offlineTokenString);
assertEquals(TokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
return offlineToken;
}
@Test
public void offlineTokenRequest_ClientES256_RealmPS256() throws Exception {
conductOfflineTokenRequest(Algorithm.HS256, Algorithm.ES256, Algorithm.PS256);