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,25 +90,43 @@ 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);
|
||||
entity.setOffline(offlineStr);
|
||||
entity.setUserSessionId(clientSession.getUserSession().getId());
|
||||
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.setTimestamp(clientSession.getTimestamp());
|
||||
entity.setData(model.getData());
|
||||
em.persist(entity);
|
||||
em.flush();
|
||||
|
||||
if (!exists) {
|
||||
em.persist(entity);
|
||||
em.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue