diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java deleted file mode 100644 index b2374bc248..0000000000 --- a/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.keycloak.representations.idm; - -/** - * @author Marek Posolda - */ -public class OfflineClientSessionRepresentation { - - private String clientSessionId; - private String client; // clientId (not DB ID) - private String data; - - public String getClientSessionId() { - return clientSessionId; - } - - public void setClientSessionId(String clientSessionId) { - this.clientSessionId = clientSessionId; - } - - public String getClient() { - return client; - } - - public void setClient(String client) { - this.client = client; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } -} diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java deleted file mode 100644 index e877c3ee7a..0000000000 --- a/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.keycloak.representations.idm; - -import java.util.List; - -/** - * @author Marek Posolda - */ -public class OfflineUserSessionRepresentation { - - private String userSessionId; - private String data; - private List offlineClientSessions; - - public String getUserSessionId() { - return userSessionId; - } - - public void setUserSessionId(String userSessionId) { - this.userSessionId = userSessionId; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } - - public List getOfflineClientSessions() { - return offlineClientSessions; - } - - public void setOfflineClientSessions(List offlineClientSessions) { - this.offlineClientSessions = offlineClientSessions; - } -} diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index ee96b0471f..07865db1a8 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -34,7 +34,6 @@ public class UserRepresentation { protected List realmRoles; protected Map> clientRoles; protected List clientConsents; - protected List offlineUserSessions; @Deprecated protected Map> applicationRoles; @@ -217,12 +216,4 @@ public class UserRepresentation { public void setServiceAccountClientId(String serviceAccountClientId) { this.serviceAccountClientId = serviceAccountClientId; } - - public List getOfflineUserSessions() { - return offlineUserSessions; - } - - public void setOfflineUserSessions(List offlineUserSessions) { - this.offlineUserSessions = offlineUserSessions; - } } diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml index 25a393fd19..650ed769bd 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml @@ -91,6 +91,31 @@ settings. + + Some packages renamed + + We did a bit of restructure and renamed some packages. It is mainly about renaming internal packages of util classes. + The most important classes used in your application ( for example AccessToken or KeycloakSecurityContext ) as well + as the SPI are still unchanged. However there is slight chance that you will be affected and will need to update imports of your classes. + For example if you are using multitenancy and KeycloakConfigResolver, you will be affected as for example class + HttpFacade was moved to different package - it is org.keycloak.adapters.spi.HttpFacade now. + + + + Persisting user sessions + + We added support for offline tokens in this release, which means that we are persisting "offline" user sessions into database now. + If you are not using offline tokens, nothing will be persisted for you, so you don't need to care about worse performance for more DB writes. + However in all cases, you will need to update standalone/configuration/keycloak-server.json and add userSessionPersister + like this: + +"userSessionPersister": { + "provider": "jpa" +}, + + If you want sessions to be persisted in Mongo instead of classic RDBMS, use provider mongo instead. + +
Migrating to 1.5.0.Final diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml b/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml index 78418cbf18..7f8800e8ee 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/timeouts.xml @@ -67,7 +67,12 @@ The difference between classic Refresh token and Offline token is, that offline token will never expire and is not subject of SSO Session Idle timeout . - The offline token is valid even after user logout or server restart. User can revoke the offline tokens in Account management UI. The admin + The offline token is valid even after user logout or server restart. However you need to use offline token for refresh at least once per each 30 days ( + The value can be changed in admin console. It is Offline Session Idle timeout ). Also if you enable option Revoke refresh tokens + , then each offline token can be used just once. So after refresh, you always need to store new offline token from refresh response into your DB instead of the previous one. + + + User can revoke the offline tokens in Account management UI. The admin user can revoke offline tokens for individual users in admin console (The Consent tab of particular user) and he can see all the offline tokens of all users for particular client application in the settings of the client. Revoking of all offline tokens for particular client is possible by set notBefore policy for the client. diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java index ec791d1a57..8290b3b268 100644 --- a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java +++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java @@ -98,6 +98,10 @@ public class OfflineAccessPortalServlet extends HttpServlet { KeycloakDeployment deployment = getDeployment(req); AccessTokenResponse response = ServerRequest.invokeRefresh(deployment, refreshToken); accessToken = response.getToken(); + + // Uncomment this when you use revokeRefreshToken for realm. In that case each offline token can be used just once. So at this point, you need to + // save new offline token into DB + // RefreshTokenDAO.saveToken(response.getRefreshToken()); } catch (ServerRequest.HttpFailure failure) { return "Failed to refresh token. Status from auth-server request: " + failure.getStatus() + ", Error: " + failure.getError(); } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 6073e57055..30eb0558fe 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -294,28 +294,6 @@ public class ExportUtils { } } -// // Offline sessions -// List offlineSessionReps = new LinkedList<>(); -// Collection offlineSessions = session.users().getOfflineUserSessions(realm, user); -// Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, user); -// -// Map> processed = new HashMap<>(); -// for (PersistentClientSessionModel clsm : offlineClientSessions) { -// String userSessionId = clsm.getUserSessionId(); -// List current = processed.get(userSessionId); -// if (current == null) { -// current = new LinkedList<>(); -// processed.put(userSessionId, current); -// } -// current.add(clsm); -// } -// -// for (PersistentUserSessionModel userSession : offlineSessions) { -// OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId())); -// offlineSessionReps.add(sessionRep); -// } -// userRep.setOfflineUserSessions(offlineSessionReps); - return userRep; } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 732444eab1..5fcb826222 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -32,8 +32,6 @@ import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.idm.OfflineClientSessionRepresentation; -import org.keycloak.representations.idm.OfflineUserSessionRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -517,33 +515,4 @@ public class ModelToRepresentation { return rep; } - public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, PersistentUserSessionModel model, Collection clientSessions) { - OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation(); - rep.setData(model.getData()); - rep.setUserSessionId(model.getUserSessionId()); - - List clientSessionReps = new LinkedList<>(); - for (PersistentClientSessionModel clsm : clientSessions) { - OfflineClientSessionRepresentation clrep = toRepresentation(realm, clsm); - clientSessionReps.add(clrep); - } - rep.setOfflineClientSessions(clientSessionReps); - return rep; - } - - public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, PersistentClientSessionModel model) { - OfflineClientSessionRepresentation rep = new OfflineClientSessionRepresentation(); - - String clientInternalId = model.getClientId(); - ClientModel client = realm.getClientById(clientInternalId); - rep.setClient(client.getClientId()); - - rep.setClientSessionId(model.getClientSessionId()); - rep.setData(model.getData()); - return rep; - } - - - - } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 025ac8a2ab..85b4ac02dc 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -1161,30 +1161,6 @@ public class RepresentationToModel { return consentModel; } - // TODO -// public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) { -// PersistentUserSessionModel model = new PersistentUserSessionModel(); -// model.setUserSessionId(sessionRep.getUserSessionId()); -// model.setData(sessionRep.getData()); -// session.users().createOfflineUserSession(newRealm, user, model); -// -// for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) { -// PersistentClientSessionModel csModel = new PersistentClientSessionModel(); -// String clientId = csRep.getClient(); -// ClientModel client = newRealm.getClientByClientId(clientId); -// if (client == null) { -// throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername()); -// } -// csModel.setClientId(client.getId()); -// csModel.setUserId(user.getId()); -// csModel.setClientSessionId(csRep.getClientSessionId()); -// csModel.setUserSessionId(sessionRep.getUserSessionId()); -// csModel.setData(csRep.getData()); -// -// session.users().createOfflineClientSession(newRealm, csModel); -// } -// } - public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) { AuthenticationFlowModel model = new AuthenticationFlowModel(); model.setBuiltIn(rep.isBuiltIn()); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java index faf3f80556..51a523978b 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java @@ -20,10 +20,10 @@ import javax.persistence.Table; @NamedQuery(name="deleteClientSessionsByRealm", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.realmId=:realmId)"), @NamedQuery(name="deleteClientSessionsByClient", query="delete from PersistentClientSessionEntity sess where sess.clientId=:clientId"), @NamedQuery(name="deleteClientSessionsByUser", query="delete from PersistentClientSessionEntity sess where sess.userSessionId IN (select u.userSessionId from PersistentUserSessionEntity u where u.userId=:userId)"), - @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), + @NamedQuery(name="deleteClientSessionsByUserSession", query="delete from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline=:offline"), @NamedQuery(name="deleteDetachedClientSessions", query="delete from PersistentClientSessionEntity sess where sess.userSessionId NOT IN (select u.userSessionId from PersistentUserSessionEntity u)"), - @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and offline=:offline"), - @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"), + @NamedQuery(name="findClientSessionsByUserSession", query="select sess from PersistentClientSessionEntity sess where sess.userSessionId=:userSessionId and sess.offline=:offline"), + @NamedQuery(name="findClientSessionsByUserSessions", query="select sess from PersistentClientSessionEntity sess where sess.offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"), @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp=:timestamp"), }) @Table(name="OFFLINE_CLIENT_SESSION") diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java index 95745a823a..f56284c899 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java @@ -25,8 +25,8 @@ import org.keycloak.models.jpa.entities.UserEntity; @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId=:realmId"), @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"), @NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"), - @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where offline=:offline"), - @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId"), + @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline=:offline"), + @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where sess.offline=:offline order by sess.userSessionId"), @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh=:lastSessionRefresh"), }) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index bbc840ec86..b2d14b9019 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -327,21 +327,6 @@ public class ImportTest extends AbstractModelTest { Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin"))); Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper)); -// // Test offline sessions -// Collection offlineUserSessions = session.users().getOfflineUserSessions(realm, admin); -// Collection offlineClientSessions = session.users().getOfflineClientSessions(realm, admin); -// Assert.assertEquals(offlineUserSessions.size(), 1); -// Assert.assertEquals(offlineClientSessions.size(), 1); -// PersistentUserSessionModel offlineSession = offlineUserSessions.iterator().next(); -// PersistentClientSessionModel offlineClSession = offlineClientSessions.iterator().next(); -// Assert.assertEquals(offlineSession.getData(), "something1"); -// Assert.assertEquals(offlineSession.getUserSessionId(), "123"); -// Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId()); -// Assert.assertEquals(offlineClSession.getUserSessionId(), "123"); -// Assert.assertEquals(offlineClSession.getUserId(), admin.getId()); -// Assert.assertEquals(offlineClSession.getData(), "something2"); - - // Test service accounts Assert.assertFalse(application.isServiceAccountsEnabled()); Assert.assertTrue(otherApp.isServiceAccountsEnabled()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java index fee7bdee53..258dd3cbd7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java @@ -4,8 +4,6 @@ import org.junit.Assert; import org.junit.Test; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; -import org.keycloak.models.session.PersistentClientSessionModel; -import org.keycloak.models.session.PersistentUserSessionModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; @@ -15,7 +13,6 @@ import static org.junit.Assert.assertNotNull; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -286,83 +283,6 @@ public class UserModelTest extends AbstractModelTest { Assert.assertNull(session.users().getUserByUsername("user1", realm)); } -// @Test -// public void testOfflineSessionsRemoved() { -// RealmModel realm = realmManager.createRealm("original"); -// ClientModel fooClient = realm.addClient("foo"); -// ClientModel barClient = realm.addClient("bar"); -// -// UserModel user1 = session.users().addUser(realm, "user1"); -// UserModel user2 = session.users().addUser(realm, "user2"); -// -// createOfflineUserSession(realm, user1, "123", "something1"); -// createOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2"); -// createOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3"); -// -// createOfflineUserSession(realm, user2, "2123", "something4"); -// createOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5"); -// -// commit(); -// -// // Searching by clients -// Assert.assertEquals(2, session.users().getOfflineSessionsCount(realm, fooClient)); -// Assert.assertEquals(1, session.users().getOfflineSessionsCount(realm, barClient)); -// -// Collection clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10); -// Assert.assertEquals(2, clientSessions.size()); -// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1); -// PersistentClientSessionModel cls = clientSessions.iterator().next(); -// assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2"); -// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1); -// cls = clientSessions.iterator().next(); -// assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5"); -// -// clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10); -// Assert.assertEquals(1, clientSessions.size()); -// cls = clientSessions.iterator().next(); -// assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3"); -// -// realm = realmManager.getRealmByName("original"); -// realm.removeClient(barClient.getId()); -// -// commit(); -// -// realm = realmManager.getRealmByName("original"); -// user1 = session.users().getUserByUsername("user1", realm); -// Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData()); -// Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData()); -// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); -// -// realm.removeClient(fooClient.getId()); -// -// commit(); -// -// realm = realmManager.getRealmByName("original"); -// user1 = session.users().getUserByUsername("user1", realm); -// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456")); -// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789")); -// Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123")); -// Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size()); -// Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size()); -// } -// -// private void createOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) { -// PersistentUserSessionModel model = new PersistentUserSessionModel(); -// model.setUserSessionId(userSessionId); -// model.setData(data); -// session.users().createOfflineUserSession(realm, user, model); -// } -// -// private void createOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) { -// PersistentClientSessionModel model = new PersistentClientSessionModel(); -// model.setClientSessionId(clientSessionId); -// model.setUserSessionId(userSessionId); -// model.setUserId(user.getId()); -// model.setClientId(clientId); -// model.setData(data); -// session.users().createOfflineClientSession(realm, model); -// } - public static void assertEquals(UserModel expected, UserModel actual) { Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); @@ -377,14 +297,5 @@ public class UserModelTest extends AbstractModelTest { Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions); } - private static void assertSessionEquals(PersistentClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId, - String expectedClientId, String expectedUserId, String expectedData) { - Assert.assertEquals(cls.getData(), expectedData); - Assert.assertEquals(cls.getClientSessionId(), expectedClientSessionId); - Assert.assertEquals(cls.getUserSessionId(), expectedUserSessionId); - Assert.assertEquals(cls.getUserId(), expectedUserId); - Assert.assertEquals(cls.getClientId(), expectedClientId); - } - } diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json index 53e7f7ee25..68ec4d3327 100755 --- a/testsuite/integration/src/test/resources/model/testrealm.json +++ b/testsuite/integration/src/test/resources/model/testrealm.json @@ -120,19 +120,6 @@ "openid-connect": [ "gss delegation credential" ] } } - ], - "offlineUserSessions": [ - { - "userSessionId": "123", - "data": "something1", - "offlineClientSessions": [ - { - "clientSessionId": "456", - "client": "OtherApp", - "data": "something2" - } - ] - } ] }, {