parent
9c431f3b90
commit
1d75000a0e
18 changed files with 207 additions and 84 deletions
|
@ -66,10 +66,6 @@
|
|||
<artifactId>guava</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ua-parser</groupId>
|
||||
<artifactId>uap-java</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
|
|
|
@ -20,15 +20,11 @@ package org.keycloak.device;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
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>
|
||||
|
@ -36,13 +32,6 @@ import ua_parser.Parser;
|
|||
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 {
|
||||
UA_PARSER = new Parser();
|
||||
}
|
||||
|
||||
/** Returns the device information associated with the given {@code userSession}.
|
||||
*
|
||||
|
@ -72,7 +61,7 @@ public class DeviceActivityManager {
|
|||
* @param session the keycloak session
|
||||
*/
|
||||
public static void attachDevice(UserSessionModel userSession, KeycloakSession session) {
|
||||
DeviceRepresentation current = getDeviceFromUserAgent(session);
|
||||
DeviceRepresentation current = session.getProvider(DeviceRepresentationProvider.class).deviceRepresentation();
|
||||
|
||||
if (current != null) {
|
||||
try {
|
||||
|
@ -82,72 +71,4 @@ public class DeviceActivityManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DeviceRepresentation getDeviceFromUserAgent(KeycloakSession session) {
|
||||
KeycloakContext context = session.getContext();
|
||||
|
||||
if (context.getRequestHeaders() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.keycloak.device;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.representations.account.DeviceRepresentation;
|
||||
|
||||
public interface DeviceRepresentationProvider extends Provider {
|
||||
|
||||
DeviceRepresentation deviceRepresentation();
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package org.keycloak.device;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
public interface DeviceRepresentationProviderFactory extends ProviderFactory<DeviceRepresentationProvider> {
|
||||
|
||||
@Override
|
||||
default void init(Config.Scope config) {
|
||||
}
|
||||
|
||||
@Override
|
||||
default void postInit(KeycloakSessionFactory factory) {
|
||||
}
|
||||
|
||||
@Override
|
||||
default void close() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.keycloak.device;
|
||||
|
||||
import org.keycloak.provider.Provider;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.provider.Spi;
|
||||
|
||||
public class DeviceRepresentationSpi implements Spi {
|
||||
|
||||
public static final String NAME = "deviceRepresentation";
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Provider> getProviderClass() {
|
||||
return DeviceRepresentationProvider.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends ProviderFactory> getProviderFactoryClass() {
|
||||
return DeviceRepresentationProviderFactory.class;
|
||||
}
|
||||
|
||||
}
|
|
@ -89,3 +89,4 @@ org.keycloak.services.clientpolicy.condition.ClientPolicyConditionSpi
|
|||
org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorSpi
|
||||
org.keycloak.services.clientpolicy.ClientPolicyManagerSpi
|
||||
org.keycloak.userprofile.UserProfileSpi
|
||||
org.keycloak.device.DeviceRepresentationSpi
|
||||
|
|
|
@ -180,6 +180,10 @@
|
|||
<groupId>com.webauthn4j</groupId>
|
||||
<artifactId>webauthn4j-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.ua-parser</groupId>
|
||||
<artifactId>uap-java</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package org.keycloak.device;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import ua_parser.Parser;
|
||||
|
||||
public class DeviceRepresentationProviderFactoryImpl implements DeviceRepresentationProviderFactory {
|
||||
|
||||
private volatile Parser parser;
|
||||
|
||||
public static final String PROVIDER_ID = "deviceRepresentation";
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRepresentationProvider create(KeycloakSession session) {
|
||||
lazyInit(session);
|
||||
return new DeviceRepresentationProviderImpl(session, parser);
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if(parser == null) {
|
||||
synchronized (this) {
|
||||
parser = new Parser();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package org.keycloak.device;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.representations.account.DeviceRepresentation;
|
||||
import ua_parser.Client;
|
||||
import ua_parser.Parser;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
|
||||
public class DeviceRepresentationProviderImpl implements DeviceRepresentationProvider {
|
||||
private static final Logger logger = Logger.getLogger(DeviceActivityManager.class);
|
||||
private static final int USER_AGENT_MAX_LENGTH = 512;
|
||||
private final Parser parser;
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
DeviceRepresentationProviderImpl(KeycloakSession session, Parser parser) {
|
||||
this.session = session;
|
||||
this.parser = parser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRepresentation deviceRepresentation() {
|
||||
KeycloakContext context = session.getContext();
|
||||
|
||||
if (context.getRequestHeaders() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.device.DeviceRepresentationProviderFactoryImpl
|
|
@ -19,6 +19,7 @@ package org.keycloak.testsuite.model.session;
|
|||
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -43,6 +44,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
|||
|
||||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(value = HotRodConnectionProvider.class, only = DefaultHotRodConnectionProviderFactory.PROVIDER_ID)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class HotRodUserSessionClientSessionRelationshipTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.testsuite.model.session;
|
|||
|
||||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.infinispan.commons.CacheException;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -65,6 +66,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
|
|||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class OfflineSessionPersistenceTest extends KeycloakModelTest {
|
||||
|
||||
private static final int USER_COUNT = 50;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.model.session;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -53,6 +54,7 @@ import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForM
|
|||
|
||||
|
||||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionConcurrencyTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package org.keycloak.testsuite.model.session;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
@ -33,6 +34,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
|
|||
|
||||
@RequireProvider(value = UserSessionProvider.class, only = MapUserSessionProviderFactory.PROVIDER_ID)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionExpirationTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.infinispan.client.hotrod.RemoteCache;
|
|||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -61,6 +62,7 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.U
|
|||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionInitializerTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.junit.Assert;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -70,6 +71,7 @@ import java.util.LinkedList;
|
|||
@RequireProvider(value = UserSessionProvider.class, only = InfinispanUserSessionProviderFactory.PROVIDER_ID)
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionPersisterProviderTest extends KeycloakModelTest {
|
||||
|
||||
private static final int USER_SESSION_COUNT = 2000;
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.hamcrest.Matchers;
|
|||
import org.infinispan.client.hotrod.RemoteCache;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -69,6 +70,7 @@ import static org.keycloak.testsuite.model.session.UserSessionPersisterProviderT
|
|||
@RequireProvider(UserSessionProvider.class)
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionProviderModelTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.junit.Assume;
|
|||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||
import org.keycloak.device.DeviceRepresentationProvider;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
|
@ -65,6 +66,7 @@ import org.keycloak.testsuite.model.RequireProvider;
|
|||
@RequireProvider(value=UserSessionProvider.class, only={"infinispan"})
|
||||
@RequireProvider(UserProvider.class)
|
||||
@RequireProvider(RealmProvider.class)
|
||||
@RequireProvider(DeviceRepresentationProvider.class)
|
||||
public class UserSessionProviderOfflineModelTest extends KeycloakModelTest {
|
||||
|
||||
private String realmId;
|
||||
|
|
Loading…
Reference in a new issue