Merge pull request #1751 from mposolda/master

KEYCLOAK-1972 docs and export/import fixes for offline tokens. DB fixes
This commit is contained in:
Marek Posolda 2015-10-16 19:58:41 +02:00
commit 2325143803
14 changed files with 40 additions and 281 deletions

View file

@ -1,35 +0,0 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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;
}
}

View file

@ -1,37 +0,0 @@
package org.keycloak.representations.idm;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionRepresentation {
private String userSessionId;
private String data;
private List<OfflineClientSessionRepresentation> 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<OfflineClientSessionRepresentation> getOfflineClientSessions() {
return offlineClientSessions;
}
public void setOfflineClientSessions(List<OfflineClientSessionRepresentation> offlineClientSessions) {
this.offlineClientSessions = offlineClientSessions;
}
}

View file

@ -34,7 +34,6 @@ public class UserRepresentation {
protected List<String> realmRoles; protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles; protected Map<String, List<String>> clientRoles;
protected List<UserConsentRepresentation> clientConsents; protected List<UserConsentRepresentation> clientConsents;
protected List<OfflineUserSessionRepresentation> offlineUserSessions;
@Deprecated @Deprecated
protected Map<String, List<String>> applicationRoles; protected Map<String, List<String>> applicationRoles;
@ -217,12 +216,4 @@ public class UserRepresentation {
public void setServiceAccountClientId(String serviceAccountClientId) { public void setServiceAccountClientId(String serviceAccountClientId) {
this.serviceAccountClientId = serviceAccountClientId; this.serviceAccountClientId = serviceAccountClientId;
} }
public List<OfflineUserSessionRepresentation> getOfflineUserSessions() {
return offlineUserSessions;
}
public void setOfflineUserSessions(List<OfflineUserSessionRepresentation> offlineUserSessions) {
this.offlineUserSessions = offlineUserSessions;
}
} }

View file

@ -91,6 +91,31 @@
settings. settings.
</para> </para>
</simplesect> </simplesect>
<simplesect>
<title>Some packages renamed</title>
<para>
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 <literal>org.keycloak.adapters.spi.HttpFacade</literal> now.
</para>
</simplesect>
<simplesect>
<title>Persisting user sessions</title>
<para>
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 <literal>standalone/configuration/keycloak-server.json</literal> and add <literal>userSessionPersister</literal>
like this:
<programlisting>
"userSessionPersister": {
"provider": "jpa"
},
</programlisting>
If you want sessions to be persisted in Mongo instead of classic RDBMS, use provider <literal>mongo</literal> instead.
</para>
</simplesect>
</section> </section>
<section> <section>
<title>Migrating to 1.5.0.Final</title> <title>Migrating to 1.5.0.Final</title>

View file

@ -67,7 +67,12 @@
</para> </para>
<para> <para>
The difference between classic Refresh token and Offline token is, that offline token will never expire and is not subject of <literal>SSO Session Idle timeout</literal> . The difference between classic Refresh token and Offline token is, that offline token will never expire and is not subject of <literal>SSO Session Idle timeout</literal> .
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 <literal>Offline Session Idle timeout</literal> ). Also if you enable option <literal>Revoke refresh tokens</literal>
, 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.
</para>
<para>
User can revoke the offline tokens in Account management UI. The admin
user can revoke offline tokens for individual users in admin console (The <literal>Consent</literal> tab of particular user) and he can user can revoke offline tokens for individual users in admin console (The <literal>Consent</literal> 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 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 <literal>notBefore</literal> policy for the client. client is possible by set <literal>notBefore</literal> policy for the client.

View file

@ -98,6 +98,10 @@ public class OfflineAccessPortalServlet extends HttpServlet {
KeycloakDeployment deployment = getDeployment(req); KeycloakDeployment deployment = getDeployment(req);
AccessTokenResponse response = ServerRequest.invokeRefresh(deployment, refreshToken); AccessTokenResponse response = ServerRequest.invokeRefresh(deployment, refreshToken);
accessToken = response.getToken(); 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) { } catch (ServerRequest.HttpFailure failure) {
return "Failed to refresh token. Status from auth-server request: " + failure.getStatus() + ", Error: " + failure.getError(); return "Failed to refresh token. Status from auth-server request: " + failure.getStatus() + ", Error: " + failure.getError();
} }

