offline token issuance can cause violation of PRIMARY KEY constraint CONSTRAINT_OFFL_CL_SES_PK3 (#14658)
closes #13706
This commit is contained in:
parent
390c7485c7
commit
fb24c86a3b
2 changed files with 90 additions and 13 deletions
|
@ -90,26 +90,44 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
|
||||||
PersistentAuthenticatedClientSessionAdapter adapter = new PersistentAuthenticatedClientSessionAdapter(session, clientSession);
|
PersistentAuthenticatedClientSessionAdapter adapter = new PersistentAuthenticatedClientSessionAdapter(session, clientSession);
|
||||||
PersistentClientSessionModel model = adapter.getUpdatedModel();
|
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());
|
StorageId clientStorageId = new StorageId(clientSession.getClient().getId());
|
||||||
if (clientStorageId.isLocal()) {
|
if (clientStorageId.isLocal()) {
|
||||||
entity.setClientId(clientStorageId.getId());
|
clientId = clientStorageId.getId();
|
||||||
entity.setClientStorageProvider(PersistentClientSessionEntity.LOCAL);
|
clientStorageProvider = PersistentClientSessionEntity.LOCAL;
|
||||||
entity.setExternalClientId(PersistentClientSessionEntity.LOCAL);
|
externalClientId = PersistentClientSessionEntity.LOCAL;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
entity.setClientId(PersistentClientSessionEntity.EXTERNAL);
|
clientId = PersistentClientSessionEntity.EXTERNAL;
|
||||||
entity.setClientStorageProvider(clientStorageId.getProviderId());
|
clientStorageProvider = clientStorageId.getProviderId();
|
||||||
entity.setExternalClientId(clientStorageId.getExternalId());
|
externalClientId = clientStorageId.getExternalId();
|
||||||
}
|
}
|
||||||
entity.setTimestamp(clientSession.getTimestamp());
|
|
||||||
String offlineStr = offlineToString(offline);
|
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.setOffline(offlineStr);
|
||||||
entity.setUserSessionId(clientSession.getUserSession().getId());
|
}
|
||||||
|
|
||||||
|
entity.setTimestamp(clientSession.getTimestamp());
|
||||||
entity.setData(model.getData());
|
entity.setData(model.getData());
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
em.persist(entity);
|
em.persist(entity);
|
||||||
em.flush();
|
em.flush();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeUserSession(String userSessionId, boolean offline) {
|
public void removeUserSession(String userSessionId, boolean offline) {
|
||||||
|
|
|
@ -57,6 +57,7 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
||||||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||||
import org.keycloak.testsuite.pages.AccountApplicationsPage;
|
import org.keycloak.testsuite.pages.AccountApplicationsPage;
|
||||||
import org.keycloak.testsuite.pages.LoginPage;
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
|
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.ClientBuilder;
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
import org.keycloak.testsuite.util.ClientManager;
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
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);
|
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
|
@Test
|
||||||
public void offlineTokenRequest_ClientES256_RealmPS256() throws Exception {
|
public void offlineTokenRequest_ClientES256_RealmPS256() throws Exception {
|
||||||
conductOfflineTokenRequest(Algorithm.HS256, Algorithm.ES256, Algorithm.PS256);
|
conductOfflineTokenRequest(Algorithm.HS256, Algorithm.ES256, Algorithm.PS256);
|
||||||
|
|
Loading…
Reference in a new issue