SessionResource.java
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.services.resources.account;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.util.Time;
import org.keycloak.device.DeviceActivityManager;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.account.ClientRepresentation;
import org.keycloak.representations.account.DeviceRepresentation;
import org.keycloak.representations.account.SessionRepresentation;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class SessionResource {
private final KeycloakSession session;
private final Auth auth;
private final RealmModel realm;
private final UserModel user;
public SessionResource(KeycloakSession session, Auth auth) {
this.session = session;
this.auth = auth;
this.realm = auth.getRealm();
this.user = auth.getUser();
}
/**
* Get session information.
*
* @return
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Stream<SessionRepresentation> toRepresentation() {
return session.sessions().getUserSessionsStream(realm, user).map(this::toRepresentation);
}
/**
* Get device activity information based on the active sessions.
*
* @return
*/
@Path("devices")
@GET
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Collection<DeviceRepresentation> devices() {
Map<String, DeviceRepresentation> reps = new HashMap<>();
session.sessions().getUserSessionsStream(realm, user).forEach(s -> {
DeviceRepresentation device = getAttachedDevice(s);
DeviceRepresentation rep = reps
.computeIfAbsent(device.getOs() + device.getOsVersion(), key -> {
DeviceRepresentation representation = new DeviceRepresentation();
representation.setLastAccess(device.getLastAccess());
representation.setOs(device.getOs());
representation.setOsVersion(device.getOsVersion());
representation.setDevice(device.getDevice());
representation.setMobile(device.isMobile());
return representation;
});
if (isCurrentSession(s)) {
rep.setCurrent(true);
}
if (rep.getLastAccess() == 0 || rep.getLastAccess() < s.getLastSessionRefresh()) {
rep.setLastAccess(s.getLastSessionRefresh());
}
rep.addSession(createSessionRepresentation(s, device));
});
return reps.values();
}
/**
* Remove sessions
*
* @param removeCurrent remove current session (default is false)
* @return
*/
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response logout(@QueryParam("current") boolean removeCurrent) {
auth.require(AccountRoles.MANAGE_ACCOUNT);
session.sessions().getUserSessionsStream(realm, user).filter(s -> removeCurrent || !isCurrentSession(s))
.collect(Collectors.toList()) // collect to avoid concurrent modification as backchannelLogout removes the user sessions.
.forEach(s -> AuthenticationManager.backchannelLogout(session, s, true));
return Response.noContent().build();
}
/**
* Remove a specific session
*
* @param id a specific session to remove
* @return
*/
@Path("/{id}")
@DELETE
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response logout(@PathParam("id") String id) {
auth.require(AccountRoles.MANAGE_ACCOUNT);
UserSessionModel userSession = lockUserSessionsForModification(session, () -> session.sessions().getUserSession(realm, id));
if (userSession != null && userSession.getUser().equals(user)) {
AuthenticationManager.backchannelLogout(session, userSession, true);
}
return Response.noContent().build();
}
private SessionRepresentation createSessionRepresentation(UserSessionModel s, DeviceRepresentation device) {
SessionRepresentation sessionRep = new SessionRepresentation();
sessionRep.setId(s.getId());
sessionRep.setIpAddress(s.getIpAddress());
sessionRep.setStarted(s.getStarted());
sessionRep.setLastAccess(s.getLastSessionRefresh());
int maxLifespan = s.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0
? realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan();
int expires = s.getStarted() + maxLifespan;
sessionRep.setExpires(expires);
sessionRep.setBrowser(device.getBrowser());
if (isCurrentSession(s)) {
sessionRep.setCurrent(true);
}
sessionRep.setClients(new LinkedList());
for (String clientUUID : s.getAuthenticatedClientSessions().keySet()) {
ClientModel client = realm.getClientById(clientUUID);
ClientRepresentation clientRep = new ClientRepresentation();
clientRep.setClientId(client.getClientId());
clientRep.setClientName(client.getName());
sessionRep.getClients().add(clientRep);
}
return sessionRep;
}
private DeviceRepresentation getAttachedDevice(UserSessionModel s) {
DeviceRepresentation device = DeviceActivityManager.getCurrentDevice(s);
if (device == null) {
device = DeviceRepresentation.unknown();
device.setIpAddress(s.getIpAddress());
}
return device;
}
private boolean isCurrentSession(UserSessionModel session) {
if (auth.getSession() == null) return false;
return session.getId().equals(auth.getSession().getId());
}
private SessionRepresentation toRepresentation(UserSessionModel s) {
return createSessionRepresentation(s, getAttachedDevice(s));
}
}