View file

@ -294,28 +294,6 @@ public class ExportUtils {
} }
} }
// // Offline sessions
// List<OfflineUserSessionRepresentation> offlineSessionReps = new LinkedList<>();
// Collection<PersistentUserSessionModel> offlineSessions = session.users().getOfflineUserSessions(realm, user);
// Collection<PersistentClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, user);
//
// Map<String, List<PersistentClientSessionModel>> processed = new HashMap<>();
// for (PersistentClientSessionModel clsm : offlineClientSessions) {
// String userSessionId = clsm.getUserSessionId();
// List<PersistentClientSessionModel> 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; return userRep;
} }

View file

@ -32,8 +32,6 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation; 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.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
@ -517,33 +515,4 @@ public class ModelToRepresentation {
return rep; return rep;
} }
public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, PersistentUserSessionModel model, Collection<PersistentClientSessionModel> clientSessions) {
OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation();
rep.setData(model.getData());
rep.setUserSessionId(model.getUserSessionId());
List<OfflineClientSessionRepresentation> 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;
}
} }

View file

@ -1161,30 +1161,6 @@ public class RepresentationToModel {
return consentModel; 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) { public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) {
AuthenticationFlowModel model = new AuthenticationFlowModel(); AuthenticationFlowModel model = new AuthenticationFlowModel();
model.setBuiltIn(rep.isBuiltIn()); model.setBuiltIn(rep.isBuiltIn());

View file

@ -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="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="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="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="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="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 offline=:offline and sess.userSessionId IN (:userSessionIds) order by sess.userSessionId"), @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"), @NamedQuery(name="updateClientSessionsTimestamps", query="update PersistentClientSessionEntity c set timestamp=:timestamp"),
}) })
@Table(name="OFFLINE_CLIENT_SESSION") @Table(name="OFFLINE_CLIENT_SESSION")

View file

@ -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="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId=:realmId"),
@NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"), @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="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="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where sess.offline=:offline"),
@NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId"), @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"), @NamedQuery(name="updateUserSessionsTimestamps", query="update PersistentUserSessionEntity c set lastSessionRefresh=:lastSessionRefresh"),
}) })

View file

@ -327,21 +327,6 @@ public class ImportTest extends AbstractModelTest {
Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin"))); Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper)); Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
// // Test offline sessions
// Collection<PersistentUserSessionModel> offlineUserSessions = session.users().getOfflineUserSessions(realm, admin);
// Collection<PersistentClientSessionModel> 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 // Test service accounts
Assert.assertFalse(application.isServiceAccountsEnabled()); Assert.assertFalse(application.isServiceAccountsEnabled());
Assert.assertTrue(otherApp.isServiceAccountsEnabled()); Assert.assertTrue(otherApp.isServiceAccountsEnabled());

View file

@ -4,8 +4,6 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; 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.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
@ -15,7 +13,6 @@ import static org.junit.Assert.assertNotNull;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -286,83 +283,6 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertNull(session.users().getUserByUsername("user1", realm)); 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<PersistentClientSessionModel> 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) { public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getUsername(), actual.getUsername());
Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
@ -377,14 +297,5 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions); 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);
}
} }

View file

@ -120,19 +120,6 @@
"openid-connect": [ "gss delegation credential" ] "openid-connect": [ "gss delegation credential" ]
} }
} }
],
"offlineUserSessions": [
{
"userSessionId": "123",
"data": "something1",
"offlineClientSessions": [
{
"clientSessionId": "456",
"client": "OtherApp",
"data": "something2"
}
]
}
] ]
}, },
{ {