KEYCLOAK-904 Offline tokens storage changes. Added UserSessionPersisterProvider . offline sessions preloaded to cache at startup
This commit is contained in:
1 parent
6aa029784a
commit
f92fe6bea9
93 files changed
+4397
-1945
No files matched your search
@@ -32,7 +32,7 @@ import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.offline.OfflineTokenUtils;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
@@ -98,7 +98,7 @@ public class TokenManager {
|
||||
ClientSessionModel clientSession = null;
|
||||
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
|
||||
|
||||
clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState());
|
||||
clientSession = new UserSessionManager(session).findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState());
|
||||
if (clientSession != null) {
|
||||
userSession = clientSession.getUserSession();
|
||||
}
|
||||
@@ -490,14 +490,15 @@ public class TokenManager {
|
||||
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
boolean offlineTokenRequested = TokenUtil.isOfflineTokenRequested(scopeParam);
|
||||
if (offlineTokenRequested) {
|
||||
if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) {
|
||||
UserSessionManager sessionManager = new UserSessionManager(session);
|
||||
if (!sessionManager.isOfflineTokenAllowed(clientSession)) {
|
||||
event.error(Errors.NOT_ALLOWED);
|
||||
throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
refreshToken = new RefreshToken(accessToken);
|
||||
refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
|
||||
OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession);
|
||||
sessionManager.persistOfflineSession(clientSession, userSession);
|
||||
} else {
|
||||
refreshToken = new RefreshToken(accessToken);
|
||||
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
|
||||
@@ -54,6 +55,11 @@ public class ClientManager {
|
||||
sessions.onClientRemoved(realm, client);
|
||||
}
|
||||
|
||||
UserSessionPersisterProvider sessionsPersister = realmManager.getSession().getProvider(UserSessionPersisterProvider.class);
|
||||
if (sessionsPersister != null) {
|
||||
sessionsPersister.onClientRemoved(realm, client);
|
||||
}
|
||||
|
||||
UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client);
|
||||
if (serviceAccountUser != null) {
|
||||
realmManager.getSession().users().removeUser(realm, serviceAccountUser);
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.keycloak.services.managers;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.enums.SslRequired;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.utils.RealmImporter;
|
||||
import org.keycloak.models.AccountRoles;
|
||||
import org.keycloak.models.AdminRoles;
|
||||
@@ -197,6 +198,11 @@ public class RealmManager implements RealmImporter {
|
||||
sessions.onRealmRemoved(realm);
|
||||
}
|
||||
|
||||
UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
if (sessionsPersister != null) {
|
||||
sessionsPersister.onRealmRemoved(realm);
|
||||
}
|
||||
|
||||
// Remove all periodic syncs for configured federation providers
|
||||
UsersSyncManager usersSyncManager = new UsersSyncManager();
|
||||
for (final UserFederationProviderModel fedProvider : federationProviders) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionProvider;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
@@ -21,6 +22,12 @@ public class UserManager {
|
||||
if (sessions != null) {
|
||||
sessions.onUserRemoved(realm, user);
|
||||
}
|
||||
|
||||
UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
if (sessionsPersister != null) {
|
||||
sessionsPersister.onUserRemoved(realm, user);
|
||||
}
|
||||
|
||||
if (session.users().removeUser(realm, user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.keycloak.services.managers;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserSessionManager {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(UserSessionManager.class);
|
||||
|
||||
private final KeycloakSession kcSession;
|
||||
private final UserSessionPersisterProvider persister;
|
||||
|
||||
public UserSessionManager(KeycloakSession session) {
|
||||
this.kcSession = session;
|
||||
this.persister = session.getProvider(UserSessionPersisterProvider.class);
|
||||
}
|
||||
|
||||
public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
UserModel user = userSession.getUser();
|
||||
|
||||
// Verify if we already have UserSession with this ID. If yes, don't create another one
|
||||
UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId());
|
||||
if (offlineUserSession == null) {
|
||||
offlineUserSession = createOfflineUserSession(user, userSession);
|
||||
}
|
||||
|
||||
// Create clientSession and save to DB.
|
||||
createOfflineClientSession(user, clientSession, offlineUserSession);
|
||||
}
|
||||
|
||||
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
|
||||
public ClientSessionModel findOfflineClientSession(RealmModel realm, String clientSessionId, String userSessionId) {
|
||||
ClientSessionModel clientSession = kcSession.sessions().getOfflineClientSession(realm, clientSessionId);
|
||||
if (clientSession == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!userSessionId.equals(clientSession.getUserSession().getId())) {
|
||||
throw new ModelException("User session don't match. Offline client session " + clientSession.getId() + ", It's user session " + clientSession.getUserSession().getId() +
|
||||
" Wanted user session: " + userSessionId);
|
||||
}
|
||||
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
|
||||
List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
|
||||
Set<ClientModel> clients = new HashSet<>();
|
||||
for (ClientSessionModel clientSession : clientSessions) {
|
||||
clients.add(clientSession.getClient());
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
|
||||
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
|
||||
RealmModel realm = client.getRealm();
|
||||
|
||||
List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
|
||||
boolean anyRemoved = false;
|
||||
for (ClientSessionModel clientSession : clientSessions) {
|
||||
if (clientSession.getClient().getId().equals(client.getId())) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
|
||||
user.getUsername(), client.getClientId(), clientSession.getId());
|
||||
}
|
||||
|
||||
kcSession.sessions().removeOfflineClientSession(realm, clientSession.getId());
|
||||
persister.removeClientSession(clientSession.getId(), true);
|
||||
checkOfflineUserSessionHasClientSessions(realm, user, clientSession.getUserSession().getId(), clientSessions);
|
||||
anyRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
return anyRemoved;
|
||||
}
|
||||
|
||||
public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) {
|
||||
RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||
if (offlineAccessRole == null) {
|
||||
logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientSession.getRoles().contains(offlineAccessRole.getId());
|
||||
}
|
||||
|
||||
private UserSessionModel createOfflineUserSession(UserModel user, UserSessionModel userSession) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
|
||||
}
|
||||
|
||||
UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession);
|
||||
persister.createUserSession(userSession, true);
|
||||
return offlineUserSession;
|
||||
}
|
||||
|
||||
private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
|
||||
clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
|
||||
}
|
||||
|
||||
ClientSessionModel offlineClientSession = kcSession.sessions().createOfflineClientSession(clientSession);
|
||||
offlineClientSession.setUserSession(userSession);
|
||||
persister.createClientSession(clientSession, true);
|
||||
}
|
||||
|
||||
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
|
||||
private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, String userSessionId, List<ClientSessionModel> clientSessions) {
|
||||
for (ClientSessionModel clientSession : clientSessions) {
|
||||
if (clientSession.getUserSession().getId().equals(userSessionId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
|
||||
}
|
||||
kcSession.sessions().removeOfflineUserSession(realm, userSessionId);
|
||||
persister.removeUserSession(userSessionId, true);
|
||||
}
|
||||
}
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
package org.keycloak.services.offline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonProperty;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OfflineClientSessionModel;
|
||||
import org.keycloak.models.OfflineUserSessionModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OfflineClientSessionAdapter implements ClientSessionModel {
|
||||
|
||||
private final OfflineClientSessionModel model;
|
||||
private final RealmModel realm;
|
||||
private final ClientModel client;
|
||||
private final OfflineUserSessionAdapter userSession;
|
||||
|
||||
private OfflineClientSessionData data;
|
||||
|
||||
public OfflineClientSessionAdapter(OfflineClientSessionModel model, RealmModel realm, ClientModel client, OfflineUserSessionAdapter userSession) {
|
||||
this.model = model;
|
||||
this.realm = realm;
|
||||
this.client = client;
|
||||
this.userSession = userSession;
|
||||
}
|
||||
|
||||
// lazily init representation
|
||||
private OfflineClientSessionData getData() {
|
||||
if (data == null) {
|
||||
try {
|
||||
data = JsonSerialization.readValue(model.getData(), OfflineClientSessionData.class);
|
||||
} catch (IOException ioe) {
|
||||
throw new ModelException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return model.getClientSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserSessionModel getUserSession() {
|
||||
return userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserSession(UserSessionModel userSession) {
|
||||
throw new IllegalStateException("Not supported setUserSession");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRedirectUri() {
|
||||
return getData().getRedirectUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRedirectUri(String uri) {
|
||||
throw new IllegalStateException("Not supported setRedirectUri");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimestamp() {
|
||||
return getData().getTimestamp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimestamp(int timestamp) {
|
||||
throw new IllegalStateException("Not supported setTimestamp");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAction(String action) {
|
||||
throw new IllegalStateException("Not supported setAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRoles() {
|
||||
return getData().getRoles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRoles(Set<String> roles) {
|
||||
throw new IllegalStateException("Not supported setRoles");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getProtocolMappers() {
|
||||
return getData().getProtocolMappers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProtocolMappers(Set<String> protocolMappers) {
|
||||
throw new IllegalStateException("Not supported setProtocolMappers");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, ExecutionStatus> getExecutionStatus() {
|
||||
return getData().getAuthenticatorStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
|
||||
throw new IllegalStateException("Not supported setExecutionStatus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearExecutionStatus() {
|
||||
throw new IllegalStateException("Not supported clearExecutionStatus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getAuthenticatedUser() {
|
||||
return userSession.getUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticatedUser(UserModel user) {
|
||||
throw new IllegalStateException("Not supported setAuthenticatedUser");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthMethod() {
|
||||
return getData().getAuthMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthMethod(String method) {
|
||||
throw new IllegalStateException("Not supported setAuthMethod");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote(String name) {
|
||||
return getData().getNotes()==null ? null : getData().getNotes().get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNote(String name, String value) {
|
||||
throw new IllegalStateException("Not supported setNote");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNote(String name) {
|
||||
throw new IllegalStateException("Not supported removeNote");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getNotes() {
|
||||
return getData().getNotes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getRequiredActions() {
|
||||
throw new IllegalStateException("Not supported getRequiredActions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(String action) {
|
||||
throw new IllegalStateException("Not supported addRequiredAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(String action) {
|
||||
throw new IllegalStateException("Not supported removeRequiredAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addRequiredAction(UserModel.RequiredAction action) {
|
||||
throw new IllegalStateException("Not supported addRequiredAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeRequiredAction(UserModel.RequiredAction action) {
|
||||
throw new IllegalStateException("Not supported removeRequiredAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserSessionNote(String name, String value) {
|
||||
throw new IllegalStateException("Not supported setUserSessionNote");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getUserSessionNotes() {
|
||||
throw new IllegalStateException("Not supported getUserSessionNotes");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearUserSessionNotes() {
|
||||
throw new IllegalStateException("Not supported clearUserSessionNotes");
|
||||
}
|
||||
|
||||
protected static class OfflineClientSessionData {
|
||||
|
||||
@JsonProperty("authMethod")
|
||||
private String authMethod;
|
||||
|
||||
@JsonProperty("redirectUri")
|
||||
private String redirectUri;
|
||||
|
||||
@JsonProperty("protocolMappers")
|
||||
private Set<String> protocolMappers;
|
||||
|
||||
@JsonProperty("roles")
|
||||
private Set<String> roles;
|
||||
|
||||
@JsonProperty("notes")
|
||||
private Map<String, String> notes;
|
||||
|
||||
@JsonProperty("authenticatorStatus")
|
||||
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
|
||||
|
||||
@JsonProperty("timestamp")
|
||||
private int timestamp;
|
||||
|
||||
public String getAuthMethod() {
|
||||
return authMethod;
|
||||
}
|
||||
|
||||
public void setAuthMethod(String authMethod) {
|
||||
this.authMethod = authMethod;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return redirectUri;
|
||||
}
|
||||
|
||||
public void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public Set<String> getProtocolMappers() {
|
||||
return protocolMappers;
|
||||
}
|
||||
|
||||
public void setProtocolMappers(Set<String> protocolMappers) {
|
||||
this.protocolMappers = protocolMappers;
|
||||
}
|
||||
|
||||
public Set<String> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(Set<String> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public Map<String, String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(Map<String, String> notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
|
||||
return authenticatorStatus;
|
||||
}
|
||||
|
||||
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
|
||||
this.authenticatorStatus = authenticatorStatus;
|
||||
}
|
||||
|
||||
public int getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(int timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
package org.keycloak.services.offline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OfflineClientSessionModel;
|
||||
import org.keycloak.models.OfflineUserSessionModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.util.Time;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OfflineTokenUtils {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class);
|
||||
|
||||
public static void persistOfflineSession(KeycloakSession kcSession, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
UserModel user = userSession.getUser();
|
||||
ClientModel client = clientSession.getClient();
|
||||
|
||||
// First verify if we already have offlineToken for this user+client . If yes, then invalidate it (This is to avoid leaks)
|
||||
Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
|
||||
for (OfflineClientSessionModel existing : clientSessions) {
|
||||
if (existing.getClientId().equals(client.getId())) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' . Offline token will be replaced with new one",
|
||||
user.getUsername(), client.getClientId(), existing.getClientSessionId());
|
||||
}
|
||||
|
||||
kcSession.users().removeOfflineClientSession(realm, user, existing.getClientSessionId());
|
||||
|
||||
// Check if userSession is ours. If not, then check if it has other clientSessions and remove it otherwise
|
||||
if (!existing.getUserSessionId().equals(userSession.getId())) {
|
||||
checkUserSessionHasClientSessions(kcSession, realm, user, existing.getUserSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify if we already have UserSession with this ID. If yes, don't create another one
|
||||
OfflineUserSessionModel userSessionRep = kcSession.users().getOfflineUserSession(realm, user, userSession.getId());
|
||||
if (userSessionRep == null) {
|
||||
createOfflineUserSession(kcSession, realm, user, userSession);
|
||||
}
|
||||
|
||||
// Create clientRep and save to DB.
|
||||
createOfflineClientSession(kcSession, realm, user, clientSession, userSession);
|
||||
}
|
||||
|
||||
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
|
||||
public static ClientSessionModel findOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
|
||||
OfflineClientSessionModel clientSession = kcSession.users().getOfflineClientSession(realm, user, clientSessionId);
|
||||
if (clientSession == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!userSessionId.equals(clientSession.getUserSessionId())) {
|
||||
throw new ModelException("User session don't match. Offline client session " + clientSession.getClientSessionId() + ", It's user session " + clientSession.getUserSessionId() +
|
||||
" Wanted user session: " + userSessionId);
|
||||
}
|
||||
|
||||
OfflineUserSessionModel userSession = kcSession.users().getOfflineUserSession(realm, user, userSessionId);
|
||||
if (userSession == null) {
|
||||
throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId);
|
||||
}
|
||||
|
||||
OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user);
|
||||
|
||||
ClientModel client = realm.getClientById(clientSession.getClientId());
|
||||
return new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
|
||||
}
|
||||
|
||||
public static Set<ClientModel> findClientsWithOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user) {
|
||||
Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
|
||||
Set<ClientModel> clients = new HashSet<>();
|
||||
for (OfflineClientSessionModel clientSession : clientSessions) {
|
||||
ClientModel client = realm.getClientById(clientSession.getClientId());
|
||||
clients.add(client);
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
|
||||
public static boolean revokeOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientModel client) {
|
||||
Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
|
||||
boolean anyRemoved = false;
|
||||
for (OfflineClientSessionModel clientSession : clientSessions) {
|
||||
if (clientSession.getClientId().equals(client.getId())) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
|
||||
user.getUsername(), client.getClientId(), clientSession.getClientSessionId());
|
||||
}
|
||||
|
||||
kcSession.users().removeOfflineClientSession(realm, user, clientSession.getClientSessionId());
|
||||
checkUserSessionHasClientSessions(kcSession, realm, user, clientSession.getUserSessionId());
|
||||
anyRemoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
return anyRemoved;
|
||||
}
|
||||
|
||||
public static boolean isOfflineTokenAllowed(RealmModel realm, ClientSessionModel clientSession) {
|
||||
RoleModel offlineAccessRole = realm.getRole(Constants.OFFLINE_ACCESS_ROLE);
|
||||
if (offlineAccessRole == null) {
|
||||
logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE);
|
||||
return false;
|
||||
}
|
||||
|
||||
return clientSession.getRoles().contains(offlineAccessRole.getId());
|
||||
}
|
||||
|
||||
private static void createOfflineUserSession(KeycloakSession kcSession, RealmModel realm, UserModel user, UserSessionModel userSession) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
|
||||
}
|
||||
OfflineUserSessionAdapter.OfflineUserSessionData rep = new OfflineUserSessionAdapter.OfflineUserSessionData();
|
||||
rep.setBrokerUserId(userSession.getBrokerUserId());
|
||||
rep.setBrokerSessionId(userSession.getBrokerSessionId());
|
||||
rep.setIpAddress(userSession.getIpAddress());
|
||||
rep.setAuthMethod(userSession.getAuthMethod());
|
||||
rep.setRememberMe(userSession.isRememberMe());
|
||||
rep.setStarted(userSession.getStarted());
|
||||
rep.setNotes(userSession.getNotes());
|
||||
|
||||
try {
|
||||
String stringRep = JsonSerialization.writeValueAsString(rep);
|
||||
OfflineUserSessionModel sessionModel = new OfflineUserSessionModel();
|
||||
sessionModel.setUserSessionId(userSession.getId());
|
||||
sessionModel.setData(stringRep);
|
||||
kcSession.users().addOfflineUserSession(realm, user, sessionModel);
|
||||
} catch (IOException ioe) {
|
||||
throw new ModelException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
|
||||
clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
|
||||
}
|
||||
OfflineClientSessionAdapter.OfflineClientSessionData rep = new OfflineClientSessionAdapter.OfflineClientSessionData();
|
||||
rep.setAuthMethod(clientSession.getAuthMethod());
|
||||
rep.setRedirectUri(clientSession.getRedirectUri());
|
||||
rep.setProtocolMappers(clientSession.getProtocolMappers());
|
||||
rep.setRoles(clientSession.getRoles());
|
||||
rep.setNotes(clientSession.getNotes());
|
||||
rep.setAuthenticatorStatus(clientSession.getExecutionStatus());
|
||||
rep.setTimestamp(Time.currentTime());
|
||||
|
||||
try {
|
||||
String stringRep = JsonSerialization.writeValueAsString(rep);
|
||||
OfflineClientSessionModel clsModel = new OfflineClientSessionModel();
|
||||
clsModel.setClientSessionId(clientSession.getId());
|
||||
clsModel.setClientId(clientSession.getClient().getId());
|
||||
clsModel.setUserId(user.getId());
|
||||
clsModel.setUserSessionId(userSession.getId());
|
||||
clsModel.setData(stringRep);
|
||||
kcSession.users().addOfflineClientSession(realm, clsModel);
|
||||
} catch (IOException ioe) {
|
||||
throw new ModelException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
|
||||
private static void checkUserSessionHasClientSessions(KeycloakSession kcSession, RealmModel realm, UserModel user, String userSessionId) {
|
||||
Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
|
||||
|
||||
for (OfflineClientSessionModel clientSession : clientSessions) {
|
||||
if (clientSession.getUserSessionId().equals(userSessionId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
|
||||
}
|
||||
kcSession.users().removeOfflineUserSession(realm, user, userSessionId);
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package org.keycloak.services.offline;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.codehaus.jackson.annotate.JsonProperty;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.models.OfflineUserSessionModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class OfflineUserSessionAdapter implements UserSessionModel {
|
||||
|
||||
private final OfflineUserSessionModel model;
|
||||
private final UserModel user;
|
||||
|
||||
private OfflineUserSessionData data;
|
||||
|
||||
public OfflineUserSessionAdapter(OfflineUserSessionModel model, UserModel user) {
|
||||
this.model = model;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
// lazily init representation
|
||||
private OfflineUserSessionData getData() {
|
||||
if (data == null) {
|
||||
try {
|
||||
data = JsonSerialization.readValue(model.getData(), OfflineUserSessionData.class);
|
||||
} catch (IOException ioe) {
|
||||
throw new ModelException(ioe);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return model.getUserSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBrokerSessionId() {
|
||||
return getData().getBrokerSessionId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBrokerUserId() {
|
||||
return getData().getBrokerUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLoginUsername() {
|
||||
return user.getUsername();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIpAddress() {
|
||||
return getData().getIpAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthMethod() {
|
||||
return getData().getAuthMethod();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRememberMe() {
|
||||
return getData().isRememberMe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStarted() {
|
||||
return getData().getStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSessionRefresh() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLastSessionRefresh(int seconds) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientSessionModel> getClientSessions() {
|
||||
throw new IllegalStateException("Not yet supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNote(String name) {
|
||||
return getData().getNotes()==null ? null : getData().getNotes().get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNote(String name, String value) {
|
||||
throw new IllegalStateException("Illegal to set note offline session");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNote(String name) {
|
||||
throw new IllegalStateException("Illegal to remove note from offline session");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getNotes() {
|
||||
return getData().getNotes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setState(State state) {
|
||||
throw new IllegalStateException("Illegal to set state on offline session");
|
||||
}
|
||||
|
||||
|
||||
protected static class OfflineUserSessionData {
|
||||
|
||||
@JsonProperty("brokerSessionId")
|
||||
private String brokerSessionId;
|
||||
|
||||
@JsonProperty("brokerUserId")
|
||||
private String brokerUserId;
|
||||
|
||||
@JsonProperty("ipAddress")
|
||||
private String ipAddress;
|
||||
|
||||
@JsonProperty("authMethod")
|
||||
private String authMethod;
|
||||
|
||||
@JsonProperty("rememberMe")
|
||||
private boolean rememberMe;
|
||||
|
||||
@JsonProperty("started")
|
||||
private int started;
|
||||
|
||||
@JsonProperty("notes")
|
||||
private Map<String, String> notes;
|
||||
|
||||
public String getBrokerSessionId() {
|
||||
return brokerSessionId;
|
||||
}
|
||||
|
||||
public void setBrokerSessionId(String brokerSessionId) {
|
||||
this.brokerSessionId = brokerSessionId;
|
||||
}
|
||||
|
||||
public String getBrokerUserId() {
|
||||
return brokerUserId;
|
||||
}
|
||||
|
||||
public void setBrokerUserId(String brokerUserId) {
|
||||
this.brokerUserId = brokerUserId;
|
||||
}
|
||||
|
||||
public String getIpAddress() {
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
public void setIpAddress(String ipAddress) {
|
||||
this.ipAddress = ipAddress;
|
||||
}
|
||||
|
||||
public String getAuthMethod() {
|
||||
return authMethod;
|
||||
}
|
||||
|
||||
public void setAuthMethod(String authMethod) {
|
||||
this.authMethod = authMethod;
|
||||
}
|
||||
|
||||
public boolean isRememberMe() {
|
||||
return rememberMe;
|
||||
}
|
||||
|
||||
public void setRememberMe(boolean rememberMe) {
|
||||
this.rememberMe = rememberMe;
|
||||
}
|
||||
|
||||
public int getStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
public void setStarted(int started) {
|
||||
this.started = started;
|
||||
}
|
||||
|
||||
public Map<String, String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
public void setNotes(Map<String, String> notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,8 @@ import org.keycloak.services.managers.AppAuthManager;
|
||||
import org.keycloak.services.managers.Auth;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.offline.OfflineTokenUtils;
|
||||
import org.keycloak.services.util.ResolveRelative;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
import org.keycloak.util.UriUtils;
|
||||
@@ -486,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
|
||||
// Revoke grant in UserModel
|
||||
UserModel user = auth.getUser();
|
||||
user.revokeConsentForClient(client.getId());
|
||||
OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
|
||||
new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
|
||||
// Logout clientSessions for this user and client
|
||||
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
|
||||
|
||||
@@ -7,11 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.OfflineClientSessionModel;
|
||||
import org.keycloak.models.OfflineUserSessionModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
@@ -27,8 +24,6 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
|
||||
import org.keycloak.services.managers.ClientManager;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.services.managers.ResourceAdminManager;
|
||||
import org.keycloak.services.offline.OfflineClientSessionAdapter;
|
||||
import org.keycloak.services.offline.OfflineUserSessionAdapter;
|
||||
import org.keycloak.services.resources.KeycloakApplication;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
@@ -413,7 +408,7 @@ public class ClientResource {
|
||||
public Map<String, Integer> getOfflineSessionCount() {
|
||||
auth.requireView();
|
||||
Map<String, Integer> map = new HashMap<String, Integer>();
|
||||
map.put("count", session.users().getOfflineClientSessionsCount(client.getRealm(), client));
|
||||
map.put("count", session.sessions().getOfflineSessionsCount(client.getRealm(), client));
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -435,19 +430,9 @@ public class ClientResource {
|
||||
firstResult = firstResult != null ? firstResult : -1;
|
||||
maxResults = maxResults != null ? maxResults : -1;
|
||||
List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
|
||||
for (OfflineClientSessionModel offlineClientSession : session.users().getOfflineClientSessions(client.getRealm(), client, firstResult, maxResults)) {
|
||||
UserModel user = session.users().getUserById(offlineClientSession.getUserId(), client.getRealm());
|
||||
OfflineUserSessionModel offlineUserSession = session.users().getOfflineUserSession(client.getRealm(), user, offlineClientSession.getUserSessionId());
|
||||
OfflineUserSessionAdapter sessionAdapter = new OfflineUserSessionAdapter(offlineUserSession, user);
|
||||
OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(offlineClientSession, client.getRealm(), client, sessionAdapter);
|
||||
|
||||
UserSessionRepresentation rep = new UserSessionRepresentation();
|
||||
rep.setId(sessionAdapter.getId());
|
||||
rep.setStart(Time.toMillis(clientSessionAdapter.getTimestamp()));
|
||||
rep.setUsername(user.getUsername());
|
||||
rep.setUserId(user.getId());
|
||||
rep.setIpAddress(sessionAdapter.getIpAddress());
|
||||
|
||||
List<UserSessionModel> userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults);
|
||||
for (UserSessionModel userSession : userSessions) {
|
||||
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
|
||||
sessions.add(rep);
|
||||
}
|
||||
return sessions;
|
||||
|
||||
@@ -77,7 +77,7 @@ import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.keycloak.models.UsernameLoginFailureModel;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
import org.keycloak.services.offline.OfflineTokenUtils;
|
||||
import org.keycloak.services.managers.UserSessionManager;
|
||||
import org.keycloak.services.resources.AccountService;
|
||||
|
||||
/**
|
||||
@@ -451,7 +451,7 @@ public class UsersResource {
|
||||
|
||||
List<Map<String, Object>> result = new LinkedList<>();
|
||||
|
||||
Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
|
||||
Set<ClientModel> offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
|
||||
|
||||
for (ClientModel client : realm.getClients()) {
|
||||
UserConsentModel consent = user.getConsentByClient(client.getId());
|
||||
@@ -496,7 +496,7 @@ public class UsersResource {
|
||||
|
||||
ClientModel client = realm.getClientByClientId(clientId);
|
||||
boolean revokedConsent = user.revokeConsentForClient(client.getId());
|
||||
boolean revokedOfflineToken = OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
|
||||
boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||
|
||||
if (revokedConsent) {
|
||||
// Logout clientSessions for this user and client
|
||||
|
||||
Reference in new issue
Block a user