[KEYCLOAK-7416] - Device Activity
This commit is contained in:
parent
69d6613ab6
commit
a1d8850373
19 changed files with 1135 additions and 135 deletions
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
* 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.representations.account;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class DeviceRepresentation {
|
||||||
|
|
||||||
|
public static final String UNKNOWN = "Unknown";
|
||||||
|
private static final String OTHER = "Other";
|
||||||
|
private static final String BROWSER_VERSION_SEPARATOR = "/";
|
||||||
|
|
||||||
|
public static DeviceRepresentation unknown() {
|
||||||
|
DeviceRepresentation device = new DeviceRepresentation();
|
||||||
|
|
||||||
|
device.setOs(OTHER);
|
||||||
|
device.setDevice(OTHER);
|
||||||
|
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String ipAddress;
|
||||||
|
private String os;
|
||||||
|
private String osVersion;
|
||||||
|
private String browser;
|
||||||
|
private String device;
|
||||||
|
private int lastAccess;
|
||||||
|
private Boolean current;
|
||||||
|
private List<SessionRepresentation> sessions;
|
||||||
|
private boolean mobile;
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIpAddress() {
|
||||||
|
return ipAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIpAddress(String ip) {
|
||||||
|
this.ipAddress = ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOs() {
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOs(String os) {
|
||||||
|
this.os = os;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOsVersion() {
|
||||||
|
if (osVersion == null) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
return osVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOsVersion(String osVersion) {
|
||||||
|
this.osVersion = osVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrowser() {
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrowser(String browser) {
|
||||||
|
this.browser = browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBrowser(String browser, String version) {
|
||||||
|
if (browser == null) {
|
||||||
|
this.browser = OTHER;
|
||||||
|
} else {
|
||||||
|
this.browser = new StringBuilder(browser).append(BROWSER_VERSION_SEPARATOR).append(version == null ? UNKNOWN : version).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDevice() {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDevice(String device) {
|
||||||
|
this.device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLastAccess() {
|
||||||
|
return lastAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAccess(int lastAccess) {
|
||||||
|
this.lastAccess = lastAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getCurrent() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrent(Boolean current) {
|
||||||
|
this.current = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSession(SessionRepresentation sessionRep) {
|
||||||
|
if (this.sessions == null) {
|
||||||
|
this.sessions = new ArrayList<>();
|
||||||
|
}
|
||||||
|
this.sessions.add(sessionRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SessionRepresentation> getSessions() {
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMobile(boolean mobile) {
|
||||||
|
this.mobile = mobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMobile() {
|
||||||
|
return mobile;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,8 @@ public class SessionRepresentation {
|
||||||
private int lastAccess;
|
private int lastAccess;
|
||||||
private int expires;
|
private int expires;
|
||||||
private List<ClientRepresentation> clients;
|
private List<ClientRepresentation> clients;
|
||||||
|
private String browser;
|
||||||
|
private Boolean current;
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -61,4 +63,20 @@ public class SessionRepresentation {
|
||||||
public void setClients(List<ClientRepresentation> clients) {
|
public void setClients(List<ClientRepresentation> clients) {
|
||||||
this.clients = clients;
|
this.clients = clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBrowser(String browser) {
|
||||||
|
this.browser = browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBrowser() {
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean getCurrent() {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrent(Boolean current) {
|
||||||
|
this.current = current;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,16 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ua-parser</groupId>
|
||||||
|
<artifactId>uap-java</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>*</groupId>
|
||||||
|
<artifactId>*</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.zxing</groupId>
|
<groupId>com.google.zxing</groupId>
|
||||||
<artifactId>core</artifactId>
|
<artifactId>core</artifactId>
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2016 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.
|
||||||
|
-->
|
||||||
|
<module xmlns="urn:jboss:module:1.3" name="com.github.ua-parser">
|
||||||
|
<resources>
|
||||||
|
<artifact name="${com.github.ua-parser:uap-java}"/>
|
||||||
|
</resources>
|
||||||
|
<dependencies>
|
||||||
|
<module name="org.yaml.snakeyaml"/>
|
||||||
|
</dependencies>
|
||||||
|
</module>
|
|
@ -39,5 +39,6 @@
|
||||||
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
<module name="com.google.guava"/>
|
<module name="com.google.guava"/>
|
||||||
|
<module name="com.github.ua-parser" export="true"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -24,9 +24,9 @@ import org.infinispan.context.Flag;
|
||||||
import org.infinispan.stream.CacheCollectors;
|
import org.infinispan.stream.CacheCollectors;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.common.util.ObjectUtil;
|
|
||||||
import org.keycloak.common.util.Retry;
|
import org.keycloak.common.util.Retry;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.device.DeviceActivityManager;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -36,7 +36,6 @@ import org.keycloak.models.UserLoginFailureModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.UserSessionProvider;
|
import org.keycloak.models.UserSessionProvider;
|
||||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
|
||||||
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
import org.keycloak.models.sessions.infinispan.changes.Tasks;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore;
|
import org.keycloak.models.sessions.infinispan.changes.sessions.CrossDCLastSessionRefreshStore;
|
||||||
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
|
import org.keycloak.models.sessions.infinispan.changes.sessions.PersisterLastSessionRefreshStore;
|
||||||
|
@ -75,7 +74,6 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -218,7 +216,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
||||||
SessionUpdateTask<UserSessionEntity> createSessionTask = Tasks.addIfAbsentSync();
|
SessionUpdateTask<UserSessionEntity> createSessionTask = Tasks.addIfAbsentSync();
|
||||||
sessionTx.addTask(id, createSessionTask, entity);
|
sessionTx.addTask(id, createSessionTask, entity);
|
||||||
|
|
||||||
return wrap(realm, entity, false);
|
UserSessionAdapter adapter = wrap(realm, entity, false);
|
||||||
|
|
||||||
|
if (adapter != null) {
|
||||||
|
DeviceActivityManager.attachDevice(adapter, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
void updateSessionEntity(UserSessionEntity entity, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
|
||||||
|
|
6
pom.xml
6
pom.xml
|
@ -95,6 +95,7 @@
|
||||||
<xmlsec.version>2.1.3</xmlsec.version>
|
<xmlsec.version>2.1.3</xmlsec.version>
|
||||||
<glassfish.json.version>1.1.2</glassfish.json.version>
|
<glassfish.json.version>1.1.2</glassfish.json.version>
|
||||||
<wildfly.common.version>1.5.1.Final</wildfly.common.version>
|
<wildfly.common.version>1.5.1.Final</wildfly.common.version>
|
||||||
|
<ua-parser.version>1.4.3</ua-parser.version>
|
||||||
<picketbox.version>5.0.3.Final</picketbox.version>
|
<picketbox.version>5.0.3.Final</picketbox.version>
|
||||||
<google.guava.version>25.0-jre</google.guava.version>
|
<google.guava.version>25.0-jre</google.guava.version>
|
||||||
|
|
||||||
|
@ -271,6 +272,11 @@
|
||||||
<artifactId>bcpkix-jdk15on</artifactId>
|
<artifactId>bcpkix-jdk15on</artifactId>
|
||||||
<version>${bouncycastle.version}</version>
|
<version>${bouncycastle.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ua-parser</groupId>
|
||||||
|
<artifactId>uap-java</artifactId>
|
||||||
|
<version>${ua-parser.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>javax.mail</groupId>
|
<groupId>javax.mail</groupId>
|
||||||
<artifactId>javax.mail-api</artifactId>
|
<artifactId>javax.mail-api</artifactId>
|
||||||
|
|
|
@ -76,6 +76,10 @@
|
||||||
<artifactId>guava</artifactId>
|
<artifactId>guava</artifactId>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ua-parser</groupId>
|
||||||
|
<artifactId>uap-java</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* 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.device;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.util.Base64;
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.representations.account.DeviceRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import ua_parser.Client;
|
||||||
|
import ua_parser.Parser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class DeviceActivityManager {
|
||||||
|
|
||||||
|
private static final String DEVICE_NOTE = "KC_DEVICE_NOTE";
|
||||||
|
private static final Logger logger = Logger.getLogger(DeviceActivityManager.class);
|
||||||
|
private static final int USER_AGENT_MAX_LENGTH = 512;
|
||||||
|
private static final Parser UA_PARSER;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
UA_PARSER = new Parser();
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to create user agent parser", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the device information associated with the given {@code userSession}.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param userSession the userSession
|
||||||
|
* @return the device information or null if no device is attached to the user session
|
||||||
|
*/
|
||||||
|
public static DeviceRepresentation getCurrentDevice(UserSessionModel userSession) {
|
||||||
|
String deviceInfo = userSession.getNote(DEVICE_NOTE);
|
||||||
|
|
||||||
|
if (deviceInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(Base64.decode(deviceInfo), DeviceRepresentation.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches a device to the given {@code userSession} where the device information is obtained from the {@link HttpHeaders#USER_AGENT} in the current
|
||||||
|
* request, if available.
|
||||||
|
*
|
||||||
|
* @param userSession the user session
|
||||||
|
* @param session the keycloak session
|
||||||
|
*/
|
||||||
|
public static void attachDevice(UserSessionModel userSession, KeycloakSession session) {
|
||||||
|
DeviceRepresentation current = getDeviceFromUserAgent(session);
|
||||||
|
|
||||||
|
if (current != null) {
|
||||||
|
try {
|
||||||
|
userSession.setNote(DEVICE_NOTE, Base64.encodeBytes(JsonSerialization.writeValueAsBytes(current)));
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DeviceRepresentation getDeviceFromUserAgent(KeycloakSession session) {
|
||||||
|
KeycloakContext context = session.getContext();
|
||||||
|
String userAgent = context.getRequestHeaders().getHeaderString(HttpHeaders.USER_AGENT);
|
||||||
|
|
||||||
|
if (userAgent == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAgent.length() > USER_AGENT_MAX_LENGTH) {
|
||||||
|
logger.warn("Ignoring User-Agent header. Length is above the permitted: " + USER_AGENT_MAX_LENGTH);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceRepresentation current;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Client client = UA_PARSER.parse(userAgent);
|
||||||
|
current = new DeviceRepresentation();
|
||||||
|
|
||||||
|
current.setDevice(client.device.family);
|
||||||
|
|
||||||
|
String browserVersion = client.userAgent.major;
|
||||||
|
|
||||||
|
if (client.userAgent.minor != null) {
|
||||||
|
browserVersion += "." + client.userAgent.minor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.userAgent.patch != null) {
|
||||||
|
browserVersion += "." + client.userAgent.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browserVersion == null) {
|
||||||
|
browserVersion = DeviceRepresentation.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.setBrowser(client.userAgent.family, browserVersion);
|
||||||
|
current.setOs(client.os.family);
|
||||||
|
|
||||||
|
String osVersion = client.os.major;
|
||||||
|
|
||||||
|
if (client.os.minor != null) {
|
||||||
|
osVersion += "." + client.os.minor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.os.patch != null) {
|
||||||
|
osVersion += "." + client.os.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.os.patchMinor != null) {
|
||||||
|
osVersion += "." + client.os.patchMinor;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.setOsVersion(osVersion);
|
||||||
|
current.setIpAddress(context.getConnection().getRemoteAddr());
|
||||||
|
current.setMobile(userAgent.toLowerCase().contains("mobile"));
|
||||||
|
} catch (Exception cause) {
|
||||||
|
logger.error("Failed to create device info from user agent header", cause);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ package org.keycloak.services.resources.account;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.Profile;
|
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventStoreProvider;
|
import org.keycloak.events.EventStoreProvider;
|
||||||
|
@ -31,20 +30,16 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
|
||||||
import org.keycloak.representations.account.ClientRepresentation;
|
import org.keycloak.representations.account.ClientRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentRepresentation;
|
import org.keycloak.representations.account.ConsentRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||||
import org.keycloak.representations.account.SessionRepresentation;
|
|
||||||
import org.keycloak.representations.account.UserRepresentation;
|
import org.keycloak.representations.account.UserRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.theme.Theme;
|
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
|
@ -56,19 +51,20 @@ import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.theme.Theme;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -224,84 +220,10 @@ public class AccountRestService {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Path("/sessions")
|
@Path("/sessions")
|
||||||
@GET
|
public SessionResource sessions() {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@NoCache
|
|
||||||
public Response sessions() {
|
|
||||||
checkAccountApiEnabled();
|
checkAccountApiEnabled();
|
||||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||||
|
return new SessionResource(session, auth, request);
|
||||||
List<SessionRepresentation> reps = new LinkedList<>();
|
|
||||||
|
|
||||||
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
|
|
||||||
for (UserSessionModel s : sessions) {
|
|
||||||
SessionRepresentation rep = new SessionRepresentation();
|
|
||||||
rep.setId(s.getId());
|
|
||||||
rep.setIpAddress(s.getIpAddress());
|
|
||||||
rep.setStarted(s.getStarted());
|
|
||||||
rep.setLastAccess(s.getLastSessionRefresh());
|
|
||||||
rep.setExpires(s.getStarted() + realm.getSsoSessionMaxLifespan());
|
|
||||||
rep.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());
|
|
||||||
rep.getClients().add(clientRep);
|
|
||||||
}
|
|
||||||
|
|
||||||
reps.add(rep);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Cors.add(request, Response.ok(reps)).auth().allowedOrigins(auth.getToken()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove sessions
|
|
||||||
*
|
|
||||||
* @param removeCurrent remove current session (default is false)
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Path("/sessions")
|
|
||||||
@DELETE
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@NoCache
|
|
||||||
public Response sessionsLogout(@QueryParam("current") boolean removeCurrent) {
|
|
||||||
checkAccountApiEnabled();
|
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
|
||||||
|
|
||||||
UserSessionModel userSession = auth.getSession();
|
|
||||||
|
|
||||||
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
|
|
||||||
for (UserSessionModel s : userSessions) {
|
|
||||||
if (removeCurrent || !s.getId().equals(userSession.getId())) {
|
|
||||||
AuthenticationManager.backchannelLogout(session, s, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a specific session
|
|
||||||
*
|
|
||||||
* @param id a specific session to remove
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Path("/session")
|
|
||||||
@DELETE
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
|
||||||
@NoCache
|
|
||||||
public Response sessionLogout(@QueryParam("id") String id) {
|
|
||||||
checkAccountApiEnabled();
|
|
||||||
auth.require(AccountRoles.MANAGE_ACCOUNT);
|
|
||||||
|
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, id);
|
|
||||||
if (userSession != null && userSession.getUser().equals(user)) {
|
|
||||||
AuthenticationManager.backchannelLogout(session, userSession, true);
|
|
||||||
}
|
|
||||||
return Cors.add(request, Response.ok()).auth().allowedOrigins(auth.getToken()).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("/credentials")
|
@Path("/credentials")
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* 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 javax.ws.rs.DELETE;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
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 org.keycloak.services.resources.Cors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
private HttpRequest request;
|
||||||
|
|
||||||
|
public SessionResource(KeycloakSession session, Auth auth, HttpRequest request) {
|
||||||
|
this.session = session;
|
||||||
|
this.auth = auth;
|
||||||
|
this.realm = auth.getRealm();
|
||||||
|
this.user = auth.getUser();
|
||||||
|
this.request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get session information.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@NoCache
|
||||||
|
public Response toRepresentation() {
|
||||||
|
return Cors.add(request, Response.ok(session.sessions().getUserSessions(realm, user).stream()
|
||||||
|
.map(this::toRepresentation).collect(Collectors.toList()))).auth().allowedOrigins(auth.getToken()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get device activity information based on the active sessions.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Path("devices")
|
||||||
|
@GET
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@NoCache
|
||||||
|
public Response devices() {
|
||||||
|
Map<String, DeviceRepresentation> reps = new HashMap<>();
|
||||||
|
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
|
||||||
|
|
||||||
|
for (UserSessionModel s : sessions) {
|
||||||
|
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 Cors.add(request, Response.ok(reps.values())).auth().allowedOrigins(auth.getToken()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
|
||||||
|
|
||||||
|
for (UserSessionModel s : userSessions) {
|
||||||
|
if (removeCurrent || !isCurrentSession(s)) {
|
||||||
|
AuthenticationManager.backchannelLogout(session, s, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).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 = session.sessions().getUserSession(realm, id);
|
||||||
|
if (userSession != null && userSession.getUser().equals(user)) {
|
||||||
|
AuthenticationManager.backchannelLogout(session, userSession, true);
|
||||||
|
}
|
||||||
|
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).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());
|
||||||
|
sessionRep.setExpires(s.getStarted() + realm.getSsoSessionMaxLifespan());
|
||||||
|
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) {
|
||||||
|
return session.getId().equals(auth.getSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SessionRepresentation toRepresentation(UserSessionModel s) {
|
||||||
|
return createSessionRepresentation(s, getAttachedDevice(s));
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ import org.keycloak.services.filters.KeycloakSessionServletFilter;
|
||||||
import org.keycloak.services.managers.ApplianceBootstrap;
|
import org.keycloak.services.managers.ApplianceBootstrap;
|
||||||
import org.keycloak.services.resources.KeycloakApplication;
|
import org.keycloak.services.resources.KeycloakApplication;
|
||||||
import org.keycloak.testsuite.KeycloakServer;
|
import org.keycloak.testsuite.KeycloakServer;
|
||||||
|
import org.keycloak.testsuite.TestKeycloakSessionServletFilter;
|
||||||
import org.keycloak.testsuite.utils.tls.TLSUtils;
|
import org.keycloak.testsuite.utils.tls.TLSUtils;
|
||||||
import org.keycloak.testsuite.utils.undertow.UndertowDeployerHelper;
|
import org.keycloak.testsuite.utils.undertow.UndertowDeployerHelper;
|
||||||
import org.keycloak.testsuite.utils.undertow.UndertowWarClassLoader;
|
import org.keycloak.testsuite.utils.undertow.UndertowWarClassLoader;
|
||||||
|
@ -99,7 +100,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
|
||||||
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
di.setDefaultServletConfig(new DefaultServletConfig(true));
|
||||||
di.addWelcomePage("theme/keycloak/welcome/resources/index.html");
|
di.addWelcomePage("theme/keycloak/welcome/resources/index.html");
|
||||||
|
|
||||||
FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class);
|
FilterInfo filter = Servlets.filter("SessionFilter", TestKeycloakSessionServletFilter.class);
|
||||||
di.addFilter(filter);
|
di.addFilter(filter);
|
||||||
di.addFilterUrlMapping("SessionFilter", "/*", DispatcherType.REQUEST);
|
di.addFilterUrlMapping("SessionFilter", "/*", DispatcherType.REQUEST);
|
||||||
filter.setAsyncSupported(true);
|
filter.setAsyncSupported(true);
|
||||||
|
|
|
@ -29,6 +29,11 @@ public class ContainerAssume {
|
||||||
Assume.assumeFalse("Doesn't work on auth-server-undertow",
|
Assume.assumeFalse("Doesn't work on auth-server-undertow",
|
||||||
AuthServerTestEnricher.AUTH_SERVER_CONTAINER.equals(AuthServerTestEnricher.AUTH_SERVER_CONTAINER_DEFAULT));
|
AuthServerTestEnricher.AUTH_SERVER_CONTAINER.equals(AuthServerTestEnricher.AUTH_SERVER_CONTAINER_DEFAULT));
|
||||||
}
|
}
|
||||||
|
public static void assumeAuthServerUndertow() {
|
||||||
|
Assume.assumeTrue("Only works on auth-server-undertow",
|
||||||
|
AuthServerTestEnricher.AUTH_SERVER_CONTAINER.equals(AuthServerTestEnricher.AUTH_SERVER_CONTAINER_DEFAULT));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static void assumeClusteredContainer() {
|
public static void assumeClusteredContainer() {
|
||||||
Assume.assumeTrue(
|
Assume.assumeTrue(
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.apache.http.client.utils.URLEncodedUtils;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.jboss.arquillian.drone.webdriver.htmlunit.DroneHtmlUnitDriver;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.TokenVerifier;
|
import org.keycloak.TokenVerifier;
|
||||||
|
@ -1249,6 +1250,17 @@ public class OAuthClient {
|
||||||
publicKeys.clear();
|
publicKeys.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBrowserHeader(String name, String value) {
|
||||||
|
if (driver instanceof DroneHtmlUnitDriver) {
|
||||||
|
DroneHtmlUnitDriver droneDriver = (DroneHtmlUnitDriver) this.driver;
|
||||||
|
droneDriver.getWebClient().removeRequestHeader(name);
|
||||||
|
droneDriver.getWebClient().addRequestHeader(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public WebDriver getDriver() {
|
||||||
|
return driver;
|
||||||
|
}
|
||||||
|
|
||||||
private interface StateParamProvider {
|
private interface StateParamProvider {
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.util;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Qualifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:pmensik@redhat.com">Petr Mensik</a>
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||||
|
@Qualifier
|
||||||
|
public @interface ThirdBrowser {
|
||||||
|
}
|
|
@ -196,21 +196,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
TokenUtil noaccessToken = new TokenUtil("no-account-access", "password");
|
TokenUtil noaccessToken = new TokenUtil("no-account-access", "password");
|
||||||
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
||||||
|
|
||||||
// Read sessions with no access
|
|
||||||
assertEquals(403, SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
|
|
||||||
|
|
||||||
// Delete all sessions with no access
|
|
||||||
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("sessions"), httpClient).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
|
|
||||||
|
|
||||||
// Delete all sessions with read only
|
|
||||||
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("sessions"), httpClient).header("Accept", "application/json").auth(viewToken.getToken()).asStatus());
|
|
||||||
|
|
||||||
// Delete single session with no access
|
|
||||||
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("session?id=bogusId"), httpClient).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
|
|
||||||
|
|
||||||
// Delete single session with read only
|
|
||||||
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("session?id=bogusId"), httpClient).header("Accept", "application/json").auth(viewToken.getToken()).asStatus());
|
|
||||||
|
|
||||||
// Read password details with no access
|
// Read password details with no access
|
||||||
assertEquals(403, SimpleHttp.doGet(getAccountUrl("credentials/password"), httpClient).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
|
assertEquals(403, SimpleHttp.doGet(getAccountUrl("credentials/password"), httpClient).header("Accept", "application/json").auth(noaccessToken.getToken()).asStatus());
|
||||||
|
|
||||||
|
@ -232,15 +217,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
assertEquals(200, status);
|
assertEquals(200, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testGetSessions() throws IOException {
|
|
||||||
assumeFeatureEnabled(ACCOUNT_API);
|
|
||||||
|
|
||||||
List<SessionRepresentation> sessions = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).auth(tokenUtil.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
|
|
||||||
|
|
||||||
assertEquals(1, sessions.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetPasswordDetails() throws IOException {
|
public void testGetPasswordDetails() throws IOException {
|
||||||
assumeFeatureEnabled(ACCOUNT_API);
|
assumeFeatureEnabled(ACCOUNT_API);
|
||||||
|
@ -316,28 +292,6 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
|
||||||
assertEquals(1, sessions.size());
|
assertEquals(1, sessions.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDeleteSession() throws IOException {
|
|
||||||
assumeFeatureEnabled(ACCOUNT_API);
|
|
||||||
|
|
||||||
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
|
||||||
String sessionId = oauth.doLogin("view-account-access", "password").getSessionState();
|
|
||||||
List<SessionRepresentation> sessions = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).auth(viewToken.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
|
|
||||||
assertEquals(2, sessions.size());
|
|
||||||
|
|
||||||
// With `ViewToken` you can only read
|
|
||||||
int status = SimpleHttp.doDelete(getAccountUrl("session?id=" + sessionId), httpClient).acceptJson().auth(viewToken.getToken()).asStatus();
|
|
||||||
assertEquals(403, status);
|
|
||||||
sessions = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).auth(viewToken.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
|
|
||||||
assertEquals(2, sessions.size());
|
|
||||||
|
|
||||||
// Here you can delete the session
|
|
||||||
status = SimpleHttp.doDelete(getAccountUrl("session?id=" + sessionId), httpClient).acceptJson().auth(tokenUtil.getToken()).asStatus();
|
|
||||||
assertEquals(200, status);
|
|
||||||
sessions = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).auth(tokenUtil.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
|
|
||||||
assertEquals(1, sessions.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void listApplications() throws IOException {
|
public void listApplications() throws IOException {
|
||||||
assumeFeatureEnabled(ACCOUNT_API);
|
assumeFeatureEnabled(ACCOUNT_API);
|
||||||
|
|
|
@ -0,0 +1,447 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.account;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.keycloak.common.Profile.Feature.ACCOUNT_API;
|
||||||
|
import static org.keycloak.testsuite.ProfileAssume.assumeFeatureEnabled;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.broker.provider.util.SimpleHttp;
|
||||||
|
import org.keycloak.representations.account.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.account.DeviceRepresentation;
|
||||||
|
import org.keycloak.representations.account.SessionRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.util.ClientBuilder;
|
||||||
|
import org.keycloak.testsuite.util.ContainerAssume;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.SecondBrowser;
|
||||||
|
import org.keycloak.testsuite.util.ThirdBrowser;
|
||||||
|
import org.keycloak.testsuite.util.TokenUtil;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
*/
|
||||||
|
public class SessionRestServiceTest extends AbstractRestServiceTest {
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@SecondBrowser
|
||||||
|
protected WebDriver secondBrowser;
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@ThirdBrowser
|
||||||
|
protected WebDriver thirdBrowser;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
super.configureTestRealm(testRealm);
|
||||||
|
|
||||||
|
testRealm.getClients().add(ClientBuilder.create()
|
||||||
|
.clientId("public-client-0")
|
||||||
|
.name("Public Client 0")
|
||||||
|
.baseUrl("http://client0.example.com")
|
||||||
|
.redirectUris(OAuthClient.APP_ROOT + "/auth")
|
||||||
|
.publicClient().build());
|
||||||
|
|
||||||
|
testRealm.getClients().add(ClientBuilder.create()
|
||||||
|
.clientId("public-client-1")
|
||||||
|
.name("Public Client 1")
|
||||||
|
.baseUrl("http://client1.example.com")
|
||||||
|
.redirectUris(OAuthClient.APP_ROOT + "/auth")
|
||||||
|
.publicClient().build());
|
||||||
|
|
||||||
|
testRealm.getClients().add(ClientBuilder.create()
|
||||||
|
.clientId("confidential-client-0")
|
||||||
|
.name("Confidential Client 0")
|
||||||
|
.secret("secret")
|
||||||
|
.serviceAccount()
|
||||||
|
.directAccessGrants()
|
||||||
|
.redirectUris(OAuthClient.APP_ROOT + "/auth").build());
|
||||||
|
|
||||||
|
testRealm.getClients().add(ClientBuilder.create()
|
||||||
|
.clientId("confidential-client-1")
|
||||||
|
.name("Confidential Client 1")
|
||||||
|
.secret("secret")
|
||||||
|
.serviceAccount()
|
||||||
|
.directAccessGrants()
|
||||||
|
.redirectUris(OAuthClient.APP_ROOT + "/auth").build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProfilePreviewPermissions() throws IOException {
|
||||||
|
assumeFeatureEnabled(ACCOUNT_API);
|
||||||
|
|
||||||
|
TokenUtil noaccessToken = new TokenUtil("no-account-access", "password");
|
||||||
|
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
||||||
|
|
||||||
|
// Read sessions with no access
|
||||||
|
assertEquals(403, SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).header("Accept", "application/json")
|
||||||
|
.auth(noaccessToken.getToken()).asStatus());
|
||||||
|
|
||||||
|
// Delete all sessions with no access
|
||||||
|
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("sessions"), httpClient).header("Accept", "application/json")
|
||||||
|
.auth(noaccessToken.getToken()).asStatus());
|
||||||
|
|
||||||
|
// Delete all sessions with read only
|
||||||
|
assertEquals(403, SimpleHttp.doDelete(getAccountUrl("sessions"), httpClient).header("Accept", "application/json")
|
||||||
|
.auth(viewToken.getToken()).asStatus());
|
||||||
|
|
||||||
|
// Delete single session with no access
|
||||||
|
assertEquals(403,
|
||||||
|
SimpleHttp.doDelete(getAccountUrl("sessions/bogusId"), httpClient).header("Accept", "application/json")
|
||||||
|
.auth(noaccessToken.getToken()).asStatus());
|
||||||
|
|
||||||
|
// Delete single session with read only
|
||||||
|
assertEquals(403,
|
||||||
|
SimpleHttp.doDelete(getAccountUrl("sessions/bogusId"), httpClient).header("Accept", "application/json")
|
||||||
|
.auth(viewToken.getToken()).asStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
@Override
|
||||||
|
public void before() {
|
||||||
|
super.before();
|
||||||
|
assumeFeatureEnabled(ACCOUNT_API);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSessions() throws Exception {
|
||||||
|
oauth.setDriver(secondBrowser);
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
|
||||||
|
List<SessionRepresentation> sessions = getSessions();
|
||||||
|
assertEquals(2, sessions.size());
|
||||||
|
|
||||||
|
for (SessionRepresentation session : sessions) {
|
||||||
|
assertNotNull(session.getId());
|
||||||
|
assertEquals("127.0.0.1", session.getIpAddress());
|
||||||
|
assertTrue(session.getLastAccess() > 0);
|
||||||
|
assertTrue(session.getExpires() > 0);
|
||||||
|
assertTrue(session.getStarted() > 0);
|
||||||
|
assertThat(session.getClients(), Matchers.hasItem(Matchers.hasProperty("clientId",
|
||||||
|
Matchers.anyOf(Matchers.is("direct-grant"), Matchers.is("public-client-0")))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDevicesResponse() throws Exception {
|
||||||
|
oauth.setBrowserHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = codeGrant("public-client-0");
|
||||||
|
joinSsoSession("public-client-1");
|
||||||
|
|
||||||
|
List<DeviceRepresentation> devices = getDevicesOtherThanOther(tokenResponse.getAccessToken());
|
||||||
|
|
||||||
|
assertEquals("Should have a single device", 1, devices.size());
|
||||||
|
|
||||||
|
DeviceRepresentation device = devices.get(0);
|
||||||
|
|
||||||
|
assertTrue(device.getCurrent());
|
||||||
|
assertEquals("Windows", device.getOs());
|
||||||
|
assertEquals("10", device.getOsVersion());
|
||||||
|
assertEquals("Other", device.getDevice());
|
||||||
|
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(1, sessions.size());
|
||||||
|
SessionRepresentation session = sessions.get(0);
|
||||||
|
assertEquals("127.0.0.1", session.getIpAddress());
|
||||||
|
assertTrue(device.getLastAccess() == session.getLastAccess());
|
||||||
|
|
||||||
|
List<ClientRepresentation> clients = session.getClients();
|
||||||
|
assertEquals(2, clients.size());
|
||||||
|
assertThat(session.getClients(), Matchers.hasItem(Matchers.hasProperty("clientId",
|
||||||
|
Matchers.anyOf(Matchers.is("public-client-0"), Matchers.is("public-client-1")))));
|
||||||
|
assertThat(session.getClients(), Matchers.hasItem(Matchers.hasProperty("clientName",
|
||||||
|
Matchers.anyOf(Matchers.is("Public Client 0"), Matchers.is("Public Client 1")))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDevicesSessions() throws Exception {
|
||||||
|
ContainerAssume.assumeAuthServerUndertow();
|
||||||
|
WebDriver firstBrowser = oauth.getDriver();
|
||||||
|
|
||||||
|
// first browser authenticates from Fedora
|
||||||
|
oauth.setBrowserHeader("User-Agent", "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
List<DeviceRepresentation> devices = getDevicesOtherThanOther();
|
||||||
|
assertEquals("Should have a single device", 1, devices.size());
|
||||||
|
List<DeviceRepresentation> fedoraDevices = devices.stream()
|
||||||
|
.filter(deviceRepresentation -> "Fedora".equals(deviceRepresentation.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals("Should have a single Fedora device", 1, fedoraDevices.size());
|
||||||
|
fedoraDevices.stream().forEach(device -> {
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(1, sessions.size());
|
||||||
|
assertThat(sessions, Matchers.hasItem(Matchers.hasProperty("browser", Matchers.is("Firefox/15.0.1"))));
|
||||||
|
});
|
||||||
|
|
||||||
|
// second browser authenticates from Windows
|
||||||
|
oauth.setDriver(secondBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
devices = getDevicesOtherThanOther();
|
||||||
|
// should have two devices
|
||||||
|
assertEquals("Should have two devices", 2, devices.size());
|
||||||
|
fedoraDevices = devices.stream()
|
||||||
|
.filter(deviceRepresentation -> "Fedora".equals(deviceRepresentation.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals(1, fedoraDevices.size());
|
||||||
|
List<DeviceRepresentation> windowsDevices = devices.stream()
|
||||||
|
.filter(deviceRepresentation -> "Windows".equals(deviceRepresentation.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals(1, windowsDevices.size());
|
||||||
|
windowsDevices.stream().forEach(device -> {
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(1, sessions.size());
|
||||||
|
assertThat(sessions, Matchers.hasItem(Matchers.hasProperty("browser", Matchers.is("Firefox/15.0.1"))));
|
||||||
|
});
|
||||||
|
|
||||||
|
// first browser authenticates from Windows using Edge
|
||||||
|
oauth.setDriver(firstBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36 Edge/12.0");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
|
||||||
|
// second browser authenticates from Windows using Firefox
|
||||||
|
oauth.setDriver(secondBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
|
||||||
|
// third browser authenticates from Windows using Safari
|
||||||
|
oauth.setDriver(thirdBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Version/11.0 Safari/603.1.30");
|
||||||
|
oauth.setBrowserHeader("X-Forwarded-For", "192.168.10.3");
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = codeGrant("public-client-0");
|
||||||
|
devices = getDevicesOtherThanOther(tokenResponse.getAccessToken());
|
||||||
|
assertEquals(
|
||||||
|
"Should have a single device because all browsers (and sessions) are from the same platform (OS + OS version)",
|
||||||
|
1, devices.size());
|
||||||
|
windowsDevices = devices.stream()
|
||||||
|
.filter(device -> "Windows".equals(device.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals(1, windowsDevices.size());
|
||||||
|
windowsDevices.stream().forEach(device -> {
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(3, sessions.size());
|
||||||
|
assertEquals(1, sessions.stream().filter(
|
||||||
|
rep -> rep.getIpAddress().equals("127.0.0.1") && rep.getBrowser().equals("Firefox/15.0.1")
|
||||||
|
&& rep.getCurrent() == null).count());
|
||||||
|
assertEquals(1, sessions.stream().filter(
|
||||||
|
rep -> rep.getIpAddress().equals("127.0.0.1") && rep.getBrowser().equals("Edge/12.0")
|
||||||
|
&& rep.getCurrent() == null).count());
|
||||||
|
assertEquals(1, sessions.stream().filter(
|
||||||
|
rep -> rep.getIpAddress().equals("192.168.10.3") && rep.getBrowser().equals("Safari/11.0") && rep
|
||||||
|
.getCurrent()).count());
|
||||||
|
});
|
||||||
|
|
||||||
|
// third browser authenticates from Windows using a different Windows version
|
||||||
|
oauth.setDriver(thirdBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (Windows 7) AppleWebKit/537.36 (KHTML, like Gecko) Version/11.0 Safari/603.1.30");
|
||||||
|
oauth.setBrowserHeader("X-Forwarded-For", "192.168.10.3");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
devices = getDevicesOtherThanOther();
|
||||||
|
windowsDevices = devices.stream()
|
||||||
|
.filter(device -> "Windows".equals(device.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals("Should have two devices for two distinct Windows versions", 2, devices.size());
|
||||||
|
assertEquals(2, windowsDevices.size());
|
||||||
|
|
||||||
|
oauth.setDriver(firstBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
oauth.setDriver(secondBrowser);
|
||||||
|
oauth.setBrowserHeader("User-Agent",
|
||||||
|
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
devices = getDevicesOtherThanOther();
|
||||||
|
assertEquals("Should have 3 devices", 3, devices.size());
|
||||||
|
windowsDevices = devices.stream()
|
||||||
|
.filter(device -> "Windows".equals(device.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals(1, windowsDevices.size());
|
||||||
|
fedoraDevices = devices.stream()
|
||||||
|
.filter(deviceRepresentation -> "Fedora".equals(deviceRepresentation.getOs())).collect(Collectors.toList());
|
||||||
|
assertEquals(1, fedoraDevices.size());
|
||||||
|
List<DeviceRepresentation> iphoneDevices = devices.stream()
|
||||||
|
.filter(device -> "iOS".equals(device.getOs()) && "iPhone".equals(device.getDevice()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
assertEquals(1, iphoneDevices.size());
|
||||||
|
iphoneDevices.stream().forEach(device -> {
|
||||||
|
assertTrue(device.isMobile());
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(1, sessions.size());
|
||||||
|
assertEquals(1, sessions.stream().filter(
|
||||||
|
rep -> rep.getBrowser().equals("Mobile Safari/5.1")).count());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLogout() throws IOException {
|
||||||
|
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
|
||||||
|
String sessionId = oauth.doLogin("view-account-access", "password").getSessionState();
|
||||||
|
List<SessionRepresentation> sessions = getSessions(viewToken.getToken());
|
||||||
|
assertEquals(2, sessions.size());
|
||||||
|
|
||||||
|
// With `ViewToken` you can only read
|
||||||
|
int status = SimpleHttp.doDelete(getAccountUrl("sessions/" + sessionId), httpClient).acceptJson()
|
||||||
|
.auth(viewToken.getToken()).asStatus();
|
||||||
|
assertEquals(403, status);
|
||||||
|
sessions = getSessions(viewToken.getToken());
|
||||||
|
assertEquals(2, sessions.size());
|
||||||
|
|
||||||
|
// Here you can delete the session
|
||||||
|
status = SimpleHttp.doDelete(getAccountUrl("sessions/" + sessionId), httpClient).acceptJson().auth(tokenUtil.getToken())
|
||||||
|
.asStatus();
|
||||||
|
assertEquals(204, status);
|
||||||
|
sessions = getSessions(tokenUtil.getToken());
|
||||||
|
assertEquals(1, sessions.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLogoutAll() throws IOException {
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
oauth.setDriver(secondBrowser);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = codeGrant("public-client-0");
|
||||||
|
|
||||||
|
assertEquals(3, getSessions().size());
|
||||||
|
|
||||||
|
String currentToken = tokenResponse.getAccessToken();
|
||||||
|
int status = SimpleHttp.doDelete(getAccountUrl("sessions"), httpClient)
|
||||||
|
.acceptJson()
|
||||||
|
.auth(currentToken).asStatus();
|
||||||
|
assertEquals(204, status);
|
||||||
|
assertEquals(1, getSessions(currentToken).size());
|
||||||
|
|
||||||
|
status = SimpleHttp.doDelete(getAccountUrl("sessions?current=true"), httpClient)
|
||||||
|
.acceptJson()
|
||||||
|
.auth(currentToken).asStatus();
|
||||||
|
assertEquals(204, status);
|
||||||
|
|
||||||
|
status = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient)
|
||||||
|
.acceptJson()
|
||||||
|
.auth(currentToken).asStatus();
|
||||||
|
assertEquals(401, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNullOrEmptyUserAgent() throws Exception {
|
||||||
|
oauth.setBrowserHeader("User-Agent", null);
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = codeGrant("public-client-0");
|
||||||
|
|
||||||
|
List<DeviceRepresentation> devices = queryDevices(tokenResponse.getAccessToken());
|
||||||
|
|
||||||
|
assertEquals("Should have a single device", 1, devices.size());
|
||||||
|
|
||||||
|
DeviceRepresentation device = devices.get(0);
|
||||||
|
|
||||||
|
assertTrue(device.getCurrent());
|
||||||
|
assertEquals("Other", device.getOs());
|
||||||
|
assertEquals("Other", device.getDevice());
|
||||||
|
|
||||||
|
List<SessionRepresentation> sessions = device.getSessions();
|
||||||
|
assertEquals(2, sessions.size());
|
||||||
|
SessionRepresentation session = sessions.stream().filter(rep -> rep.getCurrent() != null && rep.getCurrent()).findFirst().get();
|
||||||
|
assertEquals("127.0.0.1", session.getIpAddress());
|
||||||
|
assertEquals(device.getLastAccess(), session.getLastAccess());
|
||||||
|
|
||||||
|
assertEquals(1, session.getClients().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNonBrowserSession() throws Exception {
|
||||||
|
// one device
|
||||||
|
oauth.setBrowserHeader("User-Agent", "Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1");
|
||||||
|
codeGrant("public-client-0");
|
||||||
|
|
||||||
|
// all bellow grouped from a single Other device
|
||||||
|
oauth.setBrowserHeader("User-Agent", null);
|
||||||
|
oauth.clientId("confidential-client-0");
|
||||||
|
oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
|
||||||
|
oauth.clientId("confidential-client-1");
|
||||||
|
oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
|
||||||
|
|
||||||
|
List<DeviceRepresentation> devices = getAllDevices();
|
||||||
|
assertEquals(2, devices.size());
|
||||||
|
|
||||||
|
assertThat(devices,
|
||||||
|
Matchers.hasItems(Matchers.hasProperty("os", Matchers.anyOf(Matchers.is("Fedora"), Matchers.is("Other")))));
|
||||||
|
|
||||||
|
// three because tests use another client when booting tests
|
||||||
|
assertEquals(3, devices.stream().filter(deviceRepresentation -> "Other".equals(deviceRepresentation.getOs()))
|
||||||
|
.map(deviceRepresentation -> deviceRepresentation.getSessions().size())
|
||||||
|
.findFirst().get().intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SessionRepresentation> getSessions(String sessionOne) throws IOException {
|
||||||
|
return SimpleHttp
|
||||||
|
.doGet(getAccountUrl("sessions"), httpClient).auth(sessionOne)
|
||||||
|
.asJson(new TypeReference<List<SessionRepresentation>>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DeviceRepresentation> getDevicesOtherThanOther() throws IOException {
|
||||||
|
return getDevicesOtherThanOther(tokenUtil.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DeviceRepresentation> getAllDevices() throws IOException {
|
||||||
|
return queryDevices(tokenUtil.getToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DeviceRepresentation> getDevicesOtherThanOther(String token) throws IOException {
|
||||||
|
return queryDevices(token).stream().filter(rep -> !"Other".equals(rep.getOs())).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DeviceRepresentation> queryDevices(String token) throws IOException {
|
||||||
|
return SimpleHttp
|
||||||
|
.doGet(getAccountUrl("sessions/devices"), httpClient).auth(token)
|
||||||
|
.asJson(new TypeReference<List<DeviceRepresentation>>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuthClient.AccessTokenResponse codeGrant(String clientId) {
|
||||||
|
oauth.clientId(clientId);
|
||||||
|
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||||
|
oauth.openLogout();
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
return oauth.doAccessTokenRequest(code, "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinSsoSession(String clientId) {
|
||||||
|
oauth.clientId(clientId);
|
||||||
|
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SessionRepresentation> getSessions() throws IOException {
|
||||||
|
return SimpleHttp
|
||||||
|
.doGet(getAccountUrl("sessions"), httpClient).auth(tokenUtil.getToken())
|
||||||
|
.asJson(new TypeReference<List<SessionRepresentation>>() {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -411,7 +411,7 @@ public class KeycloakServer {
|
||||||
|
|
||||||
di.addServlet(restEasyDispatcher);
|
di.addServlet(restEasyDispatcher);
|
||||||
|
|
||||||
FilterInfo filter = Servlets.filter("SessionFilter", KeycloakSessionServletFilter.class);
|
FilterInfo filter = Servlets.filter("SessionFilter", TestKeycloakSessionServletFilter.class);
|
||||||
|
|
||||||
filter.setAsyncSupported(true);
|
filter.setAsyncSupported(true);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.keycloak.services.filters.KeycloakSessionServletFilter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class TestKeycloakSessionServletFilter extends KeycloakSessionServletFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
super.doFilter(new HttpServletRequestWrapper((HttpServletRequest) servletRequest) {
|
||||||
|
@Override
|
||||||
|
public String getRemoteAddr() {
|
||||||
|
String forwardedFor = getHeader("X-Forwarded-For");
|
||||||
|
|
||||||
|
if (forwardedFor != null) {
|
||||||
|
return forwardedFor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getRemoteAddr();
|
||||||
|
}
|
||||||
|
}, servletResponse, filterChain);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue