keycloak-uncached
Changes
connections/infinispan/pom.xml 1(+1 -0)
connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 13(+13 -0)
connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 1(+1 -0)
connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 69(+48 -21)
dependencies/server-all/pom.xml 16(+0 -16)
distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json 4(+4 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/de/idyl/winzipaes/main/module.xml 14(+0 -14)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-export-import-zip/main/module.xml 26(+0 -26)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml 1(+0 -1)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml 1(+0 -1)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/de/idyl/winzipaes/main/module.xml 13(+0 -13)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml 1(+0 -1)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-export-import-zip/main/module.xml 26(+0 -26)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml 1(+0 -1)
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java 47(+23 -24)
export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipExportProvider.java 82(+0 -82)
export-import/export-import-zip/src/main/java/org/keycloak/exportimport/zip/ZipImportProvider.java 135(+0 -135)
export-import/export-import-zip/src/main/resources/META-INF/services/org.keycloak.exportimport.ExportProviderFactory 1(+0 -1)
export-import/export-import-zip/src/main/resources/META-INF/services/org.keycloak.exportimport.ImportProviderFactory 1(+0 -1)
export-import/pom.xml 1(+0 -1)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 8(+1 -7)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java 4(+2 -2)
forms/common-themes/src/main/resources/theme/base/account/messages/messages_fr.properties 70(+35 -35)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties 3(+2 -1)
forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties 5(+4 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html 7(+7 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html 2(+1 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html 11(+11 -0)
forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties 205(+205 -0)
forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_fr.properties 21(+21 -0)
forms/email-freemarker/pom.xml 5(+5 -0)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/ProfileBean.java 77(+77 -0)
forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java 8(+6 -2)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 12(+2 -10)
model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java 104(+104 -0)
model/api/src/main/java/org/keycloak/models/session/PersistentClientSessionAdapter.java 183(+142 -41)
model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java 9(+9 -0)
model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory 1(+1 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java 12(+12 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java 98(+5 -93)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java 12(+12 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java 6(+6 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java 6(+6 -0)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java 27(+3 -24)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java 239(+239 -0)
model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProviderFactory.java 33(+15 -18)
model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentClientSessionEntity.java 79(+64 -15)
model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java 147(+147 -0)
model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory 1(+1 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java 239(+0 -239)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java 276(+276 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProviderFactory.java 32(+12 -20)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java 10(+10 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java 10(+10 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java 15(+15 -0)
model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory 1(+1 -0)
model/sessions-infinispan/pom.xml 5(+5 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java 20(+13 -7)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java 219(+189 -30)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java 6(+5 -1)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java 37(+37 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java 5(+5 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java 10(+0 -10)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 317(+267 -50)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 88(+74 -14)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java 268(+268 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java 108(+108 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java 49(+49 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java 65(+65 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java 15(+15 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java 44(+44 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java 17(+13 -4)
model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java 45(+45 -0)
pom.xml 13(+0 -13)
services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java 8(+4 -4)
services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java 3(+2 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java 25(+0 -25)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java 162(+162 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java 335(+335 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java 348(+348 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 18(+16 -2)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java 8(+4 -4)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java 88(+76 -12)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java 3(+2 -1)
Details
connections/infinispan/pom.xml 1(+1 -0)
diff --git a/connections/infinispan/pom.xml b/connections/infinispan/pom.xml
index 711de79..3bfefdf 100755
--- a/connections/infinispan/pom.xml
+++ b/connections/infinispan/pom.xml
@@ -27,5 +27,6 @@
<artifactId>infinispan-core</artifactId>
<scope>provided</scope>
</dependency>
+
</dependencies>
</project>
diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index 58962dc..bc2635c 100755
--- a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -66,6 +66,19 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
} else {
initEmbedded();
}
+
+ // Backwards compatibility
+ if (cacheManager.getCacheConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME) == null) {
+ logger.warnf("No configuration provided for '%s' cache. Using '%s' configuration as template",
+ InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, InfinispanConnectionProvider.SESSION_CACHE_NAME);
+
+ Configuration sessionCacheConfig = cacheManager.getCacheConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ if (sessionCacheConfig != null) {
+ ConfigurationBuilder confBuilder = new ConfigurationBuilder().read(sessionCacheConfig);
+ Configuration offlineSessionConfig = confBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, offlineSessionConfig);
+ }
+ }
}
}
}
diff --git a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 05cfdb9..945e0de 100644
--- a/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -11,6 +11,7 @@ public interface InfinispanConnectionProvider extends Provider {
static final String REALM_CACHE_NAME = "realms";
static final String USER_CACHE_NAME = "users";
static final String SESSION_CACHE_NAME = "sessions";
+ static final String OFFLINE_SESSION_CACHE_NAME = "offlineSessions";
static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures";
<K, V> Cache<K, V> getCache(String name);
diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index f8f33be..b8592a3 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -29,8 +29,8 @@
<class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
- <class>org.keycloak.models.jpa.entities.OfflineUserSessionEntity</class>
- <class>org.keycloak.models.jpa.entities.OfflineClientSessionEntity</class>
+ <class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
+ <class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
<!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
index 9d6e545..9e48a6a 100644
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.6.0.xml
@@ -10,22 +10,27 @@
<addColumn tableName="CLIENT">
<column name="ROOT_URL" type="VARCHAR(255)"/>
+ <column name="DESCRIPTION" type="VARCHAR(255)"/>
</addColumn>
<createTable tableName="OFFLINE_USER_SESSION">
+ <column name="USER_SESSION_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
- <column name="USER_SESSION_ID" type="VARCHAR(36)">
+ <column name="REALM_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="LAST_SESSION_REFRESH" type="INT"/>
+ <column name="OFFLINE" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="DATA" type="CLOB"/>
</createTable>
<createTable tableName="OFFLINE_CLIENT_SESSION">
- <column name="USER_ID" type="VARCHAR(36)">
- <constraints nullable="false"/>
- </column>
<column name="CLIENT_SESSION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
@@ -35,14 +40,19 @@
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
+ <column name="OFFLINE" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
<column name="DATA" type="CLOB"/>
</createTable>
- <addPrimaryKey columnNames="USER_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
- <addPrimaryKey columnNames="CLIENT_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
- <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_USER_SESSION" constraintName="FK_OFFLINE_US_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
- <addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
- <addForeignKeyConstraint baseColumnNames="USER_SESSION_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_US_SES" referencedColumnNames="USER_SESSION_ID" referencedTableName="OFFLINE_USER_SESSION"/>
+ <addPrimaryKey columnNames="USER_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
+ <addPrimaryKey columnNames="CLIENT_SESSION_ID, OFFLINE" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
+ <addColumn tableName="REALM">
+ <column name="REVOKE_REFRESH_TOKEN" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
+ </addColumn>
</changeSet>
</databaseChangeLog>
\ No newline at end of file
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 90e8c98..00f3386 100755
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -3,6 +3,7 @@ package org.keycloak.connections.mongo;
import com.mongodb.DB;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
+import com.mongodb.MongoClientURI;
import com.mongodb.MongoCredential;
import com.mongodb.ServerAddress;
@@ -35,6 +36,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity",
+ "org.keycloak.models.mongo.keycloak.entities.MongoOfflineUserSessionEntity",
"org.keycloak.models.entities.IdentityProviderEntity",
"org.keycloak.models.entities.ClientIdentityProviderMappingEntity",
"org.keycloak.models.entities.RequiredCredentialEntity",
@@ -48,8 +51,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.entities.AuthenticationFlowEntity",
"org.keycloak.models.entities.AuthenticatorConfigEntity",
"org.keycloak.models.entities.RequiredActionProviderEntity",
- "org.keycloak.models.entities.OfflineUserSessionEntity",
- "org.keycloak.models.entities.OfflineClientSessionEntity",
+ "org.keycloak.models.entities.PersistentUserSessionEntity",
+ "org.keycloak.models.entities.PersistentClientSessionEntity",
};
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
@@ -147,31 +150,55 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
* @throws UnknownHostException
*/
protected MongoClient createMongoClient() throws UnknownHostException {
- String host = config.get("host", ServerAddress.defaultHost());
- int port = config.getInt("port", ServerAddress.defaultPort());
+ operationalInfo = new LinkedHashMap<>();
String dbName = config.get("db", "keycloak");
- String user = config.get("user");
- String password = config.get("password");
+ String uriString = config.get("uri");
+ if (uriString != null) {
+ MongoClientURI uri = new MongoClientURI(uriString);
+ MongoClient client = new MongoClient(uri);
+
+ StringBuilder hostsBuilder = new StringBuilder();
+ for (int i=0 ; i<uri.getHosts().size() ; i++) {
+ if (i!=0) {
+ hostsBuilder.append(", ");
+ }
+ hostsBuilder.append(uri.getHosts().get(i));
+ }
+ String hosts = hostsBuilder.toString();
- MongoClientOptions clientOptions = getClientOptions();
+ operationalInfo.put("mongoHosts", hosts);
+ operationalInfo.put("mongoDatabaseName", dbName);
+ operationalInfo.put("mongoUser", uri.getUsername());
+ operationalInfo.put("mongoDriverVersion", client.getVersion());
- MongoClient client;
- if (user != null && password != null) {
- MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray());
- client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions);
+ logger.debugv("Initialized mongo model. host(s): %s, db: %s", uri.getHosts(), dbName);
+ return client;
} else {
- client = new MongoClient(new ServerAddress(host, port), clientOptions);
+ String host = config.get("host", ServerAddress.defaultHost());
+ int port = config.getInt("port", ServerAddress.defaultPort());
+
+ String user = config.get("user");
+ String password = config.get("password");
+
+ MongoClientOptions clientOptions = getClientOptions();
+
+ MongoClient client;
+ if (user != null && password != null) {
+ MongoCredential credential = MongoCredential.createMongoCRCredential(user, dbName, password.toCharArray());
+ client = new MongoClient(new ServerAddress(host, port), Collections.singletonList(credential), clientOptions);
+ } else {
+ client = new MongoClient(new ServerAddress(host, port), clientOptions);
+ }
+
+ operationalInfo.put("mongoServerAddress", client.getAddress().toString());
+ operationalInfo.put("mongoDatabaseName", dbName);
+ operationalInfo.put("mongoUser", user);
+ operationalInfo.put("mongoDriverVersion", client.getVersion());
+
+ logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
+ return client;
}
-
- operationalInfo = new LinkedHashMap<>();
- operationalInfo.put("mongoServerAddress", client.getAddress().toString());
- operationalInfo.put("mongoDatabaseName", dbName);
- operationalInfo.put("mongoUser", user);
- operationalInfo.put("mongoDriverVersion", client.getVersion());
-
- logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
- return client;
}
protected MongoClientOptions getClientOptions() {
diff --git a/core/src/main/java/org/keycloak/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index 022dadd..b552d1b 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -41,6 +41,9 @@ public interface OAuth2Constants {
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
String OFFLINE_ACCESS = "offline_access";
+ String UI_LOCALES_PARAM = "ui_locales";
+
+
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index 7ad8c6e..0999505 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -11,6 +11,7 @@ public class ClientRepresentation {
protected String id;
protected String clientId;
protected String name;
+ protected String description;
protected String rootUrl;
protected String adminUrl;
protected String baseUrl;
@@ -51,6 +52,14 @@ public class ClientRepresentation {
this.name = name;
}
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
public String getClientId() {
return clientId;
}
diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
index c1c53b3..1bec10b 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -10,6 +10,7 @@ public class RealmRepresentation {
protected String id;
protected String realm;
protected Integer notBefore;
+ protected Boolean revokeRefreshToken;
protected Integer accessTokenLifespan;
protected Integer ssoSessionIdleTimeout;
protected Integer ssoSessionMaxLifespan;
@@ -166,6 +167,14 @@ public class RealmRepresentation {
this.sslRequired = sslRequired;
}
+ public Boolean getRevokeRefreshToken() {
+ return revokeRefreshToken;
+ }
+
+ public void setRevokeRefreshToken(Boolean revokeRefreshToken) {
+ this.revokeRefreshToken = revokeRefreshToken;
+ }
+
public Integer getAccessTokenLifespan() {
return accessTokenLifespan;
}
dependencies/server-all/pom.xml 16(+0 -16)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 3776340..f54c396 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -141,22 +141,6 @@
<artifactId>mongo-java-driver</artifactId>
</dependency>
- <!-- export/import -->
- <dependency>
- <groupId>org.keycloak</groupId>
- <artifactId>keycloak-export-import-zip</artifactId>
- </dependency>
- <dependency>
- <groupId>de.idyl</groupId>
- <artifactId>winzipaes</artifactId>
- <exclusions>
- <exclusion>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcprov-jdk16</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
index 188a2a2..669cf41 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
@@ -22,6 +22,10 @@
"provider": "jpa"
},
+ "userSessionPersister": {
+ "provider": "jpa"
+ },
+
"timer": {
"provider": "basic"
},
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
index 32cd209..11f8141 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
@@ -21,7 +21,6 @@
<module name="org.keycloak.keycloak-export-import-api" services="import"/>
<module name="org.keycloak.keycloak-export-import-dir" services="import"/>
<module name="org.keycloak.keycloak-export-import-single-file" services="import"/>
- <module name="org.keycloak.keycloak-export-import-zip" services="import"/>
<module name="org.keycloak.keycloak-forms-common-freemarker" services="import"/>
<module name="org.keycloak.keycloak-forms-common-themes" services="import"/>
<module name="org.keycloak.keycloak-invalidation-cache-infinispan" services="import"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
index 122af6d..77ce3ad 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
@@ -31,7 +31,6 @@
<module name="org.keycloak.keycloak-export-import-api" services="import"/>
<module name="org.keycloak.keycloak-export-import-dir" services="import"/>
<module name="org.keycloak.keycloak-export-import-single-file" services="import"/>
- <module name="org.keycloak.keycloak-export-import-zip" services="import"/>
<module name="org.keycloak.keycloak-forms-common-freemarker" services="import"/>
<module name="org.keycloak.keycloak-forms-common-themes" services="import"/>
<module name="org.keycloak.keycloak-invalidation-cache-infinispan" services="import"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/build.xml b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
index 3941fb9..2276471 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/build.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
@@ -278,15 +278,6 @@
<maven-resource group="org.mongodb" artifact="mongo-java-driver"/>
</module-def>
- <!-- export/import -->
-
- <module-def name="org.keycloak.keycloak-export-import-zip">
- <maven-resource group="org.keycloak" artifact="keycloak-export-import-zip"/>
- </module-def>
- <module-def name="de.idyl.winzipaes">
- <maven-resource group="de.idyl" artifact="winzipaes"/>
- </module-def>
-
<module-def name="org.liquibase">
<maven-resource group="org.liquibase" artifact="liquibase-core"/>
</module-def>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
index 32cd209..11f8141 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-as7-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
@@ -21,7 +21,6 @@
<module name="org.keycloak.keycloak-export-import-api" services="import"/>
<module name="org.keycloak.keycloak-export-import-dir" services="import"/>
<module name="org.keycloak.keycloak-export-import-single-file" services="import"/>
- <module name="org.keycloak.keycloak-export-import-zip" services="import"/>
<module name="org.keycloak.keycloak-forms-common-freemarker" services="import"/>
<module name="org.keycloak.keycloak-forms-common-themes" services="import"/>
<module name="org.keycloak.keycloak-invalidation-cache-infinispan" services="import"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
index 75e1181..44703f8 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
@@ -31,7 +31,6 @@
<module name="org.keycloak.keycloak-export-import-api" services="import"/>
<module name="org.keycloak.keycloak-export-import-dir" services="import"/>
<module name="org.keycloak.keycloak-export-import-single-file" services="import"/>
- <module name="org.keycloak.keycloak-export-import-zip" services="import"/>
<module name="org.keycloak.keycloak-forms-common-freemarker" services="import"/>
<module name="org.keycloak.keycloak-forms-common-themes" services="import"/>
<module name="org.keycloak.keycloak-invalidation-cache-infinispan" services="import"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml b/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml
index 83ee9e3..580b1d7 100755
--- a/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml
+++ b/distribution/server-overlay/eap6/eap6-server-overlay/assembly.xml
@@ -20,6 +20,13 @@
<include>**/**</include>
</includes>
</fileSet>
+ <!--<fileSet>
+ <directory>cli</directory>
+ <includes>
+ <include>*.cli</include>
+ </includes>
+ <outputDirectory>bin</outputDirectory>
+ </fileSet>-->
</fileSets>
<files>
@@ -45,14 +52,6 @@
<source>src/main/providers/README.txt</source>
<outputDirectory>standalone/configuration/providers</outputDirectory>
</file>
- <file>
- <source>cli/keycloak-prepare.cli</source>
- <outputDirectory>bin</outputDirectory>
- </file>
- <file>
- <source>cli/keycloak-install.cli</source>
- <outputDirectory>bin</outputDirectory>
- </file>
</files>
</assembly>
diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli
index 000cbfa..191cb28 100644
--- a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli
+++ b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare.cli
@@ -1,2 +1,7 @@
/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true,enabled=true)
-/subsystem=logging/logger=org.jboss.resteasy.resteasy_jaxrs.i18n/:add(level=ERROR)
\ No newline at end of file
+/subsystem=logging/logger=org.jboss.resteasy.resteasy_jaxrs.i18n/:add(level=ERROR)
+/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak",start="EAGER")
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add()
\ No newline at end of file
diff --git a/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli
new file mode 100644
index 0000000..edcd1c6
--- /dev/null
+++ b/distribution/server-overlay/eap6/eap6-server-overlay/cli/keycloak-prepare-ha.cli
@@ -0,0 +1,8 @@
+/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true,enabled=true)
+/subsystem=logging/logger=org.jboss.resteasy.resteasy_jaxrs.i18n/:add(level=ERROR)
+/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
+/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
+/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
+/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
\ No newline at end of file
diff --git a/distribution/server-overlay/wf9-server-overlay/assembly.xml b/distribution/server-overlay/wf9-server-overlay/assembly.xml
index fc44079..0427f80 100755
--- a/distribution/server-overlay/wf9-server-overlay/assembly.xml
+++ b/distribution/server-overlay/wf9-server-overlay/assembly.xml
@@ -14,7 +14,6 @@
<outputDirectory>modules/system/layers/base</outputDirectory>
<includes>
<include>com/google/zxing/**</include>
- <include>de/idyl/winzipaes/**</include>
<include>org/freemarker/**</include>
<include>org/keycloak/**</include>
<include>org/liquibase/**</include>
@@ -48,7 +47,13 @@
</includes>
<outputDirectory></outputDirectory>
</fileSet>
-
+ <!--<fileSet>
+ <directory>cli</directory>
+ <includes>
+ <include>*.cli</include>
+ </includes>
+ <outputDirectory>bin</outputDirectory>
+ </fileSet>-->
</fileSets>
<files>
@@ -66,10 +71,6 @@
<source>${project.build.directory}/unpacked/keycloak-${project.version}/standalone/configuration/keycloak-server.json</source>
<outputDirectory>standalone/configuration</outputDirectory>
</file>
- <file>
- <source>cli/keycloak-install.cli</source>
- <outputDirectory>bin</outputDirectory>
- </file>
</files>
</assembly>
diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli
index ac5ca0b..cc59431 100644
--- a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli
+++ b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-install.cli
@@ -1,3 +1,2 @@
-/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
/extension=org.keycloak.keycloak-server-subsystem/:add(module=org.keycloak.keycloak-server-subsystem)
/subsystem=keycloak-server:add(web-context=auth)
\ No newline at end of file
diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli
new file mode 100644
index 0000000..dcad93a
--- /dev/null
+++ b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare.cli
@@ -0,0 +1,6 @@
+/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
+/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
+/subsystem=infinispan/cache-container=keycloak/local-cache=realms:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=users:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=sessions:add()
+/subsystem=infinispan/cache-container=keycloak/local-cache=loginFailures:add()
\ No newline at end of file
diff --git a/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli
new file mode 100644
index 0000000..5cfae38
--- /dev/null
+++ b/distribution/server-overlay/wf9-server-overlay/cli/keycloak-prepare-ha.cli
@@ -0,0 +1,7 @@
+/subsystem=datasources/data-source=KeycloakDS/:add(connection-url="jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE",driver-name=h2,jndi-name=java:jboss/datasources/KeycloakDS,password=sa,user-name=sa,use-java-context=true)
+/subsystem=infinispan/cache-container=keycloak:add(jndi-name="infinispan/Keycloak")
+/subsystem=infinispan/cache-container=keycloak/transport=TRANSPORT:add(lock-timeout=60000)
+/subsystem=infinispan/cache-container=keycloak/invalidation-cache=realms:add(mode="SYNC")
+/subsystem=infinispan/cache-container=keycloak/invalidation-cache=users:add(mode="SYNC")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=sessions:add(mode="SYNC",owners="1")
+/subsystem=infinispan/cache-container=keycloak/distributed-cache=loginFailures:add(mode="SYNC",owners="1")
\ No newline at end of file
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
index 492a847..fbd6016 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
@@ -8,12 +8,11 @@
<para>
You can export/import your database either to:
<itemizedlist>
- <listitem>Encrypted ZIP file on local filesystem</listitem>
<listitem>Directory on local filesystem</listitem>
<listitem>Single JSON file on your filesystem</listitem>
</itemizedlist>
- When importing using the "dir" or "zip" strategies, note that the files need to follow the naming convention specified below.
+ When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
@@ -21,27 +20,11 @@
</itemizedlist>
</para>
<para>
- Encrypted ZIP is recommended as export contains many sensitive informations like passwords of your users (even if they are hashed),
- but also their email addresses, and especially private keys of the realms. Directory and Single JSON file are useful especially
- for testing as data in the files are not protected. On the other hand, it's useful if you want to look at all your data in JSON
- files directly.
- </para>
- <para>
- If you import to ZIP or Directory, you can specify also the number of users to be stored in each JSON file. So if you have
+ If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para>
<para>
- So to export the content of your Keycloak database into encrypted ZIP, you can execute Keycloak server with the System properties like:
- <programlisting><![CDATA[
-bin/standalone.sh -Dkeycloak.migration.action=export
--Dkeycloak.migration.provider=zip -Dkeycloak.migration.zipFile=<FILE TO EXPORT TO>
--Dkeycloak.migration.zipPassword=<PASSWORD TO DECRYPT EXPORT>
-]]></programlisting>
- Then you can move or copy the encrypted ZIP file into second environment and you can trigger import from it into Keycloak server with the same command but use
- <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
- </para>
- <para>
To export into unencrypted directory you can use:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
@@ -80,7 +63,7 @@ bin/standalone.sh -Dkeycloak.migration.action=import
<term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem>
<para>
- can be used to specify for ZIP or Directory providers to specify where to import users.
+ can be used to specify for Directory providers to specify where to import users.
Possible values are:
<itemizedlist>
<listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
index 10027b9..25a393f 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/MigrationFromOlderVersions.xml
@@ -80,6 +80,19 @@
<section>
<title>Version specific migration</title>
<section>
+ <title>Migrating to 1.6.0.Final</title>
+ <simplesect>
+ <title>Refresh tokens are not reusable anymore</title>
+ <para>
+ Old versions of Keycloak allowed reusing refresh tokens multiple times. Keycloak no longer permits
+ this by default. When a refresh token is used to obtain a new access token a new refresh token is also
+ included. This new refresh token should be used next time the access token is refreshed. If this is
+ a problem for you it's possible to enable reuse of refresh tokens in the admin console under token
+ settings.
+ </para>
+ </simplesect>
+ </section>
+ <section>
<title>Migrating to 1.5.0.Final</title>
<simplesect>
<title>Realm and User cache providers</title>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
index c702020..8de75d7 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
@@ -59,30 +59,23 @@
(username: <emphasis>admin</emphasis> and password: <emphasis>admin</emphasis>). Keycloak will then prompt you to
enter in a new password.
</para>
- <para>
- To add Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) start the server with
- the desired server-config. Then execute the following CLI script:
- <programlisting><WILDFLY_HOME>/bin/jboss-cli.sh -c --file=keycloak-install.cli</programlisting>
- </para>
+ <!--<para>-->
+ <!--To add Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) start the server with-->
+ <!--the desired server-config. If you are running the server in standalone mode run:-->
+ <!--<programlisting><WILDFLY_HOME>/bin/jboss-cli.sh -c --file=keycloak-prepare.cli</programlisting>-->
+ <!--Or if you are running in clustering (HA) mode run:-->
+ <!--<programlisting><WILDFLY_HOME>/bin/jboss-cli.sh -c --file=keycloak-prepare-ha.cli</programlisting>-->
+ <!--After that you need to restart the server, you can do this with:-->
+ <!--<programlisting><WILDFLY_HOME>/bin/jboss-cli.sh -c :reload</programlisting>-->
+ <!--Finally you need to run:-->
+ <!--<programlisting><WILDFLY_HOME>/bin/jboss-cli.sh -c --file=keycloak-install.cli</programlisting>-->
+ <!--</para>-->
</section>
<section>
<title>Install on existing JBoss EAP 6.4.0.GA</title>
<para>
Same procedure as WildFly 9.0.0.Final, but download <literal>keycloak-overlay-eap6-&project.version;.zip</literal> or <literal>keycloak-overlay-eap6-&project.version;.tar.gz</literal>.
</para>
- <para>
- However, for EAP, adding Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) requires two CLI scripts. Start the server with
- the desired server-config. Then execute the following CLI scripts with a restart in between:
- <orderedlist>
- <listitem>
- <programlisting><EAP_HOME>/bin/jboss-cli.sh -c --file=keycloak-prepare.cli</programlisting>
- </listitem>
- <listitem>Restart the server with the same server-config.</listitem>
- <listitem>
- <programlisting><EAP_HOME>/bin/jboss-cli.sh -c --file=keycloak-install.cli</programlisting>
- </listitem>
- </orderedlist>
- </para>
</section>
<section>
<title id="demo_install">Install Development Bundle</title>
@@ -329,6 +322,30 @@
Supported long option is <literal>maxAutoConnectRetryTime</literal>. See <ulink url="http://api.mongodb.org/java/2.11.4/com/mongodb/MongoClientOptions.html">Mongo documentation</ulink>
for details about those options and their default values.
</para>
+ <para>
+ Alternatively, you can configure MongoDB using a MongoDB <ulink url="http://docs.mongodb.org/manual/reference/connection-string/">connection URI</ulink>.
+ In this case, you define all information concerning the connection and authentication within the URI, as described in the MongoDB documentation.
+ Please note that the database specified within the URI is only used for authentication. To change the database used by keycloak you have to set
+ <literal>db</literal> property as before. Therefore, a configuration like the
+ following
+ <programlisting><![CDATA[
+"connectionsMongo": {
+ "default": {
+ "uri": "mongodb://user:password@127.0.0.1/authentication",
+ "db": "keycloak"
+ }
+}
+]]></programlisting>
+ will authenticate the user against the authentication database, but store all keycloak related data in the keycloak database.
+
+ </para>
+ <section>
+ <title>MongoDB Replica Sets</title>
+ <para>
+ In order to use a mongo replica set for Keycloak, one has to use URI based configuration, which supports the
+ definition of replica sets out of the box: <literal>mongodb://host1:27017,host2:27017,host3:27017/</literal>.
+ </para>
+ </section>
</section>
<section>
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index ac93a4f..0a8b3ae 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -70,6 +70,7 @@ public enum EventType {
CUSTOM_REQUIRED_ACTION(true),
CUSTOM_REQUIRED_ACTION_ERROR(true),
EXECUTE_ACTIONS(true),
+ EXECUTE_ACTIONS_ERROR(true),
CLIENT_INFO(false),
CLIENT_INFO_ERROR(false),
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 88471d0..9963ea1 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -1,7 +1,7 @@
package org.keycloak.exportimport.util;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
import org.keycloak.util.Base64;
import org.codehaus.jackson.JsonEncoding;
@@ -11,7 +11,6 @@ import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
@@ -298,27 +297,27 @@ public class ExportUtils {
}
}
- // Offline sessions
- List<OfflineUserSessionRepresentation> offlineSessionReps = new LinkedList<>();
- Collection<OfflineUserSessionModel> offlineSessions = session.users().getOfflineUserSessions(realm, user);
- Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, user);
-
- Map<String, List<OfflineClientSessionModel>> processed = new HashMap<>();
- for (OfflineClientSessionModel clsm : offlineClientSessions) {
- String userSessionId = clsm.getUserSessionId();
- List<OfflineClientSessionModel> current = processed.get(userSessionId);
- if (current == null) {
- current = new LinkedList<>();
- processed.put(userSessionId, current);
- }
- current.add(clsm);
- }
-
- for (OfflineUserSessionModel userSession : offlineSessions) {
- OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId()));
- offlineSessionReps.add(sessionRep);
- }
- userRep.setOfflineUserSessions(offlineSessionReps);
+// // Offline sessions
+// List<OfflineUserSessionRepresentation> offlineSessionReps = new LinkedList<>();
+// Collection<PersistentUserSessionModel> offlineSessions = session.users().getOfflineUserSessions(realm, user);
+// Collection<PersistentClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, user);
+//
+// Map<String, List<PersistentClientSessionModel>> processed = new HashMap<>();
+// for (PersistentClientSessionModel clsm : offlineClientSessions) {
+// String userSessionId = clsm.getUserSessionId();
+// List<PersistentClientSessionModel> current = processed.get(userSessionId);
+// if (current == null) {
+// current = new LinkedList<>();
+// processed.put(userSessionId, current);
+// }
+// current.add(clsm);
+// }
+//
+// for (PersistentUserSessionModel userSession : offlineSessions) {
+// OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId()));
+// offlineSessionReps.add(sessionRep);
+// }
+// userRep.setOfflineUserSessions(offlineSessionReps);
return userRep;
}
export-import/pom.xml 1(+0 -1)
diff --git a/export-import/pom.xml b/export-import/pom.xml
index 35a6f0b..2c7026c 100755
--- a/export-import/pom.xml
+++ b/export-import/pom.xml
@@ -18,7 +18,6 @@
<module>export-import-api</module>
<module>export-import-dir</module>
<module>export-import-single-file</module>
- <module>export-import-zip</module>
</modules>
</project>
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index f782543..b285df6 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -51,7 +51,6 @@ import org.keycloak.events.Event;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
@@ -65,7 +64,6 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
-import org.keycloak.services.Urls;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -130,7 +128,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
logger.warn("Failed to load properties", e);
}
- Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers);
+ Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle;
try {
messagesBundle = theme.getMessages(locale);
@@ -213,10 +211,6 @@ public class FreeMarkerAccountProvider implements AccountProvider {
String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme);
Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
-
- String keycloakLocaleCookiePath = Urls.localeCookiePath(baseUri, realm.getName());
-
- LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, keycloakLocaleCookiePath);
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
index 71bf200..95ebe4f 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
@@ -13,7 +13,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
-import org.keycloak.services.offline.OfflineTokenUtils;
+import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.util.MultivaluedHashMap;
/**
@@ -25,7 +25,7 @@ public class ApplicationsBean {
public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
- Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
+ Set<ClientModel> offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
List<ClientModel> realmClients = realm.getClients();
for (ClientModel client : realmClients) {
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java
index 91ae108..aedf33e 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/beans/LocaleBean.java
@@ -1,6 +1,5 @@
package org.keycloak.freemarker.beans;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.models.RealmModel;
import javax.ws.rs.core.UriBuilder;
@@ -22,7 +21,7 @@ public class LocaleBean {
supported = new LinkedList<>();
for (String l : realm.getSupportedLocales()) {
String label = messages.getProperty("locale_" + l, l);
- String url = uriBuilder.replaceQueryParam(LocaleHelper.KC_LOCALE_PARAM, l).build().toString();
+ String url = uriBuilder.replaceQueryParam("kc_locale", l).build().toString();
supported.add(new Locale(label, url));
}
}
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_fr.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_fr.properties
index f34967d..6831687 100644
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_fr.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_fr.properties
@@ -1,4 +1,4 @@
-# TIPS to encode UTF-8 to ISO
+# TIPS to encode UTF-8 to ISO
# native2ascii -encoding ISO8859_1 srcFile > dstFile
doSave=Sauvegarder
@@ -45,19 +45,19 @@ role_view-users=Voir les utilisateurs
role_view-applications=Voir les applications
role_view-clients=Voir les clients
role_view-events=Voir les \u00e9v\u00e9nements
-role_view-identity-providers=Voir les fournisseurs d'identit\u00e9s
+role_view-identity-providers=Voir les fournisseurs d''identit\u00e9s
role_manage-realm=G\u00e9rer le domaine
role_manage-users=G\u00e9rer les utilisateurs
role_manage-applications=G\u00e9rer les applications
-role_manage-identity-providers=G\u00e9rer les fournisseurs d'identit\u00e9s
+role_manage-identity-providers=G\u00e9rer les fournisseurs d''identit\u00e9s
role_manage-clients=G\u00e9rer les clients
role_manage-events=G\u00e9rer les \u00e9v\u00e9nements
role_view-profile=Voir le profile
role_manage-account=G\u00e9rer le compte
-role_read-token=Lire le jeton d'authentification
+role_read-token=Lire le jeton d''authentification
role_offline-access=Acc\u00e9s hors-ligne
client_account=Compte
-client_security-admin-console=Console d'administration de la s\u00e9curit\u00e9
+client_security-admin-console=Console d''administration de la s\u00e9curit\u00e9
client_realm-management=Gestion du domaine
client_broker=Broker
@@ -65,7 +65,7 @@ client_broker=Broker
requiredFields=Champs obligatoires
allFieldsRequired=Tous les champs obligatoires
-backToApplication=« Revenir \u00e0 l'application
+backToApplication=« Revenir \u00e0 l''application
backTo=Revenir \u00e0 {0}
date=Date
@@ -86,68 +86,68 @@ sessions=Sessions
log=Connexion
application=Application
-availablePermissions=Permissions Disponibles
+availablePermissions=Permissions Disponibles
grantedPermissions=Permissions accord\u00e9es
grantedPersonalInfo=Informations personnels accord\u00e9es
additionalGrants=Droits additionnels
action=Action
inResource=dans
fullAccess=Acc\u00e9s complet
-offlineToken=Jeton d'authentification hors-ligne
+offlineToken=Jeton d''authentification hors-ligne
revoke=R\u00e9voquer un droit
configureAuthenticators=Authentifications configur\u00e9es.
mobile=T\u00e9l\u00e9phone mobile
-totpStep1=Installlez <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> ou bien Google Authenticator sur votre mobile. Ces deux applications sont disponibles sur <a href="https://play.google.com">Google Play</a> et Apple App Store.
-totpStep2=Ouvrez l'application et scanner le code bar ou entrez la clef.
-totpStep3=Entrez le code \u00e0 usage unique fourni par l'application et cliquez sur Sauvegarder pour terminer.
+totpStep1=Installez <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> ou bien Google Authenticator sur votre mobile. Ces deux applications sont disponibles sur <a href="https://play.google.com">Google Play</a> et Apple App Store.
+totpStep2=Ouvrez l''application et scanner le code bar ou entrez la clef.
+totpStep3=Entrez le code \u00e0 usage unique fourni par l''application et cliquez sur Sauvegarder pour terminer.
-missingUsernameMessage=Veuillez entrez votre nom d'utilisateur.
-missingFirstNameMessage=Veuillez entrez votre pr\u00e9nom.
+missingUsernameMessage=Veuillez entrer votre nom d''utilisateur.
+missingFirstNameMessage=Veuillez entrer votre pr\u00e9nom.
invalidEmailMessage=Courriel invalide.
-missingLastNameMessage=Veuillez entrez votre nom.
-missingEmailMessage=Veuillez entrez votre courriel.
-missingPasswordMessage=Veuillez entrez votre mot de passe.
+missingLastNameMessage=Veuillez entrer votre nom.
+missingEmailMessage=Veuillez entrer votre courriel.
+missingPasswordMessage=Veuillez entrer votre mot de passe.
notMatchPasswordMessage=Les mots de passe ne sont pas identiques
-missingTotpMessage=Veuillez entrez le code authentification.
+missingTotpMessage=Veuillez entrer le code authentification.
invalidPasswordExistingMessage=Mot de passe existant invalide.
invalidPasswordConfirmMessage=Le mot de passe de confirmation ne correspond pas.
-invalidTotpMessage=Le code d'authentification est invalide.
+invalidTotpMessage=Le code d''authentification est invalide.
-usernameExistsMessage=Le nom d'utilisateur existe d\u00e9j\u00e0.
+usernameExistsMessage=Le nom d''utilisateur existe d\u00e9j\u00e0.
emailExistsMessage=Le courriel existe d\u00e9j\u00e0.
readOnlyUserMessage=Vous ne pouvez pas mettre \u00e0 jour votre compte car il est en lecture seule.
readOnlyPasswordMessage=Vous ne pouvez pas mettre \u00e0 jour votre mot de passe car votre compte est en lecture seule.
-successTotpMessage=L'authentification via t\u00e9l\u00e9phone mobile est configur\u00e9e.
-successTotpRemovedMessage=L'authentification via t\u00e9l\u00e9phone mobile est supprim\u00e9e.
+successTotpMessage=L''authentification via t\u00e9l\u00e9phone mobile est configur\u00e9e.
+successTotpRemovedMessage=L''authentification via t\u00e9l\u00e9phone mobile est supprim\u00e9e.
successGrantRevokedMessage=Droit r\u00e9voqu\u00e9 avec succ\u00e8s.
accountUpdatedMessage=Votre compte a \u00e9t\u00e9 mis \u00e0 jour.
accountPasswordUpdatedMessage=Votre mot de passe a \u00e9t\u00e9 mis \u00e0 jour.
-missingIdentityProviderMessage=Le fournisseur d'identit\u00e9 n'est pas sp\u00e9cifi\u00e9.
+missingIdentityProviderMessage=Le fournisseur d''identit\u00e9 n''est pas sp\u00e9cifi\u00e9.
invalidFederatedIdentityActionMessage=Action manquante ou invalide.
-identityProviderNotFoundMessage=Le fournisseur d'identit\u00e9 sp\u00e9cifi\u00e9 n'est pas trouv\u00e9.
-federatedIdentityLinkNotActiveMessage=Cette identit\u00e9 n'est plus active dor\u00e9navant.
-federatedIdentityRemovingLastProviderMessage=Vous ne pouvez pas supprimer votre derni\u00e8re f\u00e9d\u00e9ration d'identit\u00e9 sans avoir de mot de passe sp\u00e9cifi\u00e9.
-identityProviderRedirectErrorMessage=Erreur de redirection vers le fournisseur d'identit\u00e9.
-identityProviderRemovedMessage=Le fournisseur d'identit\u00e9 a \u00e9t\u00e9 supprim\u00e9 correctement.
+identityProviderNotFoundMessage=Le fournisseur d''identit\u00e9 sp\u00e9cifi\u00e9 n''est pas trouv\u00e9.
+federatedIdentityLinkNotActiveMessage=Cette identit\u00e9 n''est plus active dor\u00e9navant.
+federatedIdentityRemovingLastProviderMessage=Vous ne pouvez pas supprimer votre derni\u00e8re f\u00e9d\u00e9ration d''identit\u00e9 sans avoir de mot de passe sp\u00e9cifi\u00e9.
+identityProviderRedirectErrorMessage=Erreur de redirection vers le fournisseur d''identit\u00e9.
+identityProviderRemovedMessage=Le fournisseur d''identit\u00e9 a \u00e9t\u00e9 supprim\u00e9 correctement.
accountDisabledMessage=Ce compte est d\u00e9sactiv\u00e9, veuillez contacter votre administrateur.
accountTemporarilyDisabledMessage=Ce compte est temporairement d\u00e9sactiv\u00e9, veuillez contacter votre administrateur ou r\u00e9essayez plus tard..
-invalidPasswordMinLengthMessage=Mot de passe invalide: longueur minimale {0}.
-invalidPasswordMinLowerCaseCharsMessage=Mot de passe invalide: doit contenir au moins {0} lettre(s) en minuscule.
-invalidPasswordMinDigitsMessage=Mot de passe invalide: doit contenir au moins {0} chiffre(s).
-invalidPasswordMinUpperCaseCharsMessage=Mot de passe invalide: doit contenir au moins {0} lettre(s) en majuscule.
-invalidPasswordMinSpecialCharsMessage=Mot de passe invalide: doit contenir au moins {0} caract�re(s) sp�ciaux.
-invalidPasswordNotUsernameMessage=Mot de passe invalide: ne doit pas �tre identique au nom d'utilisateur.
-invalidPasswordRegexPatternMessage=Mot de passe invalide: ne valide pas l'expression rationnelle.
-invalidPasswordHistoryMessage=Mot de passe invalide: ne doit pas �tre �gal aux {0} derniers mot de passe.
+invalidPasswordMinLengthMessage=Mot de passe invalide : longueur minimale {0}.
+invalidPasswordMinLowerCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.
+invalidPasswordMinDigitsMessage=Mot de passe invalide : doit contenir au moins {0} chiffre(s).
+invalidPasswordMinUpperCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.
+invalidPasswordMinSpecialCharsMessage=Mot de passe invalide : doit contenir au moins {0} caract\u00e8re(s) sp\u00e9ciaux.
+invalidPasswordNotUsernameMessage=Mot de passe invalide : ne doit pas etre identique au nom d''utilisateur.
+invalidPasswordRegexPatternMessage=Mot de passe invalide : ne valide pas l''expression rationnelle.
+invalidPasswordHistoryMessage=Mot de passe invalide : ne doit pas \u00eatre \u00e9gal aux {0} derniers mot de passe.
locale_de=German
locale_en=English
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
index 10549e1..87640e0 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_de.properties
@@ -160,7 +160,7 @@ select-file=de Select file
view-details=de View details
clear-import=de Clear import
client-id.tooltip=de Specifies ID referenced in URI and tokens. For example 'my-client'
-client.name.tooltip=de Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example: ${my_client}
+client.name.tooltip=de Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example\: ${my_client}
client.enabled.tooltip=de Disabled clients cannot initiate a login or have obtain access tokens.
consent-required=de Consent Required
consent-required.tooltip=de If enabled users have to consent to client access.
@@ -460,3 +460,4 @@ realm=de Realm
identity-provider-mappers=de Identity Provider Mappers
create-identity-provider-mapper=de Create Identity Provider Mapper
add-identity-provider-mapper=de Add Identity Provider Mapper
+client.description.tooltip=de Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index d5193e7..802645e 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -66,6 +66,8 @@ realm-cache-enabled=Realm Cache Enabled
realm-cache-enabled.tooltip=Enable/disable cache for realm, client and role data.
user-cache-enabled=User Cache Enabled
user-cache-enabled.tooltip=Enable/disable user and user role mapping cache.
+revoke-refresh-token=Revoke Refresh Token
+revoke-refresh-token.tooltip=If enabled refresh tokens can only be used once. Otherwise refresh tokens are not revoked when used and can be used multiple times.
sso-session-idle=SSO Session Idle
seconds=Seconds
minutes=Minutes
@@ -160,7 +162,7 @@ select-file=Select file
view-details=View details
clear-import=Clear import
client-id.tooltip=Specifies ID referenced in URI and tokens. For example 'my-client'
-client.name.tooltip=Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example: ${my_client}
+client.name.tooltip=Specifies display name of the client. For example 'My Client'. Supports keys for localized values as well. For example\: ${my_client}
client.enabled.tooltip=Disabled clients cannot initiate a login or have obtain access tokens.
consent-required=Consent Required
consent-required.tooltip=If enabled users have to consent to client access.
@@ -458,3 +460,4 @@ realm=Realm
identity-provider-mappers=Identity Provider Mappers
create-identity-provider-mapper=Create Identity Provider Mapper
add-identity-provider-mapper=Add Identity Provider Mapper
+client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 92a1702..4a1d5a3 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -2030,16 +2030,11 @@ module.filter('capitalize', function() {
if (!input) {
return;
}
- var result = input.substring(0, 1).toUpperCase();
- var s = input.substring(1);
- for (var i=0; i<s.length ; i++) {
- var c = s[i];
- if (c.match(/[A-Z]/)) {
- result = result.concat(" ")
- };
- result = result.concat(c);
+ var splittedWords = input.split(/\s+/);
+ for (var i=0; i<splittedWords.length ; i++) {
+ splittedWords[i] = splittedWords[i].charAt(0).toUpperCase() + splittedWords[i].slice(1);
};
- return result;
+ return splittedWords.join(" ");
};
});
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index 5d2b2dd..443cbe9 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -38,6 +38,13 @@
</div>
<kc-tooltip>{{:: 'client.name.tooltip' | translate}}</kc-tooltip>
</div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="client.description">
+ </div>
+ <kc-tooltip>{{:: 'client.description.tooltip' | translate}}</kc-tooltip>
+ </div>
<div class="form-group clearfix block">
<label class="col-md-2 control-label" for="enabled">{{:: 'enabled' | translate}}</label>
<div class="col-sm-6">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
index ccc9fd7..0768c28 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-flow.html
@@ -7,7 +7,7 @@
<div class="form-group">
<label class="col-md-2 control-label" for="alias">Alias </label>
<div class="col-sm-6">
- <input class="form-control" type="text" id="alias" name="alias" data-ng-model="flow.alias" autofocus>
+ <input class="form-control" type="text" id="alias" name="alias" data-ng-model="flow.alias" autofocus required>
</div>
<kc-tooltip>Specifies display name for the flow.</kc-tooltip>
</div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
index 5c337d3..b0d8797 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-events-config.html
@@ -46,7 +46,7 @@
<input ui-select2="eventSelectOptions" id="enabledEventTypes" ng-model="eventsConfig.enabledEventTypes" data-placeholder="Select event types..."/>
</div>
- <span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Configure what event types are saved. By default events related to login and users modifying their accounts are persisted." class="fa fa-info-circle"></span>
+ <span tooltip-trigger="mouseover mouseout" tooltip-placement="right" tooltip="Configure what event types are saved." class="fa fa-info-circle"></span>
</div>
<div class="form-group" data-ng-show="access.manageEvents && eventsConfig.eventsEnabled">
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
index a44b939..bb55022 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-tokens.html
@@ -4,6 +4,17 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<div class="form-group">
+ <label class="col-md-2 control-label" for="revokeRefreshToken">{{:: 'revoke-refresh-token' | translate}}</label>
+
+ <div class="col-md-6">
+ <input ng-model="realm.revokeRefreshToken" name="revokeRefreshToken" id="revokeRefreshToken" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
+ </div>
+
+ <kc-tooltip>{{:: 'revoke-refresh-token.tooltip' | translate}}
+ </kc-tooltip>
+ </div>
+
+ <div class="form-group">
<label class="col-md-2 control-label" for="ssoSessionIdleTimeout">{{:: 'sso-session-idle' | translate}}</label>
<div class="col-md-6 time-selector">
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
index 4cbbf0f..3445de5 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties
@@ -196,4 +196,5 @@ emailVerifiedMessage=Ihr E-Mail Adresse wurde erfolgreich verifiziert.
locale_de=Deutsch
locale_en=English
locale_it=Italian
-locale_pt-BR=Portugu\u00EAs (Brasil)
\ No newline at end of file
+locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Français
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
index 65ad002..6fa441c 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties
@@ -196,6 +196,7 @@ locale_de=German
locale_en=English
locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Français
backToApplication=« Back to Application
missingParameterMessage=Missing parameters\: {0}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties
new file mode 100644
index 0000000..22bd4ae
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_fr.properties
@@ -0,0 +1,205 @@
+doLogIn=Connexion
+doRegister=Enregistrement
+doCancel=Annuler
+doSubmit=Soumettre
+doYes=Oui
+doNo=Non
+doContinue=Continuer
+doAccept=Accepter
+doDecline=Decliner
+doForgotPassword=Mot de passe oublié ?
+doClickHere=Cliquez ici
+doImpersonate=Impersonate
+kerberosNotConfigured=Kerberos non configuré
+kerberosNotConfiguredTitle=Kerberos non configuré
+bypassKerberosDetail=Si vous n''êtes pas connecté via Kerberos ou bie que votre navigateur n''est opas configurer pour la connexion via Kerberos. Veuillez cliquer pour vous connecter via un autre moyen.
+kerberosNotSetUp=Kerberos n''est pas configuré. Connexion impossible.
+registerWithTitle=Enregistrement avec {0}
+registerWithTitleHtml=Enregistrement avec<strong>{0}</strong>
+loginTitle=Connecté {0}
+loginTitleHtml=Connecté <strong>{0}</strong>
+impersonateTitle={0} utilisateur impersonate
+impersonateTitleHtml=<strong>{0}</strong> utilisateur impersonate</strong>
+realmChoice=Domaine
+unknownUser=Utilisateur inconnu
+loginTotpTitle=Configuration de l''authentification par mobile
+loginProfileTitle=Mise à jour du compte
+loginTimeout=Le temps imparti pour la connexion est écoulé. Le processus de connexion redémarre depuis le debut.
+oauthGrantTitle=OAuth Grant
+oauthGrantTitleHtml=Accès temporaire pour <strong>{0}</strong> demandé par
+errorTitle=Nous sommes désolé ...
+errorTitleHtml=Nous sommes <strong>désolé</strong> ...
+emailVerifyTitle=Vérification du courriel
+emailForgotTitle=Mot de passe oublié ?
+updatePasswordTitle=Mise à jour du mot de passe
+codeSuccessTitle=Code succès
+codeErrorTitle=Code Erreur\: {0}
+
+termsTitle=Termes et Conditions
+termsTitleHtml=Termes et Conditions
+termsText=<p>Termes et conditions à définir</p>
+
+recaptchaFailed=Re-captcha invalide
+recaptchaNotConfigured=Re-captcha est requis, mais il n''est pas configuré
+consentDenied=Consentement refusé.
+
+noAccount=Nouvel utilisateur?
+username=Nom d''utilisateur
+usernameOrEmail=Nom d''utilisateur ou courriel
+firstName=Prénom
+givenName=Prénom
+fullName=Nom complet
+lastName=Nom
+familyName=Nom de famille
+email=Courriel
+password=Mot de passe
+passwordConfirm=Confirmation du mot de passe
+passwordNew=Nouveau mot de passe
+passwordNewConfirm=Confirmation du nouveau not de passe
+rememberMe=Se souvenir de moi
+authenticatorCode=Code à usage unique
+address=Adresse
+street=Rue
+locality=Ville ou Localité
+region=État, Province, ou Région
+postal_code=Code postal
+country=Pays
+emailVerified=Courriel vérifié
+gssDelegationCredential=GSS Delegation Credential
+
+loginTotpStep1=Installez <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> ou bien Google Authenticator sur votre mobile. Ces deux applications sont disponibles sur <a href="https://play.google.com">Google Play</a> et Apple App Store.
+loginTotpStep2=Ouvrez l''application et scanner le code bar ou entrez la clef.
+loginTotpStep3=Entrez le code \u00e0 usage unique fourni par l''application et cliquez sur Sauvegarder pour terminer.
+loginTotpOneTime=Code à usage unique
+
+oauthGrantRequest=Voulez-vous accorder ces privileges d''accès ?
+inResource=dans
+
+emailVerifyInstruction1=Un courriel avec des instructions à suivre vous a été envoyé.
+emailVerifyInstruction2=Vous n''avez pas reçu de code dans le courriel ?
+emailVerifyInstruction3=Pour renvoyer le courriel.
+
+backToLogin=« Retour à la connexion
+
+emailInstruction=Entrez votre nom d''utilisateur ou votre courriel, un email va vous \u00eatre envoyer vous permettant de créer un nouveau mot de passe.
+
+copyCodeInstruction=Copiez le code et recopiez le dans votre application :
+
+personalInfo=Information personnelle:
+role_admin=Adminitrateur
+role_realm-admin=Administrateur du domaine
+role_create-realm=Cr\u00e9er un domaine
+role_view-realm=Voir un domaine
+role_view-users=Voir les utilisateurs
+role_view-applications=Voir les applications
+role_view-clients=Voir les clients
+role_view-events=Voir les \u00e9v\u00e9nements
+role_view-identity-providers=Voir les fournisseurs d''identit\u00e9s
+role_manage-realm=G\u00e9rer le domaine
+role_manage-users=G\u00e9rer les utilisateurs
+role_manage-applications=G\u00e9rer les applications
+role_manage-identity-providers=G\u00e9rer les fournisseurs d''identit\u00e9s
+role_manage-clients=G\u00e9rer les clients
+role_manage-events=G\u00e9rer les \u00e9v\u00e9nements
+role_view-profile=Voir le profile
+role_manage-account=G\u00e9rer le compte
+role_read-token=Lire le jeton d''authentification
+role_offline-access=Acc\u00e9s hors-ligne
+client_account=Compte
+client_security-admin-console=Console d''administration de la s\u00e9curit\u00e9
+client_realm-management=Gestion du domaine
+client_broker=Broker
+
+invalidUserMessage=Nom d''utilisateur ou mot de passe invalide.
+invalidEmailMessage=Adresse courriel invalide.
+accountDisabledMessage=Compte désactivé, contactez votre administrateur.
+accountTemporarilyDisabledMessage=Ce compte est temporairement désactivé, contactez votre administrateur ou bien réassayer plus tard.
+expiredCodeMessage=Fin de connexion. Veuillez vous reconnecter.
+
+missingFirstNameMessage=Veuillez entrer votre pr\u00e9nom.
+missingLastNameMessage=Veuillez entrer votre nom.
+missingEmailMessage=Veuillez entrer votre courriel.
+missingUsernameMessage=Veuillez entrer votre nom d''utilisateur.
+missingPasswordMessage=Veuillez entrer votre mot de passe.
+missingTotpMessage=Veuillez entrer votre code d''authentification.
+notMatchPasswordMessage=Les mots de passe ne sont pas identiques.
+
+invalidPasswordExistingMessage=Mot de passe existant invalide.
+invalidPasswordConfirmMessage=Le mot de passe de confirmation ne correspond pas.
+invalidTotpMessage=Le code d''authentification est invalide.
+
+usernameExistsMessage=Le nom d''utilisateur existe d\u00e9j\u00e0.
+emailExistsMessage=Le courriel existe d\u00e9j\u00e0.
+
+federatedIdentityEmailExistsMessage=Cet utilisateur avec ce courriel existe déjà. Veuillez vous connecté au gestionnaire de compte pour lier le compte.
+federatedIdentityUsernameExistsMessage=Cet utilisateur avec ce nom d''utilisateur existe déjà. Veuillez vous connecté au gestionnaire de compte pour lier le compte.
+
+configureTotpMessage=Vous devez configurer l''authentification par mobile pour activer votre compte.
+updateProfileMessage=Vous devez mettre à jour votre profile pour activer votre compte.
+updatePasswordMessage=Vous devez changer votre mot de passe pour activer votre compte.
+verifyEmailMessage=Vous devez vérifier votre courriel pour activer votre compte.
+
+emailSentMessage=Vous devriez recevoir rapidement un courriel avec de plus ample instructions.
+emailSendErrorMessage=Erreur lors de l''envoie du courriel, veuillez essayer plus tard.
+
+accountUpdatedMessage=Votre compte a été mis à jour.
+accountPasswordUpdatedMessage=Votre mot de passe a été mis à jour.
+
+noAccessMessage=Aucun accès
+
+invalidPasswordMinLengthMessage=Mot de passe invalide : longueur minimale {0}.
+invalidPasswordMinDigitsMessage=Mot de passe invalide : doit contenir au moins {0} chiffre(s).
+invalidPasswordMinLowerCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.
+invalidPasswordMinUpperCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.
+invalidPasswordMinSpecialCharsMessage=Mot de passe invalide : doit contenir au moins {0} caract\u00e8re(s) sp\u00e9ciaux.
+invalidPasswordNotUsernameMessage=Mot de passe invalide : ne doit pas etre identique au nom d''utilisateur.
+invalidPasswordRegexPatternMessage=Mot de passe invalide : ne valide pas l''expression rationnelle.
+invalidPasswordHistoryMessage=Mot de passe invalide : ne doit pas \u00eatre \u00e9gal aux {0} derniers mot de passe.
+
+
+failedToProcessResponseMessage=Erreur lors du traitement de la réponse
+httpsRequiredMessage=Le protocole HTTPS est requis
+realmNotEnabledMessage=Le domaine n''est pas activé
+invalidRequestMessage=Requete invalide
+failedLogout=La déconnexion a échouée
+unknownLoginRequesterMessage=Compte inconnu du demandeur
+loginRequesterNotEnabledMessage=La connexion du demandeur n''est pas active
+bearerOnlyMessage=Les applications Bearer-only ne sont pas autorisées à initier la connexion par navigateur.
+directGrantsOnlyMessage=Les clients Direct-grants-only ne sont pas autorisés à initier la connexion par navigateur.
+invalidRedirectUriMessage=L''uri de redirection est invalide
+unsupportedNameIdFormatMessage=NameIDFormat non supporté
+invlidRequesterMessage=Demandeur invalide
+registrationNotAllowedMessage=L''enregistrement n''est pas autorisé
+resetCredentialNotAllowedMessage=La remise \u00e0 z\u00e9ro n''est pas autorisé
+
+permissionNotApprovedMessage=La permission n''est pas approuvée.
+noRelayStateInResponseMessage=Aucun état de relais dans la réponse du fournisseur d''identité.
+identityProviderAlreadyLinkedMessage=L''identité retournée par le fournisseur d''identité est déjà liée avec un autre utilisateur.
+insufficientPermissionMessage=Permissions insuffisantes pour lier les identités.
+couldNotProceedWithAuthenticationRequestMessage=Impossible de continuer avec la requête d''authentification vers le fournisseur d''identité.
+couldNotObtainTokenMessage=Impossible de récuperer le jeton du fournisseur d''identité.
+unexpectedErrorRetrievingTokenMessage=Erreur inattendue lors de la récupération du jeton provenant du fournisseur d''identité.
+unexpectedErrorHandlingResponseMessage=Erreur inattendue lors du traitement de la réponse provenant du fournisseur d''identité.
+identityProviderAuthenticationFailedMessage=L''authentification a échouée. Impossible de s''authentifier avec le fournisseur d''identité.
+couldNotSendAuthenticationRequestMessage=Impossible d''envoyer la requête d''authentification vers le fournisseur d''identité.
+unexpectedErrorHandlingRequestMessage=Erreur inattendue lors du traitement de la requête vers le fournisseur d''identité.
+invalidAccessCodeMessage=Code d''accès invalide.
+sessionNotActiveMessage=La session n''est pas active.
+invalidCodeMessage=Une erreur est survenue, veuillez vous reconnecter à votre application.
+identityProviderUnexpectedErrorMessage=Erreur inattendue lors de l''authentification avec fournisseur d''identité.
+identityProviderNotFoundMessage=Impossible de trouver le fournisseur d''identité avec ce identifiant.
+realmSupportsNoCredentialsMessage=Ce domaine ne supporte aucun type d''accréditation.
+identityProviderNotUniqueMessage=Ce domaine autorise plusieurs fournisseurs d''identité. Impossible de déterminer fournisseurs d''identité avec lequel s''authentifier.
+emailVerifiedMessage=Votre adresse courriel a été vérifiée.
+
+locale_de=German
+locale_en=English
+locale_it=Italian
+locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Français
+
+
+backToApplication=« Revenir à l''application
+missingParameterMessage=Paramètres manquants \: {0}
+clientNotFoundMessage=Client inconnu.
+invalidParameterMessage=Paramètres invalide \: {0}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
index 4833b9b..e353153 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties
@@ -189,6 +189,7 @@ locale_de=German
locale_en=English
locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
+locale_fr=Français
backToApplication=« Torna all''Applicazione
missingParameterMessage=Parametri Mancanti\: {0}
diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
index d2b1b65..e2a2257 100644
--- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
+++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties
@@ -194,6 +194,7 @@ locale_de=Deutsch
locale_en=English
locale_it=Italian
locale_pt-BR=Portugu\u00EAs (BR)
+locale_fr=Français
backToApplication=« Voltar para o aplicativo
missingParameterMessage=Par\u00E2metros que faltam\: {0}
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_fr.properties b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_fr.properties
new file mode 100755
index 0000000..20c6309
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/keycloak/email/messages/messages_fr.properties
@@ -0,0 +1,21 @@
+emailVerificationSubject=V\u00a9rification du courriel
+emailVerificationBody=Quelqu''un vient de cr\u00a9er un compte "{2}" avec votre courriel. Si c''est bien vous, veuillez cliquer sur le lien ci-dessous afin de v\u00a9rifier votre adresse de courriel\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon ignorer ce message.
+emailVerificationBodyHtml=<p>Quelqu''un a cr\u00a9er un compte "{2}" avec votre courriel. Si c''est bien vous, veuillez cliquer sur le lien ci-dessous afin de v\u00a9rifier votre adresse de courriel</p><p><a href="{0}">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon veuillez ignorer ce message.</p>
+passwordResetSubject=R\u00a9initialiser le mot de passe
+passwordResetBody=Quelqu''un vient de demander une reinitialisation de mot de passe de votre compte {2}. Si c''est bien vous, veuillez cliquer sur le lien ci-dessous pour le mettre \u00a0 jour .\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon ignorer ce message, aucun changement ne sera effectuer sur votre compte.
+passwordResetBodyHtml=<p>Quelqu''un vient de demander une reinitialisation de mot de passe de votre compte {2}. Si c''est bien vous, veuillez cliquer sur le lien ci-dessous pour le mettre \u00a0 jour.</p><p><a href="{0}">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon ignorer ce message, aucun changement ne sera effectuer sur votre compte.</p>
+executeActionsSubject=Mettre \u00a0 jour votre compte
+executeActionsBody=Votre adminitrateurvient de demander une mise \u00a0 jour de votre compte {2}. veuillez cliquer sur le lien ci-dessous afin commencer le processus.\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSi vous n''etes pas au courant de cette requete, ignorer ce message, aucun changement ne sera effectuer sur votre compte.
+executeActionsBodyHtml=<p>Votre adminitrateurvient de demander une mise \u00a0 jour de votre compte {2}. veuillez cliquer sur le lien ci-dessous afin commencer le processus.</p><p><a href="{0}">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Si vous n''etes pas au courant de cette requete, ignorer ce message, aucun changement ne sera effectuer sur votre compte.</p>
+eventLoginErrorSubject=Erreur de connexion
+eventLoginErrorBody=Une tentative de connexion a \u00a9t\u00a9 d\u00a9tect\u00a9e pour votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.
+eventLoginErrorBodyHtml=<p>Une tentative de connexion a \u00a9t\u00a9 d\u00a9tect\u00a9e pour votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.</p>
+eventRemoveTotpSubject=Suppression du TOTP
+eventRemoveTotpBody=Le TOTP a \u00a9t\u00a9 supprim\u00a9 de votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.
+eventRemoveTotpBodyHtml=<p>Le TOTP a \u00a9t\u00a9 supprim\u00a9 de votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.</p>
+eventUpdatePasswordSubject=Mise \u00a0 jour du mot de passe
+eventUpdatePasswordBody=Votre mot de passe \u00a0 chang\u00a9 sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.
+eventUpdatePasswordBodyHtml=<p>Votre mot de passe \u00a0 chang\u00a9 sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.</p>
+eventUpdateTotpSubject=Mise \u00a0 jour du TOTP
+eventUpdateTotpBody=Le TOTP a \u00a9t\u00a9 mis \u00a0 jour pour votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.
+eventUpdateTotpBodyHtml=<p>Le TOTP a \u00a9t\u00a9 mis \u00a0 jour pour votre compte sur {0} depuis {1}. Si ce n''est pas vous, veuillez contacter votre administrateur.</p>
forms/email-freemarker/pom.xml 5(+5 -0)
diff --git a/forms/email-freemarker/pom.xml b/forms/email-freemarker/pom.xml
index e1eff9d..0b9f540 100755
--- a/forms/email-freemarker/pom.xml
+++ b/forms/email-freemarker/pom.xml
@@ -31,6 +31,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-events-api</artifactId>
<scope>provided</scope>
</dependency>
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/ProfileBean.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/ProfileBean.java
new file mode 100755
index 0000000..812fb44
--- /dev/null
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/beans/ProfileBean.java
@@ -0,0 +1,77 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.email.freemarker.beans;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.UserModel;
+
+import javax.ws.rs.core.MultivaluedMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public class ProfileBean {
+
+ private static final Logger logger = Logger.getLogger(ProfileBean.class);
+
+ private UserModel user;
+ private final Map<String, String> attributes = new HashMap<>();
+
+ public ProfileBean(UserModel user) {
+ this.user = user;
+
+ if (user.getAttributes() != null) {
+ for (Map.Entry<String, List<String>> attr : user.getAttributes().entrySet()) {
+ List<String> attrValue = attr.getValue();
+ if (attrValue != null && attrValue.size() > 0) {
+ attributes.put(attr.getKey(), attrValue.get(0));
+ }
+
+ if (attrValue != null && attrValue.size() > 1) {
+ logger.warnf("There are more values for attribute '%s' of user '%s' . Will display just first value", attr.getKey(), user.getUsername());
+ }
+ }
+ }
+ }
+
+ public String getUsername() { return user.getUsername(); }
+
+ public String getFirstName() {
+ return user.getFirstName();
+ }
+
+ public String getLastName() {
+ return user.getLastName();
+ }
+
+ public String getEmail() {
+ return user.getEmail();
+ }
+
+ public Map<String, String> getAttributes() {
+ return attributes;
+ }
+}
diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
index b620a59..9f55e37 100755
--- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
+++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java
@@ -20,11 +20,11 @@ import org.jboss.logging.Logger;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.email.freemarker.beans.EventBean;
+import org.keycloak.email.freemarker.beans.ProfileBean;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
@@ -64,6 +64,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
@Override
public void sendEvent(Event event) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("user", new ProfileBean(user));
attributes.put("event", new EventBean(event));
send(toCamelCase(event.getType()) + "Subject", "event-" + event.getType().toString().toLowerCase() + ".ftl", attributes);
@@ -72,6 +73,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
@Override
public void sendPasswordReset(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -84,6 +86,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
@Override
public void sendExecuteActions(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -97,6 +100,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
@Override
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<String, Object>();
+ attributes.put("user", new ProfileBean(user));
attributes.put("link", link);
attributes.put("linkExpiration", expirationInMinutes);
@@ -110,7 +114,7 @@ public class FreeMarkerEmailProvider implements EmailProvider {
try {
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme = themeProvider.getTheme(realm.getEmailTheme(), Theme.Type.EMAIL);
- Locale locale = LocaleHelper.getLocale(realm, user);
+ Locale locale = session.getContext().resolveLocale(user);
attributes.put("locale", locale);
Properties rb = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, rb));
diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
index 1a16fbc..7fc1bcd 100755
--- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -24,7 +24,6 @@ import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
@@ -201,7 +200,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
Properties messagesBundle;
- Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders());
+ Locale locale = session.getContext().resolveLocale(user);
try {
messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
@@ -292,10 +291,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
-
- String cookiePath = Urls.localeCookiePath(baseUri, realm.getName());
- LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath);
-
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
@@ -345,7 +340,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
Properties messagesBundle;
- Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders());
+ Locale locale = session.getContext().resolveLocale(user);
try {
messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
@@ -393,9 +388,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
-
- String cookiePath = Urls.localeCookiePath(baseUri, realm.getName());
- LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, cookiePath);
return builder.build();
} catch (FreeMarkerException e) {
logger.error("Failed to process template", e);
diff --git a/model/api/src/main/java/org/keycloak/models/ClientModel.java b/model/api/src/main/java/org/keycloak/models/ClientModel.java
index ee4317c..8753f45 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientModel.java
@@ -28,6 +28,10 @@ public interface ClientModel extends RoleContainerModel {
void setName(String name);
+ String getDescription();
+
+ void setDescription(String description);
+
boolean isEnabled();
void setEnabled(boolean enabled);
diff --git a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
index 4c742ab..aa4f725 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
@@ -12,6 +12,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private String clientId;
private String name;
+ private String description;
private String realmId;
private boolean enabled;
private String clientAuthenticatorType;
@@ -61,6 +62,10 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.name = name;
}
+ public String getDescription() { return description; }
+
+ public void setDescription(String description) { this.description = description; }
+
public boolean isEnabled() {
return enabled;
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java b/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java
new file mode 100644
index 0000000..dd5262c
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/entities/PersistentUserSessionEntity.java
@@ -0,0 +1,64 @@
+package org.keycloak.models.entities;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class PersistentUserSessionEntity {
+
+ private String id;
+ private String realmId;
+ private String userId;
+ private int lastSessionRefresh;
+ private String data;
+ private List<PersistentClientSessionEntity> clientSessions;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public int getLastSessionRefresh() {
+ return lastSessionRefresh;
+ }
+
+ public void setLastSessionRefresh(int lastSessionRefresh) {
+ this.lastSessionRefresh = lastSessionRefresh;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ public List<PersistentClientSessionEntity> getClientSessions() {
+ return clientSessions;
+ }
+
+ public void setClientSessions(List<PersistentClientSessionEntity> clientSessions) {
+ this.clientSessions = clientSessions;
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
index 3dc43ef..e5fe6d2 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java
@@ -39,6 +39,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private int failureFactor;
//--- end brute force settings
+ private boolean revokeRefreshToken;
private int ssoSessionIdleTimeout;
private int ssoSessionMaxLifespan;
private int accessTokenLifespan;
@@ -229,6 +230,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.failureFactor = failureFactor;
}
+ public boolean isRevokeRefreshToken() {
+ return revokeRefreshToken;
+ }
+
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ this.revokeRefreshToken = revokeRefreshToken;
+ }
+
public int getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout;
}
diff --git a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
index 66020db..8c82a8e 100755
--- a/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
+++ b/model/api/src/main/java/org/keycloak/models/entities/UserEntity.java
@@ -28,7 +28,6 @@ public class UserEntity extends AbstractIdentifiableEntity {
private List<FederatedIdentityEntity> federatedIdentities;
private String federationLink;
private String serviceAccountClientLink;
- private List<OfflineUserSessionEntity> offlineUserSessions;
public String getUsername() {
return username;
@@ -158,13 +157,5 @@ public class UserEntity extends AbstractIdentifiableEntity {
public void setServiceAccountClientLink(String serviceAccountClientLink) {
this.serviceAccountClientLink = serviceAccountClientLink;
}
-
- public List<OfflineUserSessionEntity> getOfflineUserSessions() {
- return offlineUserSessions;
- }
-
- public void setOfflineUserSessions(List<OfflineUserSessionEntity> offlineUserSessions) {
- this.offlineUserSessions = offlineUserSessions;
- }
}
diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
index 760ffe4..e66927a 100755
--- a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
+++ b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java
@@ -5,6 +5,7 @@ import org.keycloak.models.utils.RealmImporter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
+import java.util.Locale;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -33,4 +34,6 @@ public interface KeycloakContext {
RealmImporter getRealmManager();
+ Locale resolveLocale(UserModel user);
+
}
diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java
index 13d111d..7471a4c 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -91,12 +91,18 @@ public interface RealmModel extends RoleContainerModel {
void setResetPasswordAllowed(boolean resetPasswordAllowed);
+ boolean isRevokeRefreshToken();
+ void setRevokeRefreshToken(boolean revokeRefreshToken);
+
int getSsoSessionIdleTimeout();
void setSsoSessionIdleTimeout(int seconds);
int getSsoSessionMaxLifespan();
void setSsoSessionMaxLifespan(int seconds);
+// int getOfflineSessionIdleTimeout();
+// void setOfflineSessionIdleTimeout(int seconds);
+
int getAccessTokenLifespan();
void setAccessTokenLifespan(int seconds);
@@ -286,6 +292,10 @@ public interface RealmModel extends RoleContainerModel {
void setEventsEnabled(boolean enabled);
+// boolean isPersistUserSessions();
+//
+// void setPersistUserSessions();
+
long getEventsExpiration();
void setEventsExpiration(long expiration);
diff --git a/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
new file mode 100644
index 0000000..6daf7c7
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/session/DisabledUserSessionPersisterProvider.java
@@ -0,0 +1,104 @@
+package org.keycloak.models.session;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.Config;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+
+/**
+ * Persistence of userSessions is disabled . Useful just if you never need survive of userSessions/clientSessions
+ * among server restart. Offline sessions / offline tokens will be invalid after server restart as well,
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class DisabledUserSessionPersisterProvider implements UserSessionPersisterProviderFactory, UserSessionPersisterProvider {
+
+ public static final String ID = "disabled";
+
+ @Override
+ public UserSessionPersisterProvider create(KeycloakSession session) {
+ return this;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public void createUserSession(UserSessionModel userSession, boolean offline) {
+
+ }
+
+ @Override
+ public void createClientSession(ClientSessionModel clientSession, boolean offline) {
+
+ }
+
+ @Override
+ public void updateUserSession(UserSessionModel userSession, boolean offline) {
+
+ }
+
+ @Override
+ public void removeUserSession(String userSessionId, boolean offline) {
+
+ }
+
+ @Override
+ public void removeClientSession(String clientSessionId, boolean offline) {
+
+ }
+
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+
+ }
+
+ @Override
+ public void onClientRemoved(RealmModel realm, ClientModel client) {
+
+ }
+
+ @Override
+ public void onUserRemoved(RealmModel realm, UserModel user) {
+
+ }
+
+ @Override
+ public void clearDetachedUserSessions() {
+
+ }
+
+ @Override
+ public List<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public int getUserSessionsCount(boolean offline) {
+ return 0;
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
new file mode 100644
index 0000000..4b3355e
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProvider.java
@@ -0,0 +1,43 @@
+package org.keycloak.models.session;
+
+import java.util.List;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserSessionPersisterProvider extends Provider {
+
+ // Persist just userSession. Not it's clientSessions
+ void createUserSession(UserSessionModel userSession, boolean offline);
+
+ // Assuming that corresponding userSession is already persisted
+ void createClientSession(ClientSessionModel clientSession, boolean offline);
+
+ void updateUserSession(UserSessionModel userSession, boolean offline);
+
+ // Called during logout (for online session) or during periodic expiration. It will remove all corresponding clientSessions too
+ void removeUserSession(String userSessionId, boolean offline);
+
+ // Called during revoke. It will remove userSession too if this was last clientSession attached to it
+ void removeClientSession(String clientSessionId, boolean offline);
+
+ void onRealmRemoved(RealmModel realm);
+ void onClientRemoved(RealmModel realm, ClientModel client);
+ void onUserRemoved(RealmModel realm, UserModel user);
+
+ // Called at startup to remove userSessions without any clientSession
+ void clearDetachedUserSessions();
+
+ // Called during startup. For each userSession, it loads also clientSessions
+ List<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline);
+
+ int getUserSessionsCount(boolean offline);
+
+}
diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
new file mode 100644
index 0000000..39e7b2e
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.models.session;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface UserSessionPersisterProviderFactory extends ProviderFactory<UserSessionPersisterProvider> {
+}
diff --git a/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java
new file mode 100644
index 0000000..cb66fa2
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/models/session/UserSessionPersisterSpi.java
@@ -0,0 +1,31 @@
+package org.keycloak.models.session;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionPersisterSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "userSessionPersister";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return UserSessionPersisterProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return UserSessionPersisterProviderFactory.class;
+ }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index b502084..bea51e0 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -476,70 +476,6 @@ public class UserFederationManager implements UserProvider {
}
@Override
- public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- session.userStorage().addOfflineUserSession(realm, user, offlineUserSession);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().getOfflineUserSession(realm, user, userSessionId);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().getOfflineUserSessions(realm, user);
- }
-
- @Override
- public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().removeOfflineUserSession(realm, user, userSessionId);
- }
-
- @Override
- public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
- session.userStorage().addOfflineClientSession(realm, offlineClientSession);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().getOfflineClientSession(realm, user, clientSessionId);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().getOfflineClientSessions(realm, user);
- }
-
- @Override
- public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- validateUser(realm, user);
- if (user == null) throw new IllegalStateException("Federated user no longer valid");
- return session.userStorage().removeOfflineClientSession(realm, user, clientSessionId);
- }
-
- @Override
- public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
- return session.userStorage().getOfflineClientSessionsCount(realm, client);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- return session.userStorage().getOfflineClientSessions(realm, client, firstResult, maxResults);
- }
-
- @Override
public void close() {
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index f57bb04..9dec778 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -1,6 +1,5 @@
package org.keycloak.models;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -10,11 +9,11 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface UserModel {
- public static final String USERNAME = "username";
- public static final String LAST_NAME = "lastName";
- public static final String FIRST_NAME = "firstName";
- public static final String EMAIL = "email";
- public static final String LOCALE = "locale";
+ String USERNAME = "username";
+ String LAST_NAME = "lastName";
+ String FIRST_NAME = "firstName";
+ String EMAIL = "email";
+ String LOCALE = "locale";
String getId();
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index 0012b24..7d7064d 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -56,17 +56,5 @@ public interface UserProvider extends Provider {
boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
- void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession);
- OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
- Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user);
- boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
- void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession);
- OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
- Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user);
- boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
-
- int getOfflineClientSessionsCount(RealmModel realm, ClientModel client);
- Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int first, int max);
-
void close();
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
index 12ebd70..8db5cdc 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -9,6 +9,7 @@ import java.util.Map;
public interface UserSessionModel {
String getId();
+ RealmModel getRealm();
/**
* If created via a broker external login, this is an identifier that can be
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
index 3b4d147..836cc75 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProvider.java
@@ -2,6 +2,7 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
+import java.util.Collection;
import java.util.List;
/**
@@ -27,6 +28,8 @@ public interface UserSessionProvider extends Provider {
int getActiveUserSessions(RealmModel realm, ClientModel client);
void removeUserSession(RealmModel realm, UserSessionModel session);
void removeUserSessions(RealmModel realm, UserModel user);
+
+ // Implementation should propagate removal of expired userSessions to userSessionPersister too
void removeExpiredUserSessions(RealmModel realm);
void removeUserSessions(RealmModel realm);
void removeClientSession(RealmModel realm, ClientSessionModel clientSession);
@@ -40,6 +43,22 @@ public interface UserSessionProvider extends Provider {
void onClientRemoved(RealmModel realm, ClientModel client);
void onUserRemoved(RealmModel realm, UserModel user);
+ UserSessionModel createOfflineUserSession(UserSessionModel userSession);
+ UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId);
+
+ // Removes the attached clientSessions as well
+ void removeOfflineUserSession(RealmModel realm, String userSessionId);
+
+ ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession);
+ ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId);
+ List<ClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user);
+
+ // Don't remove userSession even if it's last userSession
+ void removeOfflineClientSession(RealmModel realm, String clientSessionId);
+
+ int getOfflineSessionsCount(RealmModel realm, ClientModel client);
+ List<UserSessionModel> getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max);
+
void close();
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java b/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java
index b57b55e..93b6f32 100755
--- a/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java
+++ b/model/api/src/main/java/org/keycloak/models/UserSessionProviderFactory.java
@@ -7,4 +7,8 @@ import org.keycloak.provider.ProviderFactory;
* @version $Revision: 1 $
*/
public interface UserSessionProviderFactory extends ProviderFactory<UserSessionProvider> {
+
+ // This is supposed to prefill all userSessions and clientSessions from userSessionPersister to the userSession infinispan/memory storage
+ void loadPersistentSessions(KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment);
+
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 6b19606..6b4a6be 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -10,8 +10,8 @@ import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -144,6 +144,7 @@ public class ModelToRepresentation {
rep.setVerifyEmail(realm.isVerifyEmail());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setEditUsernameAllowed(realm.isEditUsernameAllowed());
+ rep.setRevokeRefreshToken(realm.isRevokeRefreshToken());
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
@@ -250,11 +251,11 @@ public class ModelToRepresentation {
}
if (realm.getEventsListeners() != null) {
- rep.setEventsListeners(new LinkedList<String>(realm.getEventsListeners()));
+ rep.setEventsListeners(new LinkedList<>(realm.getEventsListeners()));
}
if(realm.getEnabledEventTypes() != null) {
- rep.setEnabledEventTypes(new LinkedList<String>(realm.getEnabledEventTypes()));
+ rep.setEnabledEventTypes(new LinkedList<>(realm.getEnabledEventTypes()));
}
rep.setAdminEventsEnabled(realm.isAdminEventsEnabled());
@@ -299,6 +300,7 @@ public class ModelToRepresentation {
rep.setId(clientModel.getId());
rep.setClientId(clientModel.getClientId());
rep.setName(clientModel.getName());
+ rep.setDescription(clientModel.getDescription());
rep.setEnabled(clientModel.isEnabled());
rep.setAdminUrl(clientModel.getManagementUrl());
rep.setPublicClient(clientModel.isPublicClient());
@@ -511,13 +513,13 @@ public class ModelToRepresentation {
return rep;
}
- public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, OfflineUserSessionModel model, Collection<OfflineClientSessionModel> clientSessions) {
+ public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, PersistentUserSessionModel model, Collection<PersistentClientSessionModel> clientSessions) {
OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation();
rep.setData(model.getData());
rep.setUserSessionId(model.getUserSessionId());
List<OfflineClientSessionRepresentation> clientSessionReps = new LinkedList<>();
- for (OfflineClientSessionModel clsm : clientSessions) {
+ for (PersistentClientSessionModel clsm : clientSessions) {
OfflineClientSessionRepresentation clrep = toRepresentation(realm, clsm);
clientSessionReps.add(clrep);
}
@@ -525,7 +527,7 @@ public class ModelToRepresentation {
return rep;
}
- public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, OfflineClientSessionModel model) {
+ public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, PersistentClientSessionModel model) {
OfflineClientSessionRepresentation rep = new OfflineClientSessionRepresentation();
String clientInternalId = model.getClientId();
diff --git a/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java
index 6e95490..066f424 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/Pbkdf2PasswordEncoder.java
@@ -90,14 +90,7 @@ public class Pbkdf2PasswordEncoder {
public static byte[] getSalt() {
byte[] buffer = new byte[16];
- SecureRandom secureRandom;
-
- try {
- secureRandom = SecureRandom.getInstance(RNG_ALGORITHM);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException("RNG algorithm not found");
- }
-
+ SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(buffer);
return buffer;
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 4829182..6c61e66 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1,9 +1,5 @@
package org.keycloak.models.utils;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
-import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
-import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
import org.keycloak.util.Base64;
import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired;
@@ -100,6 +96,9 @@ public class RepresentationToModel {
if (rep.getNotBefore() != null) newRealm.setNotBefore(rep.getNotBefore());
+ if (rep.getRevokeRefreshToken() != null) newRealm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
+ else newRealm.setRevokeRefreshToken(false);
+
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
else newRealm.setAccessTokenLifespan(300);
@@ -532,6 +531,7 @@ public class RepresentationToModel {
if (rep.getAccessCodeLifespanUserAction() != null) realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
if (rep.getAccessCodeLifespanLogin() != null) realm.setAccessCodeLifespanLogin(rep.getAccessCodeLifespanLogin());
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
+ if (rep.getRevokeRefreshToken() != null) realm.setRevokeRefreshToken(rep.getRevokeRefreshToken());
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
@@ -692,6 +692,7 @@ public class RepresentationToModel {
ClientModel client = resourceRep.getId()!=null ? realm.addClient(resourceRep.getId(), resourceRep.getClientId()) : realm.addClient(resourceRep.getClientId());
if (resourceRep.getName() != null) client.setName(resourceRep.getName());
+ if(resourceRep.getDescription() != null) client.setDescription(resourceRep.getDescription());
if (resourceRep.isEnabled() != null) client.setEnabled(resourceRep.isEnabled());
client.setManagementUrl(resourceRep.getAdminUrl());
if (resourceRep.isSurrogateAuthRequired() != null)
@@ -793,6 +794,7 @@ public class RepresentationToModel {
public static void updateClient(ClientRepresentation rep, ClientModel resource) {
if (rep.getClientId() != null) resource.setClientId(rep.getClientId());
if (rep.getName() != null) resource.setName(rep.getName());
+ if (rep.getDescription() != null) resource.setDescription(rep.getDescription());
if (rep.isEnabled() != null) resource.setEnabled(rep.isEnabled());
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
@@ -985,11 +987,6 @@ public class RepresentationToModel {
user.addConsent(consentModel);
}
}
- if (userRep.getOfflineUserSessions() != null) {
- for (OfflineUserSessionRepresentation sessionRep : userRep.getOfflineUserSessions()) {
- importOfflineSession(session, newRealm, user, sessionRep);
- }
- }
if (userRep.getServiceAccountClientId() != null) {
String clientId = userRep.getServiceAccountClientId();
ClientModel client = clientMap.get(clientId);
@@ -1160,28 +1157,29 @@ public class RepresentationToModel {
return consentModel;
}
- public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(sessionRep.getUserSessionId());
- model.setData(sessionRep.getData());
- session.users().addOfflineUserSession(newRealm, user, model);
-
- for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) {
- OfflineClientSessionModel csModel = new OfflineClientSessionModel();
- String clientId = csRep.getClient();
- ClientModel client = newRealm.getClientByClientId(clientId);
- if (client == null) {
- throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername());
- }
- csModel.setClientId(client.getId());
- csModel.setUserId(user.getId());
- csModel.setClientSessionId(csRep.getClientSessionId());
- csModel.setUserSessionId(sessionRep.getUserSessionId());
- csModel.setData(csRep.getData());
-
- session.users().addOfflineClientSession(newRealm, csModel);
- }
- }
+ // TODO
+// public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) {
+// PersistentUserSessionModel model = new PersistentUserSessionModel();
+// model.setUserSessionId(sessionRep.getUserSessionId());
+// model.setData(sessionRep.getData());
+// session.users().createOfflineUserSession(newRealm, user, model);
+//
+// for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) {
+// PersistentClientSessionModel csModel = new PersistentClientSessionModel();
+// String clientId = csRep.getClient();
+// ClientModel client = newRealm.getClientByClientId(clientId);
+// if (client == null) {
+// throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername());
+// }
+// csModel.setClientId(client.getId());
+// csModel.setUserId(user.getId());
+// csModel.setClientSessionId(csRep.getClientSessionId());
+// csModel.setUserSessionId(sessionRep.getUserSessionId());
+// csModel.setData(csRep.getData());
+//
+// session.users().createOfflineClientSession(newRealm, csModel);
+// }
+// }
public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) {
AuthenticationFlowModel model = new AuthenticationFlowModel();
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index 5f73031..4cd162b 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -1,15 +1,12 @@
package org.keycloak.models.utils;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
-import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
new file mode 100644
index 0000000..dc21d4d
--- /dev/null
+++ b/model/api/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
@@ -0,0 +1 @@
+org.keycloak.models.session.DisabledUserSessionPersisterProvider
\ No newline at end of file
diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index ba45379..be3982b 100755
--- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -3,4 +3,5 @@ org.keycloak.mappers.UserFederationMapperSpi
org.keycloak.models.RealmSpi
org.keycloak.models.UserSessionSpi
org.keycloak.models.UserSpi
+org.keycloak.models.session.UserSessionPersisterSpi
org.keycloak.migration.MigrationSpi
\ No newline at end of file
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
index 1ddc364..81e60ee 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/ClientAdapter.java
@@ -79,6 +79,12 @@ public class ClientAdapter implements ClientModel {
}
@Override
+ public String getDescription() { return entity.getDescription(); }
+
+ @Override
+ public void setDescription(String description) { entity.setDescription(description); }
+
+ @Override
public Set<String> getWebOrigins() {
Set<String> result = new HashSet<String>();
if (entity.getWebOrigins() != null) {
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
index 5a62223..26227b1 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java
@@ -326,6 +326,16 @@ public class RealmAdapter implements RealmModel {
@Override
+ public boolean isRevokeRefreshToken() {
+ return realm.isRevokeRefreshToken();
+ }
+
+ @Override
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ realm.setRevokeRefreshToken(revokeRefreshToken);
+ }
+
+ @Override
public int getSsoSessionIdleTimeout() {
return realm.getSsoSessionIdleTimeout();
}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index 2c233d1..a4442c8 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -22,10 +22,7 @@ import org.keycloak.models.ClientModel;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
@@ -35,8 +32,6 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity;
import org.keycloak.models.entities.FederatedIdentityEntity;
-import org.keycloak.models.entities.OfflineClientSessionEntity;
-import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -44,7 +39,6 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.Time;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
@@ -53,8 +47,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
-
/**
* UserModel for JSON persistence.
*
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 9f4080a..8edfe3e 100755
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -24,8 +24,8 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -35,8 +35,8 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.entities.FederatedIdentityEntity;
-import org.keycloak.models.entities.OfflineClientSessionEntity;
-import org.keycloak.models.entities.OfflineUserSessionEntity;
+import org.keycloak.models.entities.PersistentClientSessionEntity;
+import org.keycloak.models.entities.PersistentUserSessionEntity;
import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.file.adapter.UserAdapter;
import org.keycloak.models.utils.CredentialValidation;
@@ -494,188 +494,4 @@ public class FileUserProvider implements UserProvider {
//throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
return null; // not supported yet
}
-
- @Override
- public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
-
- if (userEntity.getOfflineUserSessions() == null) {
- userEntity.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
- }
-
- if (getUserSessionEntityById(userEntity, userSession.getUserSessionId()) != null) {
- throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + userEntity.getUsername());
- }
-
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUserSessionId(userSession.getUserSessionId());
- entity.setData(userSession.getData());
- entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
- userEntity.getOfflineUserSessions().add(entity);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
-
- OfflineUserSessionEntity entity = getUserSessionEntityById(userEntity, userSessionId);
- return entity==null ? null : toModel(entity);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- if (user.getOfflineUserSessions()==null) {
- return Collections.emptyList();
- } else {
- List<OfflineUserSessionModel> result = new ArrayList<>();
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
- if (entity != null) {
- user.getOfflineUserSessions().remove(entity);
- return true;
- } else {
- return false;
- }
- }
-
- private OfflineUserSessionEntity getUserSessionEntityById(UserEntity user, String userSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return entity;
- }
- }
- }
- return null;
- }
-
- @Override
- public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
- UserModel userModel = getUserById(clientSession.getUserId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
- if (userSessionEntity == null) {
- throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
- }
-
- OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
- clEntity.setClientSessionId(clientSession.getClientSessionId());
- clEntity.setClientId(clientSession.getClientId());
- clEntity.setData(clientSession.getData());
-
- userSessionEntity.getOfflineClientSessions().add(clEntity);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- return toModel(clSession, userSession.getUserSessionId());
- }
- }
- }
- }
-
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(cls.getClientSessionId());
- model.setClientId(cls.getClientId());
- model.setData(cls.getData());
- model.setUserSessionId(userSessionId);
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- List<OfflineClientSessionModel> result = new ArrayList<>();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- result.add(toModel(clSession, userSession.getUserSessionId()));
- }
- }
- }
-
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
- userModel = getUserById(userModel.getId(), realm);
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- userSession.getOfflineClientSessions().remove(clSession);
- return true;
- }
- }
- }
- }
-
- return false;
- }
-
- @Override
- public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
- return getOfflineClientSessions(realm, client, -1, -1).size();
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- List<OfflineClientSessionModel> result = new LinkedList<>();
-
- List<UserModel> users = new ArrayList<>(inMemoryModel.getUsers(realm.getId()));
- users = sortedSubList(users, firstResult, maxResults);
-
- for (UserModel userModel : users) {
- UserEntity user = ((UserAdapter) userModel).getUserEntity();
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientId().equals(client.getId())) {
- result.add(toModel(clSession, userSession.getUserSessionId()));
- }
- }
- }
- }
- return result;
- }
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
index e181978..477c5d6 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/ClientAdapter.java
@@ -322,6 +322,18 @@ public class ClientAdapter implements ClientModel {
}
@Override
+ public String getDescription() {
+ if (updated != null) return updated.getDescription();
+ return cached.getDescription();
+ }
+
+ @Override
+ public void setDescription(String description) {
+ getDelegateForUpdate();
+ updated.setDescription(description);
+ }
+
+ @Override
public boolean isSurrogateAuthRequired() {
if (updated != null) return updated.isSurrogateAuthRequired();
return cached.isSurrogateAuthRequired();
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
index 584e91f..69fc5cf 100644
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
@@ -4,6 +4,8 @@ import org.keycloak.models.*;
import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.cache.entities.CachedUser;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import java.util.*;
@@ -121,7 +123,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
if (model == null) return null;
if (managedUsers.containsKey(id)) return managedUsers.get(id);
if (userInvalidations.containsKey(id)) return model;
- cached = new CachedUser(this, realm, model);
+ cached = new CachedUser(realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (managedUsers.containsKey(id)) {
return managedUsers.get(id);
@@ -146,7 +148,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
if (model == null) return null;
if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId());
if (userInvalidations.containsKey(model.getId())) return model;
- cached = new CachedUser(this, realm, model);
+ cached = new CachedUser(realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (userInvalidations.containsKey(cached.getId())) {
return getDelegate().getUserById(cached.getId(), realm);
@@ -173,7 +175,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
UserModel model = getDelegate().getUserByEmail(email, realm);
if (model == null) return null;
if (userInvalidations.containsKey(model.getId())) return model;
- cached = new CachedUser(this, realm, model);
+ cached = new CachedUser(realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (userInvalidations.containsKey(cached.getId())) {
return getDelegate().getUserByEmail(email, realm);
@@ -328,94 +330,4 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
getDelegate().preRemove(client, protocolMapper);
}
-
- @Override
- public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
- registerUserInvalidation(realm, user.getId());
- getDelegate().addOfflineUserSession(realm, user, offlineUserSession);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- if (isRegisteredForInvalidation(realm, user.getId())) {
- return getDelegate().getOfflineUserSession(realm, user, userSessionId);
- }
-
- CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
- if (cachedUser == null) {
- return getDelegate().getOfflineUserSession(realm, user, userSessionId);
- } else {
- return cachedUser.getOfflineUserSessions().get(userSessionId);
- }
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
- if (isRegisteredForInvalidation(realm, user.getId())) {
- return getDelegate().getOfflineUserSessions(realm, user);
- }
-
- CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
- if (cachedUser == null) {
- return getDelegate().getOfflineUserSessions(realm, user);
- } else {
- return cachedUser.getOfflineUserSessions().values();
- }
- }
-
- @Override
- public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- registerUserInvalidation(realm, user.getId());
- return getDelegate().removeOfflineUserSession(realm, user, userSessionId);
- }
-
- @Override
- public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
- registerUserInvalidation(realm, offlineClientSession.getUserId());
- getDelegate().addOfflineClientSession(realm, offlineClientSession);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- if (isRegisteredForInvalidation(realm, user.getId())) {
- return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
- }
-
- CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
- if (cachedUser == null) {
- return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
- } else {
- return cachedUser.getOfflineClientSessions().get(clientSessionId);
- }
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
- if (isRegisteredForInvalidation(realm, user.getId())) {
- return getDelegate().getOfflineClientSessions(realm, user);
- }
-
- CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
- if (cachedUser == null) {
- return getDelegate().getOfflineClientSessions(realm, user);
- } else {
- return cachedUser.getOfflineClientSessions().values();
- }
- }
-
- @Override
- public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- registerUserInvalidation(realm, user.getId());
- return getDelegate().removeOfflineClientSession(realm, user, clientSessionId);
- }
-
- @Override
- public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
- return getDelegate().getOfflineClientSessionsCount(realm, client);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- return getDelegate().getOfflineClientSessions(realm, client, firstResult, maxResults);
- }
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
index 54700e2..51d445c 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java
@@ -240,6 +240,18 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public boolean isRevokeRefreshToken() {
+ if (updated != null) return updated.isRevokeRefreshToken();
+ return cached.isRevokeRefreshToken();
+ }
+
+ @Override
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ getDelegateForUpdate();
+ updated.setRevokeRefreshToken(revokeRefreshToken);
+ }
+
+ @Override
public int getSsoSessionIdleTimeout() {
if (updated != null) return updated.getSsoSessionIdleTimeout();
return cached.getSsoSessionIdleTimeout();
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
index 3015acf..2d58222 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedClient.java
@@ -25,6 +25,7 @@ public class CachedClient implements Serializable {
private String id;
private String clientId;
private String name;
+ private String description;
private String realm;
private Set<String> redirectUris = new HashSet<String>();
private boolean enabled;
@@ -58,6 +59,7 @@ public class CachedClient implements Serializable {
secret = model.getSecret();
clientId = model.getClientId();
name = model.getName();
+ description = model.getDescription();
this.realm = realm.getId();
enabled = model.isEnabled();
protocol = model.getProtocol();
@@ -103,6 +105,10 @@ public class CachedClient implements Serializable {
return name;
}
+ public String getDescription() { return description; }
+
+ public void setDescription(String description) { this.description = description; }
+
public String getRealm() {
return realm;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
index 27bab74..5193588 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java
@@ -55,6 +55,7 @@ public class CachedRealm implements Serializable {
private int failureFactor;
//--- end brute force settings
+ private boolean revokeRefreshToken;
private int ssoSessionIdleTimeout;
private int ssoSessionMaxLifespan;
private int accessTokenLifespan;
@@ -136,6 +137,7 @@ public class CachedRealm implements Serializable {
failureFactor = model.getFailureFactor();
//--- end brute force settings
+ revokeRefreshToken = model.isRevokeRefreshToken();
ssoSessionIdleTimeout = model.getSsoSessionIdleTimeout();
ssoSessionMaxLifespan = model.getSsoSessionMaxLifespan();
accessTokenLifespan = model.getAccessTokenLifespan();
@@ -313,6 +315,10 @@ public class CachedRealm implements Serializable {
return editUsernameAllowed;
}
+ public boolean isRevokeRefreshToken() {
+ return revokeRefreshToken;
+ }
+
public int getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index 24e866a..0083856 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -1,20 +1,15 @@
package org.keycloak.models.cache.entities;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
-import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.Set;
/**
@@ -30,18 +25,16 @@ public class CachedUser implements Serializable {
private String lastName;
private String email;
private boolean emailVerified;
- private List<UserCredentialValueModel> credentials = new LinkedList<UserCredentialValueModel>();
+ private List<UserCredentialValueModel> credentials = new LinkedList<>();
private boolean enabled;
private boolean totp;
private String federationLink;
private String serviceAccountClientLink;
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> requiredActions = new HashSet<>();
- private Set<String> roleMappings = new HashSet<String>();
- private Map<String, OfflineUserSessionModel> offlineUserSessions = new HashMap<>();
- private Map<String, OfflineClientSessionModel> offlineClientSessions = new HashMap<>();
+ private Set<String> roleMappings = new HashSet<>();
- public CachedUser(CacheUserProvider cacheUserProvider, RealmModel realm, UserModel user) {
+ public CachedUser(RealmModel realm, UserModel user) {
this.id = user.getId();
this.realm = realm.getId();
this.username = user.getUsername();
@@ -60,12 +53,6 @@ public class CachedUser implements Serializable {
for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId());
}
- for (OfflineUserSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineUserSessions(realm, user)) {
- offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession);
- }
- for (OfflineClientSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineClientSessions(realm, user)) {
- offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession);
- }
}
public String getId() {
@@ -131,12 +118,4 @@ public class CachedUser implements Serializable {
public String getServiceAccountClientLink() {
return serviceAccountClientLink;
}
-
- public Map<String, OfflineUserSessionModel> getOfflineUserSessions() {
- return offlineUserSessions;
- }
-
- public Map<String, OfflineClientSessionModel> getOfflineClientSessions() {
- return offlineClientSessions;
- }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
index 1e0c21e..3baddd1 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/ClientAdapter.java
@@ -66,6 +66,12 @@ public class ClientAdapter implements ClientModel {
}
@Override
+ public String getDescription() { return entity.getDescription(); }
+
+ @Override
+ public void setDescription(String description) { entity.setDescription(description); }
+
+ @Override
public boolean isEnabled() {
return entity.isEnabled();
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
index b0a30cd..f26f651 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/ClientEntity.java
@@ -34,6 +34,8 @@ public class ClientEntity {
private String id;
@Column(name = "NAME")
private String name;
+ @Column(name = "DESCRIPTION")
+ private String description;
@Column(name = "CLIENT_ID")
private String clientId;
@Column(name="ENABLED")
@@ -143,6 +145,14 @@ public class ClientEntity {
this.name = name;
}
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
public boolean isEnabled() {
return enabled;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
index fbfb700..27c4824 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java
@@ -76,6 +76,8 @@ public class RealmEntity {
@Column(name="EDIT_USERNAME_ALLOWED")
protected boolean editUsernameAllowed;
+ @Column(name="REVOKE_REFRESH_TOKEN")
+ private boolean revokeRefreshToken;
@Column(name="SSO_IDLE_TIMEOUT")
private int ssoSessionIdleTimeout;
@Column(name="SSO_MAX_LIFESPAN")
@@ -288,6 +290,14 @@ public class RealmEntity {
this.editUsernameAllowed = editUsernameAllowed;
}
+ public boolean isRevokeRefreshToken() {
+ return revokeRefreshToken;
+ }
+
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ this.revokeRefreshToken = revokeRefreshToken;
+ }
+
public int getSsoSessionIdleTimeout() {
return ssoSessionIdleTimeout;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
index 24d23db..2da1641 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserEntity.java
@@ -3,13 +3,9 @@ package org.keycloak.models.jpa.entities;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.CascadeType;
-import javax.persistence.CollectionTable;
import javax.persistence.Column;
-import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.MapKeyColumn;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
@@ -18,8 +14,6 @@ import javax.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -89,12 +83,6 @@ public class UserEntity {
@Column(name="SERVICE_ACCOUNT_CLIENT_LINK")
protected String serviceAccountClientLink;
- @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
- protected Collection<OfflineUserSessionEntity> offlineUserSessions = new ArrayList<>();
-
- @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
- protected Collection<OfflineClientSessionEntity> offlineClientSessions = new ArrayList<>();
-
public String getId() {
return id;
}
@@ -224,22 +212,6 @@ public class UserEntity {
this.serviceAccountClientLink = serviceAccountClientLink;
}
- public Collection<OfflineUserSessionEntity> getOfflineUserSessions() {
- return offlineUserSessions;
- }
-
- public void setOfflineUserSessions(Collection<OfflineUserSessionEntity> offlineUserSessions) {
- this.offlineUserSessions = offlineUserSessions;
- }
-
- public Collection<OfflineClientSessionEntity> getOfflineClientSessions() {
- return offlineClientSessions;
- }
-
- public void setOfflineClientSessions(Collection<OfflineClientSessionEntity> offlineClientSessions) {
- this.offlineClientSessions = offlineClientSessions;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 563e898..d4d533a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -4,8 +4,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -15,21 +13,15 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
-import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
-import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
-import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -175,10 +167,6 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
- num = em.createNamedQuery("deleteOfflineClientSessionsByRealm")
- .setParameter("realmId", realm.getId()).executeUpdate();
- num = em.createNamedQuery("deleteOfflineUserSessionsByRealm")
- .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUsersByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
}
@@ -205,14 +193,6 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.executeUpdate();
- num = em.createNamedQuery("deleteOfflineClientSessionsByRealmAndLink")
- .setParameter("realmId", realm.getId())
- .setParameter("link", link.getId())
- .executeUpdate();
- num = em.createNamedQuery("deleteOfflineUserSessionsByRealmAndLink")
- .setParameter("realmId", realm.getId())
- .setParameter("link", link.getId())
- .executeUpdate();
num = em.createNamedQuery("deleteUsersByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
@@ -230,8 +210,6 @@ public class JpaUserProvider implements UserProvider {
em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
- em.createNamedQuery("deleteOfflineClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
- em.createNamedQuery("deleteDetachedOfflineUserSessions").executeUpdate();
}
@Override
@@ -479,167 +457,4 @@ public class JpaUserProvider implements UserProvider {
// Not supported yet
return null;
}
-
- @Override
- public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUser(userEntity);
- entity.setUserSessionId(offlineUserSession.getUserSessionId());
- entity.setData(offlineUserSession.getData());
- em.persist(entity);
- userEntity.getOfflineUserSessions().add(entity);
- em.flush();
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return toModel(entity);
- }
- }
- return null;
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- List<OfflineUserSessionModel> result = new LinkedList<>();
- for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
-
- @Override
- public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- OfflineUserSessionEntity found = null;
- for (OfflineUserSessionEntity session : userEntity.getOfflineUserSessions()) {
- if (session.getUserSessionId().equals(userSessionId)) {
- found = session;
- break;
- }
- }
-
- if (found == null) {
- return false;
- } else {
- userEntity.getOfflineUserSessions().remove(found);
- em.remove(found);
- em.flush();
- return true;
- }
- }
-
- @Override
- public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
- UserEntity userEntity = em.getReference(UserEntity.class, offlineClientSession.getUserId());
-
- OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
- entity.setUser(userEntity);
- entity.setClientSessionId(offlineClientSession.getClientSessionId());
- entity.setUserSessionId(offlineClientSession.getUserSessionId());
- entity.setClientId(offlineClientSession.getClientId());
- entity.setData(offlineClientSession.getData());
- em.persist(entity);
- userEntity.getOfflineClientSessions().add(entity);
- em.flush();
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
- if (entity.getClientSessionId().equals(clientSessionId)) {
- return toModel(entity);
- }
- }
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(entity.getClientSessionId());
- model.setClientId(entity.getClientId());
- model.setUserId(entity.getUser().getId());
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- List<OfflineClientSessionModel> result = new LinkedList<>();
- for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
- UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
-
- OfflineClientSessionEntity found = null;
- for (OfflineClientSessionEntity session : userEntity.getOfflineClientSessions()) {
- if (session.getClientSessionId().equals(clientSessionId)) {
- found = session;
- break;
- }
- }
-
- if (found == null) {
- return false;
- } else {
- userEntity.getOfflineClientSessions().remove(found);
- em.remove(found);
- em.flush();
- return true;
- }
- }
-
- @Override
- public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
- Query query = em.createNamedQuery("findOfflineClientSessionsCountByClient");
- query.setParameter("clientId", client.getId());
- Number n = (Number) query.getSingleResult();
- return n.intValue();
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- TypedQuery<OfflineClientSessionEntity> query = em.createNamedQuery("findOfflineClientSessionsByClient", OfflineClientSessionEntity.class);
- query.setParameter("clientId", client.getId());
-
- if (firstResult != -1) {
- query.setFirstResult(firstResult);
- }
- if (maxResults != -1) {
- query.setMaxResults(maxResults);
- }
-
- List<OfflineClientSessionEntity> results = query.getResultList();
- Set<OfflineClientSessionModel> set = new HashSet<>();
- for (OfflineClientSessionEntity entity : results) {
- set.add(toModel(entity));
- }
- return set;
- }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index 6268735..2c8e2ad 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -337,6 +337,16 @@ public class RealmAdapter implements RealmModel {
}
@Override
+ public boolean isRevokeRefreshToken() {
+ return realm.isRevokeRefreshToken();
+ }
+
+ @Override
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ realm.setRevokeRefreshToken(revokeRefreshToken);
+ }
+
+ @Override
public int getAccessTokenLifespan() {
return realm.getAccessTokenLifespan();
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
new file mode 100644
index 0000000..ffc0455
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
@@ -0,0 +1,239 @@
+package org.keycloak.models.jpa.session;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import javax.persistence.TypedQuery;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionAdapter;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionAdapter;
+import org.keycloak.models.session.PersistentUserSessionModel;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JpaUserSessionPersisterProvider implements UserSessionPersisterProvider {
+
+ private final KeycloakSession session;
+ private final EntityManager em;
+
+ public JpaUserSessionPersisterProvider(KeycloakSession session, EntityManager em) {
+ this.session = session;
+ this.em = em;
+ }
+
+ @Override
+ public void createUserSession(UserSessionModel userSession, boolean offline) {
+ PersistentUserSessionAdapter adapter = new PersistentUserSessionAdapter(userSession);
+ PersistentUserSessionModel model = adapter.getUpdatedModel();
+
+ PersistentUserSessionEntity entity = new PersistentUserSessionEntity();
+ entity.setUserSessionId(model.getUserSessionId());
+ entity.setRealmId(adapter.getRealm().getId());
+ entity.setUserId(adapter.getUser().getId());
+ entity.setOffline(offline);
+ entity.setLastSessionRefresh(model.getLastSessionRefresh());
+ entity.setData(model.getData());
+ em.persist(entity);
+ em.flush();
+ }
+
+ @Override
+ public void createClientSession(ClientSessionModel clientSession, boolean offline) {
+ PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession);
+ PersistentClientSessionModel model = adapter.getUpdatedModel();
+
+ PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
+ entity.setClientSessionId(clientSession.getId());
+ entity.setClientId(clientSession.getClient().getId());
+ entity.setOffline(offline);
+ entity.setUserSessionId(clientSession.getUserSession().getId());
+ entity.setData(model.getData());
+ em.persist(entity);
+ em.flush();
+ }
+
+ @Override
+ public void updateUserSession(UserSessionModel userSession, boolean offline) {
+ PersistentUserSessionAdapter adapter;
+ if (userSession instanceof PersistentUserSessionAdapter) {
+ adapter = (PersistentUserSessionAdapter) userSession;
+ } else {
+ adapter = new PersistentUserSessionAdapter(userSession);
+ }
+
+ PersistentUserSessionModel model = adapter.getUpdatedModel();
+
+ PersistentUserSessionEntity entity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(userSession.getId(), offline));
+ if (entity == null) {
+ throw new ModelException("UserSession with ID " + userSession.getId() + ", offline: " + offline + " not found");
+ }
+ entity.setLastSessionRefresh(model.getLastSessionRefresh());
+ entity.setData(model.getData());
+ }
+
+ @Override
+ public void removeUserSession(String userSessionId, boolean offline) {
+ em.createNamedQuery("deleteClientSessionsByUserSession")
+ .setParameter("userSessionId", userSessionId)
+ .setParameter("offline", offline)
+ .executeUpdate();
+
+ PersistentUserSessionEntity sessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(userSessionId, offline));
+ if (sessionEntity != null) {
+ em.remove(sessionEntity);
+ em.flush();
+ }
+ }
+
+ @Override
+ public void removeClientSession(String clientSessionId, boolean offline) {
+ PersistentClientSessionEntity sessionEntity = em.find(PersistentClientSessionEntity.class, new PersistentClientSessionEntity.Key(clientSessionId, offline));
+ if (sessionEntity != null) {
+ em.remove(sessionEntity);
+
+ // Remove userSession if it was last clientSession
+ List<PersistentClientSessionEntity> clientSessions = getClientSessionsByUserSession(sessionEntity.getUserSessionId(), offline);
+ if (clientSessions.size() == 0) {
+ PersistentUserSessionEntity userSessionEntity = em.find(PersistentUserSessionEntity.class, new PersistentUserSessionEntity.Key(sessionEntity.getUserSessionId(), offline));
+ if (userSessionEntity != null) {
+ em.remove(userSessionEntity);
+ }
+ }
+
+ em.flush();
+ }
+ }
+
+ private List<PersistentClientSessionEntity> getClientSessionsByUserSession(String userSessionId, boolean offline) {
+ TypedQuery<PersistentClientSessionEntity> query = em.createNamedQuery("findClientSessionsByUserSession", PersistentClientSessionEntity.class);
+ query.setParameter("userSessionId", userSessionId);
+ query.setParameter("offline", offline);
+ return query.getResultList();
+ }
+
+
+
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+ em.createNamedQuery("deleteClientSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+ em.createNamedQuery("deleteUserSessionsByRealm").setParameter("realmId", realm.getId()).executeUpdate();
+ }
+
+ @Override
+ public void onClientRemoved(RealmModel realm, ClientModel client) {
+ em.createNamedQuery("deleteClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
+ em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
+ }
+
+ @Override
+ public void onUserRemoved(RealmModel realm, UserModel user) {
+ em.createNamedQuery("deleteClientSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
+ em.createNamedQuery("deleteUserSessionsByUser").setParameter("userId", user.getId()).executeUpdate();
+ }
+
+ @Override
+ public void clearDetachedUserSessions() {
+ em.createNamedQuery("deleteDetachedClientSessions").executeUpdate();
+ em.createNamedQuery("deleteDetachedUserSessions").executeUpdate();
+ }
+
+ @Override
+ public List<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline) {
+ TypedQuery<PersistentUserSessionEntity> query = em.createNamedQuery("findUserSessions", PersistentUserSessionEntity.class);
+ query.setParameter("offline", offline);
+
+ if (firstResult != -1) {
+ query.setFirstResult(firstResult);
+ }
+ if (maxResults != -1) {
+ query.setMaxResults(maxResults);
+ }
+
+ List<PersistentUserSessionEntity> results = query.getResultList();
+ List<UserSessionModel> result = new ArrayList<>();
+ List<String> userSessionIds = new ArrayList<>();
+ for (PersistentUserSessionEntity entity : results) {
+ result.add(toAdapter(entity));
+ userSessionIds.add(entity.getUserSessionId());
+ }
+
+ TypedQuery<PersistentClientSessionEntity> query2 = em.createNamedQuery("findClientSessionsByUserSessions", PersistentClientSessionEntity.class);
+ query2.setParameter("userSessionIds", userSessionIds);
+ query2.setParameter("offline", offline);
+ List<PersistentClientSessionEntity> clientSessions = query2.getResultList();
+
+ // Assume both userSessions and clientSessions ordered by userSessionId
+ int j=0;
+ for (UserSessionModel ss : result) {
+ PersistentUserSessionAdapter userSession = (PersistentUserSessionAdapter) ss;
+ List<ClientSessionModel> currentClientSessions = userSession.getClientSessions(); // This is empty now and we want to fill it
+
+ boolean next = true;
+ while (next && j<clientSessions.size()) {
+ PersistentClientSessionEntity clientSession = clientSessions.get(j);
+ if (clientSession.getUserSessionId().equals(userSession.getId())) {
+ PersistentClientSessionAdapter clientSessAdapter = toAdapter(userSession.getRealm(), userSession, clientSession);
+ currentClientSessions.add(clientSessAdapter);
+ j++;
+ } else {
+ next = false;
+ }
+ }
+ }
+
+
+
+ return result;
+ }
+
+ private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity) {
+ RealmModel realm = session.realms().getRealm(entity.getRealmId());
+ UserModel user = session.users().getUserById(entity.getUserId(), realm);
+
+ PersistentUserSessionModel model = new PersistentUserSessionModel();
+ model.setUserSessionId(entity.getUserSessionId());
+ model.setLastSessionRefresh(entity.getLastSessionRefresh());
+ model.setData(entity.getData());
+
+ List<ClientSessionModel> clientSessions = new LinkedList<>();
+ return new PersistentUserSessionAdapter(model, realm, user, clientSessions);
+ }
+
+ private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, PersistentClientSessionEntity entity) {
+ ClientModel client = realm.getClientById(entity.getClientId());
+
+ PersistentClientSessionModel model = new PersistentClientSessionModel();
+ model.setClientSessionId(entity.getClientSessionId());
+ model.setClientId(entity.getClientId());
+ model.setUserSessionId(userSession.getId());
+ model.setUserId(userSession.getUser().getId());
+ model.setData(entity.getData());
+ return new PersistentClientSessionAdapter(model, realm, client, userSession);
+ }
+
+ @Override
+ public int getUserSessionsCount(boolean offline) {
+ Query query = em.createNamedQuery("findUserSessionsCount");
+ query.setParameter("offline", offline);
+ Number n = (Number) query.getSingleResult();
+ return n.intValue();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
new file mode 100644
index 0000000..f739091
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/PersistentUserSessionEntity.java
@@ -0,0 +1,147 @@
+package org.keycloak.models.jpa.session;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+
+import org.keycloak.models.jpa.entities.UserEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@NamedQueries({
+ @NamedQuery(name="deleteUserSessionsByRealm", query="delete from PersistentUserSessionEntity sess where sess.realmId=:realmId"),
+ @NamedQuery(name="deleteUserSessionsByUser", query="delete from PersistentUserSessionEntity sess where sess.userId=:userId"),
+ @NamedQuery(name="deleteDetachedUserSessions", query="delete from PersistentUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from PersistentClientSessionEntity c)"),
+ @NamedQuery(name="findUserSessionsCount", query="select count(sess) from PersistentUserSessionEntity sess where offline=:offline"),
+ @NamedQuery(name="findUserSessions", query="select sess from PersistentUserSessionEntity sess where offline=:offline order by sess.userSessionId")
+
+})
+@Table(name="OFFLINE_USER_SESSION")
+@Entity
+@IdClass(PersistentUserSessionEntity.Key.class)
+public class PersistentUserSessionEntity {
+
+ @Id
+ @Column(name="USER_SESSION_ID", length = 36)
+ protected String userSessionId;
+
+ @Column(name = "REALM_ID", length = 36)
+ protected String realmId;
+
+ @Column(name="USER_ID", length = 36)
+ protected String userId;
+
+ @Column(name = "LAST_SESSION_REFRESH")
+ protected int lastSessionRefresh;
+
+ @Id
+ @Column(name = "OFFLINE")
+ protected boolean offline;
+
+ @Column(name="DATA")
+ protected String data;
+
+ public String getUserSessionId() {
+ return userSessionId;
+ }
+
+ public void setUserSessionId(String userSessionId) {
+ this.userSessionId = userSessionId;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.realmId = realmId;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public int getLastSessionRefresh() {
+ return lastSessionRefresh;
+ }
+
+ public void setLastSessionRefresh(int lastSessionRefresh) {
+ this.lastSessionRefresh = lastSessionRefresh;
+ }
+
+ public boolean isOffline() {
+ return offline;
+ }
+
+ public void setOffline(boolean offline) {
+ this.offline = offline;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ public static class Key implements Serializable {
+
+ protected String userSessionId;
+
+ protected boolean offline;
+
+ public Key() {
+ }
+
+ public Key(String userSessionId, boolean offline) {
+ this.userSessionId = userSessionId;
+ this.offline = offline;
+ }
+
+ public String getUserSessionId() {
+ return userSessionId;
+ }
+
+ public boolean isOffline() {
+ return offline;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Key key = (Key) o;
+
+ if (this.userSessionId != null ? !this.userSessionId.equals(key.userSessionId) : key.userSessionId != null) return false;
+ if (offline != key.offline) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.userSessionId != null ? this.userSessionId.hashCode() : 0;
+ result = 31 * result + (offline ? 1 : 0);
+ return result;
+ }
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index 9c75057..9757d5b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -2,8 +2,6 @@ package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel;
import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.ModelDuplicateException;
@@ -16,8 +14,6 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.CredentialEntity;
-import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
-import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
import org.keycloak.models.jpa.entities.UserConsentEntity;
import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
@@ -37,11 +33,8 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Collection;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
new file mode 100644
index 0000000..b478dda
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
@@ -0,0 +1 @@
+org.keycloak.models.jpa.session.JpaUserSessionPersisterProviderFactory
\ No newline at end of file
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
index 4e1b4fa..e99f142 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/ClientAdapter.java
@@ -71,6 +71,15 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
}
@Override
+ public String getDescription() { return getMongoEntity().getDescription(); }
+
+ @Override
+ public void setDescription(String description) {
+ getMongoEntity().setDescription(description);
+ updateMongoEntity();
+ }
+
+ @Override
public void setClientId(String clientId) {
getMongoEntity().setClientId(clientId);
updateMongoEntity();
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 214733a..358e6f2 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -10,10 +10,6 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.ModelException;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -23,17 +19,13 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.entities.FederatedIdentityEntity;
-import org.keycloak.models.entities.OfflineClientSessionEntity;
-import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.utils.CredentialValidation;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -407,36 +399,6 @@ public class MongoUserProvider implements UserProvider {
.and("clientId").is(client.getId())
.get();
getMongoStore().removeEntities(MongoUserConsentEntity.class, query, false, invocationContext);
-
- // Remove all offlineClientSessions
- query = new QueryBuilder()
- .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
- .get();
- List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
- for (MongoUserEntity user : users) {
- boolean anyRemoved = false;
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clientSession : userSession.getOfflineClientSessions()) {
- if (clientSession.getClientId().equals(client.getId())) {
- userSession.getOfflineClientSessions().remove(clientSession);
- anyRemoved = true;
- break;
- }
- }
-
- // Check if it was last clientSession. Then remove userSession too
- if (userSession.getOfflineClientSessions().size() == 0) {
- user.getOfflineUserSessions().remove(userSession);
- anyRemoved = true;
- break;
- }
- }
-
- if (anyRemoved) {
- getMongoStore().updateEntity(user, invocationContext);
- }
-
- }
}
@Override
@@ -482,205 +444,4 @@ public class MongoUserProvider implements UserProvider {
// Not supported yet
return null;
}
-
- @Override
- public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- if (user.getOfflineUserSessions() == null) {
- user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
- }
-
- if (getUserSessionEntityById(user, userSession.getUserSessionId()) != null) {
- throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
- }
-
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUserSessionId(userSession.getUserSessionId());
- entity.setData(userSession.getData());
- entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
- user.getOfflineUserSessions().add(entity);
-
- getMongoStore().updateEntity(user, invocationContext);
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- private OfflineUserSessionEntity getUserSessionEntityById(MongoUserEntity user, String userSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return entity;
- }
- }
- }
- return null;
- }
-
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
- return entity==null ? null : toModel(entity);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- if (user.getOfflineUserSessions()==null) {
- return Collections.emptyList();
- } else {
- List<OfflineUserSessionModel> result = new ArrayList<>();
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
- }
-
- @Override
- public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
- if (entity != null) {
- user.getOfflineUserSessions().remove(entity);
- getMongoStore().updateEntity(user, invocationContext);
- return true;
- } else {
- return false;
- }
- }
-
- @Override
- public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
- MongoUserEntity user = getUserById(clientSession.getUserId(), realm).getUser();
-
- OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
- if (userSessionEntity == null) {
- throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
- }
-
- OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
- clEntity.setClientSessionId(clientSession.getClientSessionId());
- clEntity.setClientId(clientSession.getClientId());
- clEntity.setData(clientSession.getData());
-
- userSessionEntity.getOfflineClientSessions().add(clEntity);
- getMongoStore().updateEntity(user, invocationContext);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- return toModel(clSession, user.getId(), userSession.getUserSessionId());
- }
- }
- }
- }
-
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userId, String userSessionId) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(cls.getClientSessionId());
- model.setClientId(cls.getClientId());
- model.setUserId(userId);
- model.setData(cls.getData());
- model.setUserSessionId(userSessionId);
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
-
- List<OfflineClientSessionModel> result = new ArrayList<>();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- result.add(toModel(clSession, user.getId(), userSession.getUserSessionId()));
- }
- }
- }
-
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
- MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
- boolean updated = false;
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- userSession.getOfflineClientSessions().remove(clSession);
- updated = true;
- break;
- }
- }
-
- if (updated && userSession.getOfflineClientSessions().isEmpty()) {
- user.getOfflineUserSessions().remove(userSession);
- }
-
- if (updated) {
- getMongoStore().updateEntity(user, invocationContext);
- return true;
- }
- }
- }
-
- return false;
- }
-
- @Override
- public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
- DBObject query = new QueryBuilder()
- .and("realmId").is(realm.getId())
- .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
- .get();
- return getMongoStore().countEntities(MongoUserEntity.class, query, invocationContext);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- DBObject query = new QueryBuilder()
- .and("realmId").is(realm.getId())
- .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
- .get();
- DBObject sort = new BasicDBObject("username", 1);
- List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext);
-
- List<OfflineClientSessionModel> result = new LinkedList<>();
- for (MongoUserEntity user : users) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientId().equals(client.getId())) {
- OfflineClientSessionModel model = toModel(clSession, user.getId(), userSession.getUserSessionId());
- result.add(model);
- }
- }
- }
- }
-
- return result;
- }
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java
new file mode 100644
index 0000000..53917c2
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserSessionPersisterProvider.java
@@ -0,0 +1,276 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.connections.mongo.api.MongoStore;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.entities.PersistentClientSessionEntity;
+import org.keycloak.models.entities.PersistentUserSessionEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoOfflineUserSessionEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoOnlineUserSessionEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoUserSessionEntity;
+import org.keycloak.models.session.PersistentClientSessionAdapter;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionAdapter;
+import org.keycloak.models.session.PersistentUserSessionModel;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MongoUserSessionPersisterProvider implements UserSessionPersisterProvider {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final KeycloakSession session;
+
+ public MongoUserSessionPersisterProvider(KeycloakSession session, MongoStoreInvocationContext invocationContext) {
+ this.session = session;
+ this.invocationContext = invocationContext;
+ }
+
+ protected MongoStore getMongoStore() {
+ return invocationContext.getMongoStore();
+ }
+
+ private Class<? extends PersistentUserSessionEntity> getClazz(boolean offline) {
+ return offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
+ }
+
+ private MongoUserSessionEntity loadUserSession(String userSessionId, boolean offline) {
+ Class<? extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
+ return getMongoStore().loadEntity(clazz, userSessionId, invocationContext);
+ }
+
+ @Override
+ public void createUserSession(UserSessionModel userSession, boolean offline) {
+ PersistentUserSessionAdapter adapter = new PersistentUserSessionAdapter(userSession);
+ PersistentUserSessionModel model = adapter.getUpdatedModel();
+
+ MongoUserSessionEntity entity = offline ? new MongoOfflineUserSessionEntity() : new MongoOnlineUserSessionEntity();
+ entity.setId(model.getUserSessionId());
+ entity.setRealmId(adapter.getRealm().getId());
+ entity.setUserId(adapter.getUser().getId());
+ entity.setLastSessionRefresh(model.getLastSessionRefresh());
+ entity.setData(model.getData());
+ entity.setClientSessions(new ArrayList<PersistentClientSessionEntity>());
+ getMongoStore().insertEntity(entity, invocationContext);
+ }
+
+ @Override
+ public void createClientSession(ClientSessionModel clientSession, boolean offline) {
+ PersistentClientSessionAdapter adapter = new PersistentClientSessionAdapter(clientSession);
+ PersistentClientSessionModel model = adapter.getUpdatedModel();
+
+ MongoUserSessionEntity userSession = loadUserSession(model.getUserSessionId(), offline);
+ if (userSession == null) {
+ throw new ModelException("Not userSession found with ID " + clientSession.getUserSession().getId() + ". Requested by clientSession: " + clientSession.getId());
+ } else {
+ PersistentClientSessionEntity entity = new PersistentClientSessionEntity();
+ entity.setClientSessionId(clientSession.getId());
+ entity.setClientId(clientSession.getClient().getId());
+ entity.setData(model.getData());
+ userSession.getClientSessions().add(entity);
+ getMongoStore().updateEntity(userSession, invocationContext);
+ }
+ }
+
+ @Override
+ public void updateUserSession(UserSessionModel userSession, boolean offline) {
+ PersistentUserSessionAdapter adapter;
+ if (userSession instanceof PersistentUserSessionAdapter) {
+ adapter = (PersistentUserSessionAdapter) userSession;
+ } else {
+ adapter = new PersistentUserSessionAdapter(userSession);
+ }
+
+ PersistentUserSessionModel model = adapter.getUpdatedModel();
+
+ MongoUserSessionEntity entity = loadUserSession(model.getUserSessionId(), offline);
+ if (entity == null) {
+ throw new ModelException("UserSession with ID " + userSession.getId() + ", offline: " + offline + " not found");
+ }
+ entity.setLastSessionRefresh(model.getLastSessionRefresh());
+ entity.setData(model.getData());
+
+ getMongoStore().updateEntity(entity, invocationContext);
+ }
+
+ @Override
+ public void removeUserSession(String userSessionId, boolean offline) {
+ MongoUserSessionEntity entity = loadUserSession(userSessionId, offline);
+ if (entity != null) {
+ getMongoStore().removeEntity(entity, invocationContext);
+ }
+ }
+
+ @Override
+ public void removeClientSession(String clientSessionId, boolean offline) {
+ DBObject query = new QueryBuilder()
+ .and("clientSessions.clientSessionId").is(clientSessionId)
+ .get();
+ Class<? extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
+ MongoUserSessionEntity userSession = getMongoStore().loadSingleEntity(clazz, query, invocationContext);
+ if (userSession != null) {
+
+ PersistentClientSessionEntity found = null;
+ for (PersistentClientSessionEntity clientSession : userSession.getClientSessions()) {
+ if (clientSession.getClientSessionId().equals(clientSessionId)) {
+ found = clientSession;
+ break;
+ }
+ }
+
+ if (found != null) {
+ userSession.getClientSessions().remove(found);
+
+ // Remove userSession if it was last clientSession attached
+ if (userSession.getClientSessions().size() == 0) {
+ getMongoStore().removeEntity(userSession, invocationContext);
+ } else {
+ getMongoStore().updateEntity(userSession, invocationContext);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(realm.getId())
+ .get();
+ getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext);
+ getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext);
+ }
+
+ @Override
+ public void onClientRemoved(RealmModel realm, ClientModel client) {
+ DBObject query = new QueryBuilder()
+ .and("clientSessions.clientId").is(client.getId())
+ .get();
+
+ List<MongoOnlineUserSessionEntity> userSessions = getMongoStore().loadEntities(MongoOnlineUserSessionEntity.class, query, invocationContext);
+ for (MongoOnlineUserSessionEntity userSession : userSessions) {
+ removeClientSessionOfClient(userSession, client.getId());
+ }
+
+ List<MongoOfflineUserSessionEntity> userSessions2 = getMongoStore().loadEntities(MongoOfflineUserSessionEntity.class, query, invocationContext);
+ for (MongoOfflineUserSessionEntity userSession : userSessions2) {
+ removeClientSessionOfClient(userSession, client.getId());
+ }
+ }
+
+ private void removeClientSessionOfClient(MongoUserSessionEntity userSession, String clientId) {
+ PersistentClientSessionEntity found = null;
+ for (PersistentClientSessionEntity clientSession : userSession.getClientSessions()) {
+ if (clientSession.getClientId().equals(clientId)) {
+ found = clientSession;
+ break;
+ }
+ }
+
+ if (found != null) {
+ userSession.getClientSessions().remove(found);
+
+ // Remove userSession if it was last clientSession attached
+ if (userSession.getClientSessions().size() == 0) {
+ getMongoStore().removeEntity(userSession, invocationContext);
+ } else {
+ getMongoStore().updateEntity(userSession, invocationContext);
+ }
+ }
+ }
+
+ @Override
+ public void onUserRemoved(RealmModel realm, UserModel user) {
+ DBObject query = new QueryBuilder()
+ .and("userId").is(user.getId())
+ .get();
+ getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext);
+ getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext);
+ }
+
+ @Override
+ public void clearDetachedUserSessions() {
+ DBObject query = new QueryBuilder()
+ .and("clientSessions").is(Collections.emptyList())
+ .get();
+ getMongoStore().removeEntities(MongoOnlineUserSessionEntity.class, query, false, invocationContext);
+ getMongoStore().removeEntities(MongoOfflineUserSessionEntity.class, query, false, invocationContext);
+ }
+
+ @Override
+ public int getUserSessionsCount(boolean offline) {
+ DBObject query = new QueryBuilder()
+ .get();
+
+ Class<? extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
+ return getMongoStore().countEntities(clazz, query, invocationContext);
+ }
+
+ @Override
+ public List<UserSessionModel> loadUserSessions(int firstResult, int maxResults, boolean offline) {
+ DBObject query = new QueryBuilder()
+ .get();
+ DBObject sort = new BasicDBObject("id", 1);
+
+ Class<? extends MongoUserSessionEntity> clazz = offline ? MongoOfflineUserSessionEntity.class : MongoOnlineUserSessionEntity.class;
+
+ List<? extends MongoUserSessionEntity> entities = getMongoStore().loadEntities(clazz, query, sort, firstResult, maxResults, invocationContext);
+
+ List<UserSessionModel> results = new LinkedList<>();
+ for (MongoUserSessionEntity entity : entities) {
+ PersistentUserSessionAdapter userSession = toAdapter(entity, offline);
+ results.add(userSession);
+ }
+ return results;
+ }
+
+ private PersistentUserSessionAdapter toAdapter(PersistentUserSessionEntity entity, boolean offline) {
+ RealmModel realm = session.realms().getRealm(entity.getRealmId());
+ UserModel user = session.users().getUserById(entity.getUserId(), realm);
+
+ PersistentUserSessionModel model = new PersistentUserSessionModel();
+ model.setUserSessionId(entity.getId());
+ model.setLastSessionRefresh(entity.getLastSessionRefresh());
+ model.setData(entity.getData());
+
+ List<ClientSessionModel> clientSessions = new LinkedList<>();
+ PersistentUserSessionAdapter userSessionAdapter = new PersistentUserSessionAdapter(model, realm, user, clientSessions);
+ for (PersistentClientSessionEntity clientSessEntity : entity.getClientSessions()) {
+ PersistentClientSessionAdapter clientSessAdapter = toAdapter(realm, userSessionAdapter, offline, clientSessEntity);
+ clientSessions.add(clientSessAdapter);
+ }
+
+ return userSessionAdapter;
+ }
+
+ private PersistentClientSessionAdapter toAdapter(RealmModel realm, PersistentUserSessionAdapter userSession, boolean offline, PersistentClientSessionEntity entity) {
+ ClientModel client = realm.getClientById(entity.getClientId());
+
+ PersistentClientSessionModel model = new PersistentClientSessionModel();
+ model.setClientSessionId(entity.getClientSessionId());
+ model.setClientId(entity.getClientId());
+ model.setUserSessionId(userSession.getId());
+ model.setUserId(userSession.getUser().getId());
+ model.setData(entity.getData());
+ return new PersistentClientSessionAdapter(model, realm, client, userSession);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
index a6166cd..05ae8bc 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java
@@ -311,6 +311,16 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateRealm();
}
+ @Override
+ public boolean isRevokeRefreshToken() {
+ return realm.isRevokeRefreshToken();
+ }
+
+ @Override
+ public void setRevokeRefreshToken(boolean revokeRefreshToken) {
+ realm.setRevokeRefreshToken(revokeRefreshToken);
+ updateRealm();
+ }
@Override
public int getSsoSessionIdleTimeout() {
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index 9f13e63..16c4431 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -8,8 +8,6 @@ import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel;
import org.keycloak.models.OTPPolicy;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.KeycloakSession;
@@ -22,19 +20,15 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity;
-import org.keycloak.models.entities.OfflineClientSessionEntity;
-import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.entities.UserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.mongo.utils.MongoModelUtils;
-import org.keycloak.models.utils.HmacOTP;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.Time;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java
new file mode 100644
index 0000000..dd59869
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOfflineUserSessionEntity.java
@@ -0,0 +1,10 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.connections.mongo.api.MongoCollection;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@MongoCollection(collectionName = "offlineUserSessions")
+public class MongoOfflineUserSessionEntity extends MongoUserSessionEntity {
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java
new file mode 100644
index 0000000..7df63a9
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoOnlineUserSessionEntity.java
@@ -0,0 +1,10 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.connections.mongo.api.MongoCollection;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+@MongoCollection(collectionName = "userSessions")
+public class MongoOnlineUserSessionEntity extends MongoUserSessionEntity {
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
new file mode 100644
index 0000000..1da6d31
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoUserSessionEntity.java
@@ -0,0 +1,15 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.PersistentUserSessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public abstract class MongoUserSessionEntity extends PersistentUserSessionEntity implements MongoIdentifiableEntity {
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+ }
+}
diff --git a/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
new file mode 100644
index 0000000..b8acb44
--- /dev/null
+++ b/model/mongo/src/main/resources/META-INF/services/org.keycloak.models.session.UserSessionPersisterProviderFactory
@@ -0,0 +1 @@
+org.keycloak.models.mongo.keycloak.adapters.MongoUserSessionPersisterProviderFactory
\ No newline at end of file
model/sessions-infinispan/pom.xml 5(+5 -0)
diff --git a/model/sessions-infinispan/pom.xml b/model/sessions-infinispan/pom.xml
index ee64a6b..a9cdf35 100755
--- a/model/sessions-infinispan/pom.xml
+++ b/model/sessions-infinispan/pom.xml
@@ -30,5 +30,10 @@
<groupId>org.infinispan</groupId>
<artifactId>infinispan-core</artifactId>
</dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
index 179a1f0..a0b6531 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java
@@ -26,13 +26,16 @@ public class ClientSessionAdapter implements ClientSessionModel {
private Cache<String, SessionEntity> cache;
private RealmModel realm;
private ClientSessionEntity entity;
+ private boolean offline;
- public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientSessionEntity entity) {
+ public ClientSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm,
+ ClientSessionEntity entity, boolean offline) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
+ this.offline = offline;
}
@Override
@@ -51,8 +54,8 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
@Override
- public UserSessionModel getUserSession() {
- return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession()) : null;
+ public UserSessionAdapter getUserSession() {
+ return entity.getUserSession() != null ? provider.getUserSession(realm, entity.getUserSession(), offline) : null;
}
@Override
@@ -63,14 +66,15 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
entity.setUserSession(null);
} else {
+ UserSessionAdapter userSessionAdapter = (UserSessionAdapter) userSession;
if (entity.getUserSession() != null) {
if (entity.getUserSession().equals(userSession.getId())) {
return;
} else {
- provider.dettachSession(userSession, this);
+ provider.dettachSession(userSessionAdapter, this);
}
} else {
- provider.attachSession(userSession, this);
+ provider.attachSession(userSessionAdapter, this);
}
entity.setUserSession(userSession.getId());
@@ -113,7 +117,8 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override
public Set<String> getRoles() {
- return entity.getRoles();
+ if (entity.getRoles() == null || entity.getRoles().isEmpty()) return Collections.emptySet();
+ return new HashSet<>(entity.getRoles());
}
@Override
@@ -124,7 +129,8 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override
public Set<String> getProtocolMappers() {
- return entity.getProtocolMappers();
+ if (entity.getProtocolMappers() == null || entity.getProtocolMappers().isEmpty()) return Collections.emptySet();
+ return new HashSet<>(entity.getProtocolMappers());
}
@Override
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
index 947c605..0e5f2b9 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProvider.java
@@ -4,6 +4,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -38,13 +39,21 @@ public class MemUserSessionProvider implements UserSessionProvider {
private final ConcurrentHashMap<String, ClientSessionEntity> clientSessions;
private final ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures;
- public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId, ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions, ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures) {
+ private final ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions;
+ private final ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions;
+
+ public MemUserSessionProvider(KeycloakSession session, ConcurrentHashMap<String, UserSessionEntity> userSessions, ConcurrentHashMap<String, String> userSessionsByBrokerSessionId,
+ ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId, ConcurrentHashMap<String, ClientSessionEntity> clientSessions,
+ ConcurrentHashMap<UsernameLoginFailureKey, UsernameLoginFailureEntity> loginFailures,
+ ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions, ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions) {
this.session = session;
this.userSessions = userSessions;
this.clientSessions = clientSessions;
this.loginFailures = loginFailures;
this.userSessionsByBrokerSessionId = userSessionsByBrokerSessionId;
this.userSessionsByBrokerUserId = userSessionsByBrokerUserId;
+ this.offlineUserSessions = offlineUserSessions;
+ this.offlineClientSessions = offlineClientSessions;
}
@Override
@@ -189,6 +198,12 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client) {
+ return getUserSessions(realm, client, false);
+ }
+
+ protected List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, boolean offline) {
+ ConcurrentHashMap<String, ClientSessionEntity> clientSessions = offline ? this.offlineClientSessions : this.clientSessions;
+
List<UserSessionEntity> userSessionEntities = new LinkedList<UserSessionEntity>();
for (ClientSessionEntity s : clientSessions.values()) {
String realmId = realm.getId();
@@ -210,7 +225,11 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- List<UserSessionModel> userSessions = getUserSessions(realm, client);
+ return getUserSessions(realm, client, firstResult, maxResults, false);
+ }
+
+ protected List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) {
+ List<UserSessionModel> userSessions = getUserSessions(realm, client, offline);
if (firstResult > userSessions.size()) {
return Collections.emptyList();
}
@@ -221,7 +240,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public int getActiveUserSessions(RealmModel realm, ClientModel client) {
- return getUserSessions(realm, client).size();
+ return getUserSessions(realm, client, false).size();
}
@Override
@@ -229,40 +248,51 @@ public class MemUserSessionProvider implements UserSessionProvider {
UserSessionEntity entity = getUserSessionEntity(realm, session.getId());
if (entity != null) {
userSessions.remove(entity.getId());
- remove(entity);
+ remove(entity, false);
}
}
@Override
public void removeUserSessions(RealmModel realm, UserModel user) {
- Iterator<UserSessionEntity> itr = userSessions.values().iterator();
+ removeUserSessions(realm, user, false);
+ }
+
+ protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) {
+ Iterator<UserSessionEntity> itr = offline ? offlineUserSessions.values().iterator() : userSessions.values().iterator();
+
while (itr.hasNext()) {
UserSessionEntity s = itr.next();
if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) {
itr.remove();
- remove(s);
+ remove(s, offline);
}
}
}
- protected void remove(UserSessionEntity s) {
- if (s.getBrokerSessionId() != null) {
- userSessionsByBrokerSessionId.remove(s.getBrokerSessionId());
- }
- if (s.getBrokerUserId() != null) {
- Set<String> set = userSessionsByBrokerUserId.get(s.getBrokerUserId());
- if (set != null) {
- synchronized (set) {
- set.remove(s.getId());
- // this is a race condition :(
- // Since it will be very rare for a user to have concurrent sessions, I'm hoping we never hit this
- if (set.isEmpty()) userSessionsByBrokerUserId.remove(s.getBrokerUserId());
+ protected void remove(UserSessionEntity s, boolean offline) {
+ if (offline) {
+ for (ClientSessionEntity clientSession : s.getClientSessions()) {
+ offlineClientSessions.remove(clientSession.getId());
+ }
+ } else {
+ if (s.getBrokerSessionId() != null) {
+ userSessionsByBrokerSessionId.remove(s.getBrokerSessionId());
+ }
+ if (s.getBrokerUserId() != null) {
+ Set<String> set = userSessionsByBrokerUserId.get(s.getBrokerUserId());
+ if (set != null) {
+ synchronized (set) {
+ set.remove(s.getId());
+ // this is a race condition :(
+ // Since it will be very rare for a user to have concurrent sessions, I'm hoping we never hit this
+ if (set.isEmpty()) userSessionsByBrokerUserId.remove(s.getBrokerUserId());
+ }
}
}
+ for (ClientSessionEntity clientSession : s.getClientSessions()) {
+ clientSessions.remove(clientSession.getId());
+ }
}
- for (ClientSessionEntity clientSession : s.getClientSessions()) {
- clientSessions.remove(clientSession.getId());
- }
}
@Override
@@ -273,7 +303,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
if (s.getRealm().equals(realm.getId()) && (s.getLastSessionRefresh() < Time.currentTime() - realm.getSsoSessionIdleTimeout() || s.getStarted() < Time.currentTime() - realm.getSsoSessionMaxLifespan())) {
itr.remove();
- remove(s);
+ remove(s, false);
}
}
int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
@@ -288,16 +318,19 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public void removeUserSessions(RealmModel realm) {
- Iterator<UserSessionEntity> itr = userSessions.values().iterator();
+ removeUserSessions(realm, false);
+ }
+
+ protected void removeUserSessions(RealmModel realm, boolean offline) {
+ Iterator<UserSessionEntity> itr = offline ? offlineUserSessions.values().iterator() : userSessions.values().iterator();
while (itr.hasNext()) {
UserSessionEntity s = itr.next();
if (s.getRealm().equals(realm.getId())) {
itr.remove();
-
- remove(s);
+ remove(s, offline);
}
}
- Iterator<ClientSessionEntity> citr = clientSessions.values().iterator();
+ Iterator<ClientSessionEntity> citr = offline ? offlineClientSessions.values().iterator() : clientSessions.values().iterator();
while (citr.hasNext()) {
ClientSessionEntity c = citr.next();
if (c.getSession() == null && c.getRealmId().equals(realm.getId())) {
@@ -340,15 +373,23 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public void onRealmRemoved(RealmModel realm) {
- removeUserSessions(realm);
+ removeUserSessions(realm, true);
+ removeUserSessions(realm, false);
removeAllUserLoginFailures(realm);
}
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
- for (ClientSessionEntity e : clientSessions.values()) {
+ onClientRemoved(realm, client, true);
+ onClientRemoved(realm, client, false);
+ }
+
+ private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) {
+ ConcurrentHashMap<String, ClientSessionEntity> clientSessionsMap = offline ? offlineClientSessions : clientSessions;
+
+ for (ClientSessionEntity e : clientSessionsMap.values()) {
if (e.getRealmId().equals(realm.getId()) && e.getClientId().equals(client.getId())) {
- clientSessions.remove(e.getId());
+ clientSessionsMap.remove(e.getId());
e.getSession().removeClientSession(e);
}
}
@@ -356,12 +397,130 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public void onUserRemoved(RealmModel realm, UserModel user) {
- removeUserSessions(realm, user);
+ removeUserSessions(realm, user, true);
+ removeUserSessions(realm, user, false);
loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), user.getUsername()));
loginFailures.remove(new UsernameLoginFailureKey(realm.getId(), user.getEmail()));
}
+
+ @Override
+ public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
+ UserSessionEntity entity = new UserSessionEntity();
+ entity.setId(userSession.getId());
+ entity.setRealm(userSession.getRealm().getId());
+
+ entity.setAuthMethod(userSession.getAuthMethod());
+ entity.setBrokerSessionId(userSession.getBrokerSessionId());
+ entity.setBrokerUserId(userSession.getBrokerUserId());
+ entity.setIpAddress(userSession.getIpAddress());
+ entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
+ entity.setLoginUsername(userSession.getLoginUsername());
+ if (userSession.getNotes() != null) {
+ entity.getNotes().putAll(userSession.getNotes());
+ }
+ entity.setRememberMe(userSession.isRememberMe());
+ entity.setStarted(userSession.getStarted());
+ entity.setState(userSession.getState());
+ entity.setUser(userSession.getUser().getId());
+
+ offlineUserSessions.put(userSession.getId(), entity);
+ return new UserSessionAdapter(session, this, userSession.getRealm(), entity);
+ }
+
+ @Override
+ public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) {
+ UserSessionEntity entity = offlineUserSessions.get(userSessionId);
+ if (entity != null && entity.getRealm().equals(realm.getId())) {
+ return new UserSessionAdapter(session, this, realm, entity);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void removeOfflineUserSession(RealmModel realm, String userSessionId) {
+ UserSessionEntity entity = offlineUserSessions.get(userSessionId);
+ if (entity != null && entity.getRealm().equals(realm.getId())) {
+ offlineUserSessions.remove(entity);
+ remove(entity, true);
+ }
+ }
+
+ @Override
+ public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) {
+ ClientSessionEntity entity = new ClientSessionEntity();
+ entity.setId(clientSession.getId());
+ entity.setRealmId(clientSession.getRealm().getId());
+
+ entity.setAction(clientSession.getAction());
+ entity.setAuthenticatorStatus(clientSession.getExecutionStatus());
+ entity.setAuthMethod(clientSession.getAuthMethod());
+ if (clientSession.getAuthenticatedUser() != null) {
+ entity.setAuthUserId(clientSession.getAuthenticatedUser().getId());
+ }
+ entity.setClientId(clientSession.getClient().getId());
+ if (clientSession.getNotes() != null) {
+ entity.getNotes().putAll(clientSession.getNotes());
+ }
+ entity.setProtocolMappers(clientSession.getProtocolMappers());
+ entity.setRedirectUri(clientSession.getRedirectUri());
+ entity.setRoles(clientSession.getRoles());
+ entity.setTimestamp(clientSession.getTimestamp());
+
+ if (clientSession.getUserSessionNotes() != null) {
+ entity.getUserSessionNotes().putAll(clientSession.getUserSessionNotes());
+ }
+
+ offlineClientSessions.put(clientSession.getId(), entity);
+ return new ClientSessionAdapter(session, this, clientSession.getRealm(), entity);
+ }
+
+ @Override
+ public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) {
+ ClientSessionEntity entity = offlineClientSessions.get(clientSessionId);
+ if (entity != null && entity.getRealmId().equals(realm.getId())) {
+ return new ClientSessionAdapter(session, this, realm, entity);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public List<ClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ List<ClientSessionModel> clientSessions = new LinkedList<>();
+ for (UserSessionEntity s : this.offlineUserSessions.values()) {
+ if (s.getRealm().equals(realm.getId()) && s.getUser().equals(user.getId())) {
+ for (ClientSessionEntity cls : s.getClientSessions()) {
+ ClientSessionAdapter clAdapter = new ClientSessionAdapter(session, this, realm, cls);
+ clientSessions.add(clAdapter);
+ }
+ }
+ }
+ return clientSessions;
+ }
+
+ @Override
+ public void removeOfflineClientSession(RealmModel realm, String clientSessionId) {
+ ClientSessionEntity entity = offlineClientSessions.get(clientSessionId);
+ if (entity != null && entity.getRealmId().equals(realm.getId())) {
+ offlineClientSessions.remove(entity.getId());
+ UserSessionEntity userSession = entity.getSession();
+ userSession.removeClientSession(entity);
+ }
+ }
+
+ @Override
+ public int getOfflineSessionsCount(RealmModel realm, ClientModel client) {
+ return getUserSessions(realm, client, true).size();
+ }
+
+ @Override
+ public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) {
+ return getUserSessions(realm, client, first, max, true);
+ }
+
@Override
public void close() {
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
index 6a84b1a..187a33f 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/MemUserSessionProviderFactory.java
@@ -26,8 +26,12 @@ public class MemUserSessionProviderFactory {
private final ConcurrentHashMap<String, String> userSessionsByBrokerSessionId = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Set<String>> userSessionsByBrokerUserId = new ConcurrentHashMap<>();
+ private ConcurrentHashMap<String, UserSessionEntity> offlineUserSessions = new ConcurrentHashMap<String, UserSessionEntity>();
+ private ConcurrentHashMap<String, ClientSessionEntity> offlineClientSessions = new ConcurrentHashMap<String, ClientSessionEntity>();
+
public UserSessionProvider create(KeycloakSession session) {
- return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures);
+ return new MemUserSessionProvider(session, userSessions, userSessionsByBrokerSessionId, userSessionsByBrokerUserId, clientSessions, loginFailures,
+ offlineUserSessions, offlineClientSessions);
}
public void close() {
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java
new file mode 100644
index 0000000..450bbe1
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/SimpleUserSessionInitializer.java
@@ -0,0 +1,37 @@
+package org.keycloak.models.sessions.infinispan.compat;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.sessions.infinispan.initializer.SessionLoader;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SimpleUserSessionInitializer {
+
+ private final KeycloakSessionFactory sessionFactory;
+ private final SessionLoader sessionLoader;
+ private final int sessionsPerSegment;
+
+ public SimpleUserSessionInitializer(KeycloakSessionFactory sessionFactory, SessionLoader sessionLoader, int sessionsPerSegment) {
+ this.sessionFactory = sessionFactory;
+ this.sessionLoader = sessionLoader;
+ this.sessionsPerSegment = sessionsPerSegment;
+ }
+
+ public void loadPersistentSessions() {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ int count = sessionLoader.getSessionsCount(session);
+ for (int i=0 ; i<=count ; i+=sessionsPerSegment) {
+ sessionLoader.loadSessions(session, i, sessionsPerSegment);
+ }
+ }
+
+ });
+ }
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
index a9db618..c3bed67 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/compat/UserSessionAdapter.java
@@ -40,6 +40,11 @@ public class UserSessionAdapter implements UserSessionModel {
}
@Override
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
public String getBrokerSessionId() {
return entity.getBrokerSessionId();
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
index 5ddea31..b260b33 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java
@@ -22,8 +22,6 @@ public class ClientSessionEntity extends SessionEntity {
private String redirectUri;
- private String state;
-
private int timestamp;
private String action;
@@ -69,14 +67,6 @@ public class ClientSessionEntity extends SessionEntity {
this.redirectUri = redirectUri;
}
- public String getState() {
- return state;
- }
-
- public void setState(String state) {
- this.state = state;
- }
-
public int getTimestamp() {
return timestamp;
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 83776df..dbfecb4 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -7,6 +7,7 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
@@ -18,6 +19,7 @@ import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionMapper;
+import org.keycloak.models.sessions.infinispan.mapreduce.ClientSessionsOfUserSessionMapper;
import org.keycloak.models.sessions.infinispan.mapreduce.FirstResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.LargestResultReducer;
import org.keycloak.models.sessions.infinispan.mapreduce.SessionMapper;
@@ -36,6 +38,7 @@ import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -46,18 +49,25 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
private final KeycloakSession session;
private final Cache<String, SessionEntity> sessionCache;
+ private final Cache<String, SessionEntity> offlineSessionCache;
private final Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache;
private final InfinispanKeycloakTransaction tx;
- public InfinispanUserSessionProvider(KeycloakSession session, Cache<String, SessionEntity> sessionCache, Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
+ public InfinispanUserSessionProvider(KeycloakSession session, Cache<String, SessionEntity> sessionCache, Cache<String, SessionEntity> offlineSessionCache,
+ Cache<LoginFailureKey, LoginFailureEntity> loginFailureCache) {
this.session = session;
this.sessionCache = sessionCache;
+ this.offlineSessionCache = offlineSessionCache;
this.loginFailureCache = loginFailureCache;
this.tx = new InfinispanKeycloakTransaction();
session.getTransaction().enlistAfterCompletion(tx);
}
+ protected Cache<String, SessionEntity> getCache(boolean offline) {
+ return offline ? offlineSessionCache : sessionCache;
+ }
+
@Override
public ClientSessionModel createClientSession(RealmModel realm, ClientModel client) {
String id = KeycloakModelUtils.generateId();
@@ -70,7 +80,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
tx.put(sessionCache, id, entity);
- return wrap(realm, entity);
+ return wrap(realm, entity, false);
}
@Override
@@ -95,29 +105,57 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
tx.put(sessionCache, id, entity);
- return wrap(realm, entity);
+ return wrap(realm, entity, false);
}
@Override
public ClientSessionModel getClientSession(RealmModel realm, String id) {
- ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id);
- return wrap(realm, entity);
+ return getClientSession(realm, id, false);
+ }
+
+ protected ClientSessionModel getClientSession(RealmModel realm, String id, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+ ClientSessionEntity entity = (ClientSessionEntity) cache.get(id);
+
+ // Chance created in this transaction
+ if (entity == null) {
+ entity = (ClientSessionEntity) tx.get(cache, id);
+ }
+
+ return wrap(realm, entity, offline);
}
@Override
public ClientSessionModel getClientSession(String id) {
ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id);
+
+ // Chance created in this transaction
+ if (entity == null) {
+ entity = (ClientSessionEntity) tx.get(sessionCache, id);
+ }
+
if (entity != null) {
RealmModel realm = session.realms().getRealm(entity.getRealm());
- return wrap(realm, entity);
+ return wrap(realm, entity, false);
}
return null;
}
@Override
public UserSessionModel getUserSession(RealmModel realm, String id) {
- UserSessionEntity entity = (UserSessionEntity) sessionCache.get(id);
- return wrap(realm, entity);
+ return getUserSession(realm, id, false);
+ }
+
+ protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+ UserSessionEntity entity = (UserSessionEntity) cache.get(id);
+
+ // Chance created in this transaction
+ if (entity == null) {
+ entity = (UserSessionEntity) tx.get(cache, id);
+ }
+
+ return wrap(realm, entity, offline);
}
@Override
@@ -127,7 +165,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
.reducedWith(new FirstResultReducer())
.execute();
- return wrapUserSessions(realm, sessions.values());
+ return wrapUserSessions(realm, sessions.values(), false);
}
@Override
@@ -137,7 +175,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
.reducedWith(new FirstResultReducer())
.execute();
- return wrapUserSessions(realm, sessions.values());
+ return wrapUserSessions(realm, sessions.values(), false);
}
@Override
@@ -147,7 +185,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
.reducedWith(new FirstResultReducer())
.execute();
- List<UserSessionModel> userSessionModels = wrapUserSessions(realm, sessions.values());
+ List<UserSessionModel> userSessionModels = wrapUserSessions(realm, sessions.values(), false);
if (userSessionModels.isEmpty()) return null;
return userSessionModels.get(0);
}
@@ -159,7 +197,13 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
- Map<String, Integer> map = new MapReduceTask(sessionCache)
+ return getUserSessions(realm, client, firstResult, maxResults, false);
+ }
+
+ protected List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ Map<String, Integer> map = new MapReduceTask(cache)
.mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp())
.reducedWith(new LargestResultReducer())
.execute();
@@ -192,9 +236,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
List<UserSessionModel> userSessions = new LinkedList<UserSessionModel>();
for (Map.Entry<String, Integer> e : sessionTimestamps) {
- UserSessionEntity userSessionEntity = (UserSessionEntity) sessionCache.get(e.getKey());
+ UserSessionEntity userSessionEntity = (UserSessionEntity) cache.get(e.getKey());
if (userSessionEntity != null) {
- userSessions.add(wrap(realm, userSessionEntity));
+ userSessions.add(wrap(realm, userSessionEntity, offline));
}
}
@@ -214,13 +258,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
.reducedWith(new FirstResultReducer())
.execute();
- return wrapUserSessions(realm, sessions.values());
+ return wrapUserSessions(realm, sessions.values(), false);
}
@Override
public int getActiveUserSessions(RealmModel realm, ClientModel client) {
- Map map = new MapReduceTask(sessionCache)
+ return getUserSessionsCount(realm, client, false);
+ }
+
+ protected int getUserSessionsCount(RealmModel realm, ClientModel client, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ Map map = new MapReduceTask(cache)
.mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitUserSessionAndTimestamp())
.reducedWith(new LargestResultReducer()).execute();
@@ -234,13 +284,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void removeUserSessions(RealmModel realm, UserModel user) {
- Map<String, String> sessions = new MapReduceTask(sessionCache)
+ removeUserSessions(realm, user, false);
+ }
+
+ protected void removeUserSessions(RealmModel realm, UserModel user, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ Map<String, String> sessions = new MapReduceTask(cache)
.mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
for (String id : sessions.keySet()) {
- removeUserSession(realm, id);
+ removeUserSession(realm, id, offline);
}
}
@@ -271,13 +327,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void removeUserSessions(RealmModel realm) {
- Map<String, String> ids = new MapReduceTask(sessionCache)
+ removeUserSessions(realm, false);
+ }
+
+ protected void removeUserSessions(RealmModel realm, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ Map<String, String> ids = new MapReduceTask(cache)
.mappedWith(SessionMapper.create(realm.getId()).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
for (String id : ids.keySet()) {
- sessionCache.remove(id);
+ cache.remove(id);
}
}
@@ -319,25 +381,39 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public void onRealmRemoved(RealmModel realm) {
- removeUserSessions(realm);
+ removeUserSessions(realm, true);
+ removeUserSessions(realm, false);
removeAllUserLoginFailures(realm);
}
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
- Map<String, String> map = new MapReduceTask(sessionCache)
- .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()).emitKey())
+ onClientRemoved(realm, client, true);
+ onClientRemoved(realm, client, false);
+ }
+
+ private void onClientRemoved(RealmModel realm, ClientModel client, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ Map<String, ClientSessionEntity> map = new MapReduceTask(cache)
+ .mappedWith(ClientSessionMapper.create(realm.getId()).client(client.getId()))
.reducedWith(new FirstResultReducer())
.execute();
- for (String id : map.keySet()) {
- tx.remove(sessionCache, id);
+ for (Map.Entry<String, ClientSessionEntity> entry : map.entrySet()) {
+
+ // detach from userSession
+ ClientSessionAdapter adapter = wrap(realm, entry.getValue(), offline);
+ adapter.setUserSession(null);
+
+ tx.remove(cache, entry.getKey());
}
}
@Override
public void onUserRemoved(RealmModel realm, UserModel user) {
- removeUserSessions(realm, user);
+ removeUserSessions(realm, user, true);
+ removeUserSessions(realm, user, false);
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getUsername()));
loginFailureCache.remove(new LoginFailureKey(realm.getId(), user.getEmail()));
@@ -347,20 +423,26 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void close() {
}
- void attachSession(UserSessionModel userSession, ClientSessionModel clientSession) {
- UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity();
+ void attachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) {
+ UserSessionEntity entity = userSession.getEntity();
String clientSessionId = clientSession.getId();
if (entity.getClientSessions() == null) {
entity.setClientSessions(new HashSet<String>());
}
if (!entity.getClientSessions().contains(clientSessionId)) {
entity.getClientSessions().add(clientSessionId);
- tx.replace(sessionCache, entity.getId(), entity);
+ userSession.update();
}
}
@Override
public void removeClientSession(RealmModel realm, ClientSessionModel clientSession) {
+ removeClientSession(realm, clientSession, false);
+ }
+
+ protected void removeClientSession(RealmModel realm, ClientSessionModel clientSession, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
UserSessionModel userSession = clientSession.getUserSession();
if (userSession != null) {
UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity();
@@ -368,34 +450,40 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.getClientSessions().remove(clientSession.getId());
}
- tx.replace(sessionCache, entity.getId(), entity);
+ tx.replace(cache, entity.getId(), entity);
}
- tx.remove(sessionCache, clientSession.getId());
+ tx.remove(cache, clientSession.getId());
}
- void dettachSession(UserSessionModel userSession, ClientSessionModel clientSession) {
- UserSessionEntity entity = ((UserSessionAdapter) userSession).getEntity();
+ void dettachSession(UserSessionAdapter userSession, ClientSessionModel clientSession) {
+ UserSessionEntity entity = userSession.getEntity();
String clientSessionId = clientSession.getId();
if (entity.getClientSessions() != null && entity.getClientSessions().contains(clientSessionId)) {
entity.getClientSessions().remove(clientSessionId);
if (entity.getClientSessions().isEmpty()) {
entity.setClientSessions(null);
}
- tx.replace(sessionCache, entity.getId(), entity);
+ userSession.update();
}
}
protected void removeUserSession(RealmModel realm, String userSessionId) {
- tx.remove(sessionCache, userSessionId);
+ removeUserSession(realm, userSessionId, false);
+ }
- Map<String, String> map = new MapReduceTask(sessionCache)
+ protected void removeUserSession(RealmModel realm, String userSessionId, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+
+ tx.remove(cache, userSessionId);
+
+ Map<String, String> map = new MapReduceTask(cache)
.mappedWith(ClientSessionMapper.create(realm.getId()).userSession(userSessionId).emitKey())
.reducedWith(new FirstResultReducer())
.execute();
for (String id : map.keySet()) {
- tx.remove(sessionCache, id);
+ tx.remove(cache, id);
}
}
@@ -404,20 +492,22 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return tx;
}
- UserSessionModel wrap(RealmModel realm, UserSessionEntity entity) {
- return entity != null ? new UserSessionAdapter(session, this, sessionCache, realm, entity) : null;
+ UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+ return entity != null ? new UserSessionAdapter(session, this, cache, realm, entity, offline) : null;
}
- List<UserSessionModel> wrapUserSessions(RealmModel realm, Collection<UserSessionEntity> entities) {
+ List<UserSessionModel> wrapUserSessions(RealmModel realm, Collection<UserSessionEntity> entities, boolean offline) {
List<UserSessionModel> models = new LinkedList<UserSessionModel>();
for (UserSessionEntity e : entities) {
- models.add(wrap(realm, e));
+ models.add(wrap(realm, e, offline));
}
return models;
}
- ClientSessionModel wrap(RealmModel realm, ClientSessionEntity entity) {
- return entity != null ? new ClientSessionAdapter(session, this, sessionCache, realm, entity) : null;
+ ClientSessionAdapter wrap(RealmModel realm, ClientSessionEntity entity, boolean offline) {
+ Cache<String, SessionEntity> cache = getCache(offline);
+ return entity != null ? new ClientSessionAdapter(session, this, cache, realm, entity, offline) : null;
}
@@ -425,14 +515,113 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return entity != null ? new UsernameLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
}
- List<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities) {
+ List<ClientSessionModel> wrapClientSessions(RealmModel realm, Collection<ClientSessionEntity> entities, boolean offline) {
List<ClientSessionModel> models = new LinkedList<ClientSessionModel>();
for (ClientSessionEntity e : entities) {
- models.add(wrap(realm, e));
+ models.add(wrap(realm, e, offline));
}
return models;
}
+
+ @Override
+ public UserSessionModel createOfflineUserSession(UserSessionModel userSession) {
+ UserSessionEntity entity = new UserSessionEntity();
+ entity.setId(userSession.getId());
+ entity.setRealm(userSession.getRealm().getId());
+
+ entity.setAuthMethod(userSession.getAuthMethod());
+ entity.setBrokerSessionId(userSession.getBrokerSessionId());
+ entity.setBrokerUserId(userSession.getBrokerUserId());
+ entity.setIpAddress(userSession.getIpAddress());
+ entity.setLastSessionRefresh(userSession.getLastSessionRefresh());
+ entity.setLoginUsername(userSession.getLoginUsername());
+ entity.setNotes(userSession.getNotes());
+ entity.setRememberMe(userSession.isRememberMe());
+ entity.setStarted(userSession.getStarted());
+ entity.setState(userSession.getState());
+ entity.setUser(userSession.getUser().getId());
+
+ tx.put(offlineSessionCache, userSession.getId(), entity);
+ return wrap(userSession.getRealm(), entity, true);
+ }
+
+ @Override
+ public UserSessionModel getOfflineUserSession(RealmModel realm, String userSessionId) {
+ return getUserSession(realm, userSessionId, true);
+ }
+
+ @Override
+ public void removeOfflineUserSession(RealmModel realm, String userSessionId) {
+ removeUserSession(realm, userSessionId, true);
+ }
+
+ @Override
+ public ClientSessionModel createOfflineClientSession(ClientSessionModel clientSession) {
+ ClientSessionEntity entity = new ClientSessionEntity();
+ entity.setId(clientSession.getId());
+ entity.setRealm(clientSession.getRealm().getId());
+
+ entity.setAction(clientSession.getAction());
+ entity.setAuthenticatorStatus(clientSession.getExecutionStatus());
+ entity.setAuthMethod(clientSession.getAuthMethod());
+ if (clientSession.getAuthenticatedUser() != null) {
+ entity.setAuthUserId(clientSession.getAuthenticatedUser().getId());
+ }
+ entity.setClient(clientSession.getClient().getId());
+ entity.setNotes(clientSession.getNotes());
+ entity.setProtocolMappers(clientSession.getProtocolMappers());
+ entity.setRedirectUri(clientSession.getRedirectUri());
+ entity.setRoles(clientSession.getRoles());
+ entity.setTimestamp(clientSession.getTimestamp());
+ entity.setUserSessionNotes(clientSession.getUserSessionNotes());
+
+ tx.put(offlineSessionCache, clientSession.getId(), entity);
+ return wrap(clientSession.getRealm(), entity, true);
+ }
+
+ @Override
+ public ClientSessionModel getOfflineClientSession(RealmModel realm, String clientSessionId) {
+ return getClientSession(realm, clientSessionId, true);
+ }
+
+ @Override
+ public List<ClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ Map<String, UserSessionEntity> sessions = new MapReduceTask(offlineSessionCache)
+ .mappedWith(UserSessionMapper.create(realm.getId()).user(user.getId()))
+ .reducedWith(new FirstResultReducer())
+ .execute();
+
+ List<ClientSessionEntity> clientSessions = new LinkedList<>();
+ for (UserSessionEntity userSession : sessions.values()) {
+ Set<String> currClientSessions = userSession.getClientSessions();
+ for (String clientSessionId : currClientSessions) {
+ ClientSessionEntity cls = (ClientSessionEntity) offlineSessionCache.get(clientSessionId);
+ if (cls != null) {
+ clientSessions.add(cls);
+ }
+ }
+ }
+
+ return wrapClientSessions(realm, clientSessions, true);
+ }
+
+ @Override
+ public void removeOfflineClientSession(RealmModel realm, String clientSessionId) {
+ ClientSessionModel clientSession = getOfflineClientSession(realm, clientSessionId);
+ removeClientSession(realm, clientSession, true);
+ }
+
+ @Override
+ public int getOfflineSessionsCount(RealmModel realm, ClientModel client) {
+ return getUserSessionsCount(realm, client, true);
+ }
+
+ @Override
+ public List<UserSessionModel> getOfflineUserSessions(RealmModel realm, ClientModel client, int first, int max) {
+ return getUserSessions(realm, client, first, max, true);
+ }
+
class InfinispanKeycloakTransaction implements KeycloakTransaction {
private boolean active;
@@ -478,17 +667,19 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
public void put(Cache cache, Object key, Object value) {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD, key);
- if (tasks.containsKey(key)) {
+ Object taskKey = getTaskKey(cache, key);
+ if (tasks.containsKey(taskKey)) {
throw new IllegalStateException("Can't add session: task in progress for session");
} else {
- tasks.put(key, new CacheTask(cache, CacheOperation.ADD, key, value));
+ tasks.put(taskKey, new CacheTask(cache, CacheOperation.ADD, key, value));
}
}
public void replace(Cache cache, Object key, Object value) {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REPLACE, key);
- CacheTask current = tasks.get(key);
+ Object taskKey = getTaskKey(cache, key);
+ CacheTask current = tasks.get(taskKey);
if (current != null) {
switch (current.operation) {
case ADD:
@@ -499,14 +690,40 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
return;
}
} else {
- tasks.put(key, new CacheTask(cache, CacheOperation.REPLACE, key, value));
+ tasks.put(taskKey, new CacheTask(cache, CacheOperation.REPLACE, key, value));
}
}
public void remove(Cache cache, Object key) {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
- tasks.put(key, new CacheTask(cache, CacheOperation.REMOVE, key, null));
+ Object taskKey = getTaskKey(cache, key);
+ tasks.put(taskKey, new CacheTask(cache, CacheOperation.REMOVE, key, null));
+ }
+
+ // This is for possibility to lookup for session by id, which was created in this transaction
+ public Object get(Cache cache, Object key) {
+ Object taskKey = getTaskKey(cache, key);
+ CacheTask current = tasks.get(taskKey);
+ if (current != null) {
+ switch (current.operation) {
+ case ADD:
+ case REPLACE:
+ return current.value; }
+ }
+
+ return null;
+ }
+
+ private Object getTaskKey(Cache cache, Object key) {
+ if (key instanceof String) {
+ return new StringBuilder(cache.getName())
+ .append("::")
+ .append(key.toString()).toString();
+ } else {
+ // loginFailure cache
+ return key;
+ }
}
public class CacheTask {
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
index ec6025b..c88e490 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java
@@ -7,12 +7,18 @@ import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.UserSessionProvider;
import org.keycloak.models.UserSessionProviderFactory;
+import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.sessions.infinispan.compat.MemUserSessionProviderFactory;
+import org.keycloak.models.sessions.infinispan.compat.SimpleUserSessionInitializer;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.initializer.InfinispanUserSessionInitializer;
+import org.keycloak.models.sessions.infinispan.initializer.OfflineUserSessionLoader;
+import org.keycloak.models.utils.KeycloakModelUtils;
/**
* Uses Infinispan to store user sessions. On EAP 6.4 (Infinispan 5.2) map reduce is not supported for local caches as a work around
@@ -24,28 +30,19 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
private static final Logger log = Logger.getLogger(InfinispanUserSessionProviderFactory.class);
+ private Config.Scope config;
private Boolean compatMode;
private MemUserSessionProviderFactory compatProviderFactory;
@Override
public UserSessionProvider create(KeycloakSession session) {
- if (compatMode == null) {
- synchronized (this) {
- if (compatMode == null) {
- compatMode = isCompatMode(session);
- if (compatMode) {
- compatProviderFactory = new MemUserSessionProviderFactory();
- log.info("Infinispan version doesn't support map reduce for local cache. Falling back to deprecated mem user session provider.");
- }
- }
- }
- }
if (!compatMode) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<String, SessionEntity> offlineSessionsCache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
- return new InfinispanUserSessionProvider(session, cache, loginFailures);
+ return new InfinispanUserSessionProvider(session, cache, offlineSessionsCache, loginFailures);
} else {
return compatProviderFactory.create(session);
}
@@ -53,11 +50,67 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
@Override
public void init(Config.Scope config) {
+ this.config = config;
+ }
+
+ @Override
+ public void postInit(final KeycloakSessionFactory factory) {
+ KeycloakModelUtils.runJobInTransaction(factory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ compatMode = isCompatMode(session);
+ if (compatMode) {
+ compatProviderFactory = new MemUserSessionProviderFactory();
+ }
+
+ log.debug("Clearing detached sessions from persistent storage");
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+ if (persister == null) {
+ throw new RuntimeException("userSessionPersister not configured. Please see the migration docs and upgrade your configuration");
+ } else {
+ persister.clearDetachedUserSessions();
+ }
+ }
+
+ });
+
+ // Max count of worker errors. Initialization will end with exception when this number is reached
+ int maxErrors = config.getInt("maxErrors", 50);
+
+ // Count of sessions to be computed in each segment
+ int sessionsPerSegment = config.getInt("sessionsPerSegment", 100);
+
+ // TODO: Possibility to run this asynchronously to not block start time
+ loadPersistentSessions(factory, maxErrors, sessionsPerSegment);
}
+
@Override
- public void postInit(KeycloakSessionFactory factory) {
+ public void loadPersistentSessions(final KeycloakSessionFactory sessionFactory, final int maxErrors, final int sessionsPerSegment) {
+ log.debug("Start pre-loading userSessions and clientSessions from persistent storage");
+
+ if (compatMode) {
+ SimpleUserSessionInitializer initializer = new SimpleUserSessionInitializer(sessionFactory, new OfflineUserSessionLoader(), sessionsPerSegment);
+ initializer.loadPersistentSessions();
+
+ } else {
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+ InfinispanUserSessionInitializer initializer = new InfinispanUserSessionInitializer(sessionFactory, cache, new OfflineUserSessionLoader(), maxErrors, sessionsPerSegment, "offlineUserSessions");
+ initializer.initCache();
+ initializer.loadPersistentSessions();
+ }
+
+ });
+ }
+
+ log.debug("Pre-loading userSessions and clientSessions from persistent storage finished");
}
@Override
@@ -72,11 +125,18 @@ public class InfinispanUserSessionProviderFactory implements UserSessionProvider
return "infinispan";
}
- private static boolean isCompatMode(KeycloakSession session) {
+ private boolean isCompatMode(KeycloakSession session) {
+ // For unit tests
+ if (this.config.getBoolean("enforceCompat", false)) {
+ log.info("Enforced compatibility mode for infinispan. Falling back to deprecated mem user session provider.");
+ return true;
+ }
+
if (Version.getVersionShort() < Version.getVersionShort("5.3.0.Final")) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
if (cache.getAdvancedCache().getRpcManager() == null) {
+ log.info("Infinispan version doesn't support map reduce for local cache. Falling back to deprecated mem user session provider.");
return true;
}
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
new file mode 100644
index 0000000..0d038bd
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InfinispanUserSessionInitializer.java
@@ -0,0 +1,268 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.io.Serializable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.infinispan.distexec.DefaultExecutorService;
+import org.infinispan.distexec.DistributedExecutorService;
+import org.infinispan.notifications.Listener;
+import org.infinispan.notifications.cachemanagerlistener.annotation.ViewChanged;
+import org.infinispan.notifications.cachemanagerlistener.event.ViewChangedEvent;
+import org.infinispan.remoting.transport.Transport;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * Startup initialization for reading persistent userSessions/clientSessions to be filled into infinispan/memory . In cluster,
+ * the initialization is distributed among all cluster nodes, so the startup time is even faster
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanUserSessionInitializer {
+
+ private static final Logger log = Logger.getLogger(InfinispanUserSessionInitializer.class);
+
+ private static final String STATE_KEY_PREFIX = "initializerState";
+
+ private final KeycloakSessionFactory sessionFactory;
+ private final Cache<String, SessionEntity> cache;
+ private final SessionLoader sessionLoader;
+ private final int maxErrors;
+ private final int sessionsPerSegment;
+ private final String stateKey;
+
+ private volatile CountDownLatch latch = new CountDownLatch(1);
+
+
+ public InfinispanUserSessionInitializer(KeycloakSessionFactory sessionFactory, Cache<String, SessionEntity> cache, SessionLoader sessionLoader, int maxErrors, int sessionsPerSegment, String stateKeySuffix) {
+ this.sessionFactory = sessionFactory;
+ this.cache = cache;
+ this.sessionLoader = sessionLoader;
+ this.maxErrors = maxErrors;
+ this.sessionsPerSegment = sessionsPerSegment;
+ this.stateKey = STATE_KEY_PREFIX + "::" + stateKeySuffix;
+ }
+
+ public void initCache() {
+ this.cache.getAdvancedCache().getComponentRegistry().registerComponent(sessionFactory, KeycloakSessionFactory.class);
+ cache.getCacheManager().addListener(new ViewChangeListener());
+ }
+
+
+ public void loadPersistentSessions() {
+ if (isFinished()) {
+ return;
+ }
+
+ while (!isFinished()) {
+ if (!isCoordinator()) {
+ try {
+ latch.await(500, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ie) {
+ log.error("Interrupted", ie);
+ }
+ } else {
+ startLoading();
+ }
+ }
+ }
+
+
+ private boolean isFinished() {
+ InitializerState stateEntity = (InitializerState) cache.get(stateKey);
+ return stateEntity != null && stateEntity.isFinished();
+ }
+
+
+ private InitializerState getOrCreateInitializerState() {
+ InitializerState state = (InitializerState) cache.get(stateKey);
+ if (state == null) {
+ final int[] count = new int[1];
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+ @Override
+ public void run(KeycloakSession session) {
+ count[0] = sessionLoader.getSessionsCount(session);
+ }
+
+ });
+
+ state = new InitializerState();
+ state.init(count[0], sessionsPerSegment);
+ saveStateToCache(state);
+ }
+ return state;
+
+ }
+
+
+ private void saveStateToCache(final InitializerState state) {
+
+ // 3 attempts to send the message (it may fail if some node fails in the meantime)
+ retry(3, new Runnable() {
+
+ @Override
+ public void run() {
+
+ // Save this synchronously to ensure all nodes read correct state
+ InfinispanUserSessionInitializer.this.cache.getAdvancedCache().
+ withFlags(Flag.IGNORE_RETURN_VALUES, Flag.FORCE_SYNCHRONOUS)
+ .put(stateKey, state);
+ }
+
+ });
+ }
+
+
+ private boolean isCoordinator() {
+ Transport transport = cache.getCacheManager().getTransport();
+ return transport == null || transport.isCoordinator();
+ }
+
+
+ // Just coordinator is supposed to run this
+ private void startLoading() {
+ InitializerState state = getOrCreateInitializerState();
+
+ // Assume each worker has same processor's count
+ int processors = Runtime.getRuntime().availableProcessors();
+
+ ExecutorService localExecutor = Executors.newCachedThreadPool();
+ DistributedExecutorService distributedExecutorService = new DefaultExecutorService(cache, localExecutor);
+
+ int errors = 0;
+
+ try {
+ while (!state.isFinished()) {
+ Transport transport = cache.getCacheManager().getTransport();
+ int nodesCount = transport==null ? 1 : transport.getMembers().size();
+ int distributedWorkersCount = processors * nodesCount;
+
+ // TODO: debug
+ log.infof("Starting next iteration with %d workers", distributedWorkersCount);
+
+ List<Integer> segments = state.getUnfinishedSegments(distributedWorkersCount);
+
+ // TODO: trace
+ log.info("unfinished segments for this iteration: " + segments);
+
+ List<Future<WorkerResult>> futures = new LinkedList<>();
+ for (Integer segment : segments) {
+ SessionInitializerWorker worker = new SessionInitializerWorker();
+ worker.setWorkerEnvironment(segment, sessionsPerSegment, sessionLoader);
+
+ Future<WorkerResult> future = distributedExecutorService.submit(worker);
+ futures.add(future);
+ }
+
+ for (Future<WorkerResult> future : futures) {
+ try {
+ WorkerResult result = future.get();
+
+ if (result.getSuccess()) {
+ int computedSegment = result.getSegment();
+ state.markSegmentFinished(computedSegment);
+ } else {
+ if (log.isTraceEnabled()) {
+ log.tracef("Segment %d failed to compute", result.getSegment());
+ }
+ }
+ } catch (InterruptedException ie) {
+ errors++;
+ log.error("Interruped exception when computed future. Errors: " + errors, ie);
+ } catch (ExecutionException ee) {
+ errors++;
+ log.error("ExecutionException when computed future. Errors: " + errors, ee);
+ }
+ }
+
+ if (errors >= maxErrors) {
+ throw new RuntimeException("Maximum count of worker errors occured. Limit was " + maxErrors + ". See server.log for details");
+ }
+
+ saveStateToCache(state);
+
+ // TODO
+ log.info("New initializer state pushed. The state is: " + state.printState(false));
+ }
+ } finally {
+ distributedExecutorService.shutdown();
+ localExecutor.shutdown();
+ }
+ }
+
+ private void retry(int retry, Runnable runnable) {
+ while (true) {
+ try {
+ runnable.run();
+ return;
+ } catch (RuntimeException e) {
+ retry--;
+ if (retry == 0) {
+ throw e;
+ }
+ }
+ }
+ }
+
+
+ @Listener
+ public class ViewChangeListener {
+
+ @ViewChanged
+ public void viewChanged(ViewChangedEvent event) {
+ boolean isCoordinator = isCoordinator();
+ // TODO:
+ log.info("View Changed: is coordinator: " + isCoordinator);
+
+ if (isCoordinator) {
+ latch.countDown();
+ latch = new CountDownLatch(1);
+ }
+ }
+
+ }
+
+
+ public static class WorkerResult implements Serializable {
+
+ private Integer segment;
+ private Boolean success;
+
+ public static WorkerResult create (Integer segment, boolean success) {
+ WorkerResult res = new WorkerResult();
+ res.setSegment(segment);
+ res.setSuccess(success);
+ return res;
+ }
+
+ public Integer getSegment() {
+ return segment;
+ }
+
+ public void setSegment(Integer segment) {
+ this.segment = segment;
+ }
+
+ public Boolean getSuccess() {
+ return success;
+ }
+
+ public void setSuccess(Boolean success) {
+ this.success = success;
+ }
+ }
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java
new file mode 100644
index 0000000..eda7370
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/InitializerState.java
@@ -0,0 +1,108 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InitializerState extends SessionEntity {
+
+ private static final Logger log = Logger.getLogger(InitializerState.class);
+
+ private int sessionsCount;
+ private List<Boolean> segments = new ArrayList<>();
+
+
+ public void init(int sessionsCount, int sessionsPerSegment) {
+ this.sessionsCount = sessionsCount;
+
+ int segmentsCount = sessionsCount / sessionsPerSegment;
+ if (sessionsPerSegment * segmentsCount < sessionsCount) {
+ segmentsCount = segmentsCount + 1;
+ }
+
+ // TODO: trace
+ log.info(String.format("sessionsCount: %d, sessionsPerSegment: %d, segmentsCount: %d", sessionsCount, sessionsPerSegment, segmentsCount));
+
+ for (int i=0 ; i<segmentsCount ; i++) {
+ segments.add(false);
+ }
+ }
+
+ // Return true just if computation is entirely finished (all segments are true)
+ public boolean isFinished() {
+ return getNextUnfinishedSegmentFromIndex(0) == -1;
+ }
+
+ // Return next un-finished segments. It can return "segmentCount" segments or less
+ public List<Integer> getUnfinishedSegments(int segmentCount) {
+ List<Integer> result = new ArrayList<>();
+ boolean remaining = true;
+ int next=0;
+ while (remaining && result.size() < segmentCount) {
+ next = getNextUnfinishedSegmentFromIndex(next);
+ if (next == -1) {
+ remaining = false;
+ } else {
+ result.add(next);
+ next++;
+ }
+ }
+
+ return result;
+ }
+
+ public void markSegmentFinished(int index) {
+ segments.set(index, true);
+ }
+
+ private int getNextUnfinishedSegmentFromIndex(int index) {
+ int segmentsSize = segments.size();
+ for (int i=index ; i<segmentsSize ; i++) {
+ Boolean entry = segments.get(i);
+ if (!entry) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ public String printState(boolean includeSegments) {
+ int finished = 0;
+ int nonFinished = 0;
+ List<Integer> finishedList = new ArrayList<>();
+ List<Integer> nonFinishedList = new ArrayList<>();
+
+ int size = segments.size();
+ for (int i=0 ; i<size ; i++) {
+ Boolean done = segments.get(i);
+ if (done) {
+ finished++;
+ if (includeSegments) {
+ finishedList.add(i);
+ }
+ } else {
+ nonFinished++;
+ if (includeSegments) {
+ nonFinishedList.add(i);
+ }
+ }
+ }
+
+ StringBuilder strBuilder = new StringBuilder("sessionsCount: " + sessionsCount)
+ .append(", finished segments count: " + finished)
+ .append(", non-finished segments count: " + nonFinished);
+
+ if (includeSegments) {
+ strBuilder.append(", finished segments: " + finishedList)
+ .append(", non-finished segments: " + nonFinishedList);
+ }
+
+ return strBuilder.toString();
+ }
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
new file mode 100644
index 0000000..e532369
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/OfflineUserSessionLoader.java
@@ -0,0 +1,49 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.util.List;
+
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.util.Time;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionLoader implements SessionLoader {
+
+ @Override
+ public int getSessionsCount(KeycloakSession session) {
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+ return persister.getUserSessionsCount(true);
+ }
+
+ @Override
+ public boolean loadSessions(KeycloakSession session, int first, int max) {
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+ List<UserSessionModel> sessions = persister.loadUserSessions(first, max, true);
+
+ // TODO: Each worker may have different time. Improve if needed...
+ int currentTime = Time.currentTime();
+
+ for (UserSessionModel persistentSession : sessions) {
+
+ // Update and persist lastSessionRefresh time
+ persistentSession.setLastSessionRefresh(currentTime);
+ persister.updateUserSession(persistentSession, true);
+
+ // Save to memory/infinispan
+ UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(persistentSession);
+
+ for (ClientSessionModel persistentClientSession : persistentSession.getClientSessions()) {
+ ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(persistentClientSession);
+ offlineClientSession.setUserSession(offlineUserSession);
+ }
+ }
+
+ return true;
+ }
+
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
new file mode 100644
index 0000000..98fa69e
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionInitializerWorker.java
@@ -0,0 +1,65 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import org.infinispan.Cache;
+import org.infinispan.distexec.DistributedCallable;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SessionInitializerWorker implements DistributedCallable<String, SessionEntity, InfinispanUserSessionInitializer.WorkerResult>, Serializable {
+
+ private static final Logger log = Logger.getLogger(SessionInitializerWorker.class);
+
+ private int segment;
+ private int sessionsPerSegment;
+ private SessionLoader sessionLoader;
+
+ private transient Cache<String, SessionEntity> cache;
+
+ public void setWorkerEnvironment(int segment, int sessionsPerSegment, SessionLoader sessionLoader) {
+ this.segment = segment;
+ this.sessionsPerSegment = sessionsPerSegment;
+ this.sessionLoader = sessionLoader;
+ }
+
+ @Override
+ public void setEnvironment(Cache<String, SessionEntity> cache, Set<String> inputKeys) {
+ this.cache = cache;
+ }
+
+ @Override
+ public InfinispanUserSessionInitializer.WorkerResult call() throws Exception {
+ // TODO
+ log.infof("Running computation for segment: %d", segment);
+
+ KeycloakSessionFactory sessionFactory = cache.getAdvancedCache().getComponentRegistry().getComponent(KeycloakSessionFactory.class);
+ if (sessionFactory == null) {
+ log.warnf("KeycloakSessionFactory not yet set in cache. Worker skipped");
+ return InfinispanUserSessionInitializer.WorkerResult.create(segment, false);
+ }
+
+ final int first = segment * sessionsPerSegment;
+ final int max = sessionsPerSegment;
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ sessionLoader.loadSessions(session, first, max);
+ }
+
+ });
+
+ return InfinispanUserSessionInitializer.WorkerResult.create(segment, true);
+ }
+
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
new file mode 100644
index 0000000..5014147
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/initializer/SessionLoader.java
@@ -0,0 +1,15 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.io.Serializable;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public interface SessionLoader extends Serializable {
+
+ int getSessionsCount(KeycloakSession session);
+
+ boolean loadSessions(KeycloakSession session, int first, int max);
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java
new file mode 100644
index 0000000..8300944
--- /dev/null
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionsOfUserSessionMapper.java
@@ -0,0 +1,44 @@
+package org.keycloak.models.sessions.infinispan.mapreduce;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import org.infinispan.distexec.mapreduce.Collector;
+import org.infinispan.distexec.mapreduce.Mapper;
+import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+
+/**
+ * Return all clientSessions attached to any from input list of userSessions
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class ClientSessionsOfUserSessionMapper implements Mapper<String, SessionEntity, String, ClientSessionEntity>, Serializable {
+
+ private String realm;
+ private Collection<String> userSessions;
+
+ public ClientSessionsOfUserSessionMapper(String realm, Collection<String> userSessions) {
+ this.realm = realm;
+ this.userSessions = userSessions;
+ }
+
+ @Override
+ public void map(String key, SessionEntity e, Collector<String, ClientSessionEntity> collector) {
+ if (!realm.equals(e.getRealm())) {
+ return;
+ }
+
+ if (!(e instanceof ClientSessionEntity)) {
+ return;
+ }
+
+ ClientSessionEntity entity = (ClientSessionEntity) e;
+
+ for (String userSessionId : userSessions) {
+ if (userSessionId.equals(((ClientSessionEntity) e).getUserSession())) {
+ collector.emit(entity.getId(), entity);
+ }
+ }
+ }
+}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index c7104fb..1601acb 100755
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -31,12 +31,16 @@ public class UserSessionAdapter implements UserSessionModel {
private final UserSessionEntity entity;
- public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, UserSessionEntity entity) {
+ private final boolean offline;
+
+ public UserSessionAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm,
+ UserSessionEntity entity, boolean offline) {
this.session = session;
this.provider = provider;
this.cache = cache;
this.realm = realm;
this.entity = entity;
+ this.offline = offline;
}
public String getId() {
@@ -44,6 +48,11 @@ public class UserSessionAdapter implements UserSessionModel {
}
@Override
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
public String getBrokerSessionId() {
return entity.getBrokerSessionId();
}
@@ -129,14 +138,14 @@ public class UserSessionAdapter implements UserSessionModel {
@Override
public List<ClientSessionModel> getClientSessions() {
if (entity.getClientSessions() != null) {
- List<ClientSessionEntity> clientSessions = new LinkedList<ClientSessionEntity>();
+ List<ClientSessionModel> clientSessions = new LinkedList<>();
for (String c : entity.getClientSessions()) {
- ClientSessionEntity clientSession = (ClientSessionEntity) cache.get(c);
+ ClientSessionModel clientSession = provider.getClientSession(realm, c, offline);
if (clientSession != null) {
clientSessions.add(clientSession);
}
}
- return provider.wrapClientSessions(realm, clientSessions);
+ return clientSessions;
} else {
return Collections.emptyList();
}
diff --git a/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java b/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
new file mode 100644
index 0000000..de48a42
--- /dev/null
+++ b/model/sessions-infinispan/src/test/java/org/keycloak/models/sessions/infinispan/initializer/InitializerStateTest.java
@@ -0,0 +1,45 @@
+package org.keycloak.models.sessions.infinispan.initializer;
+
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InitializerStateTest {
+
+ @Test
+ public void testComputationState() {
+ InitializerState state = new InitializerState();
+ state.init(28, 5);
+
+ Assert.assertFalse(state.isFinished());
+ List<Integer> segments = state.getUnfinishedSegments(3);
+ assertContains(segments, 3, 0, 1, 2);
+
+ state.markSegmentFinished(1);
+ state.markSegmentFinished(2);
+ segments = state.getUnfinishedSegments(4);
+ assertContains(segments, 4, 0, 3, 4, 5);
+
+ state.markSegmentFinished(0);
+ state.markSegmentFinished(3);
+ segments = state.getUnfinishedSegments(4);
+ assertContains(segments, 2, 4, 5);
+
+ state.markSegmentFinished(4);
+ state.markSegmentFinished(5);
+ segments = state.getUnfinishedSegments(4);
+ Assert.assertTrue(segments.isEmpty());
+ Assert.assertTrue(state.isFinished());
+ }
+
+ private void assertContains(List<Integer> segments, int expectedLength, int... expected) {
+ Assert.assertEquals(segments.size(), expectedLength);
+ for (int i : expected) {
+ Assert.assertTrue(segments.contains(i));
+ }
+ }
+}
pom.xml 13(+0 -13)
diff --git a/pom.xml b/pom.xml
index 65968cb..e5f7d4a 100755
--- a/pom.xml
+++ b/pom.xml
@@ -58,7 +58,6 @@
<servlet.api.30.version>1.0.2.Final</servlet.api.30.version>
<google.zxing.version>3.2.1</google.zxing.version>
<github.relaxng.version>2011.1</github.relaxng.version>
- <winzipaes.version>1.0.1</winzipaes.version>
<freemarker.version>2.3.23</freemarker.version>
<twitter4j.version>4.0.4</twitter4j.version>
<selenium.version>2.35.0</selenium.version>
@@ -373,13 +372,6 @@
<scope>test</scope>
</dependency>
- <!-- Encrypted ZIP -->
- <dependency>
- <groupId>de.idyl</groupId>
- <artifactId>winzipaes</artifactId>
- <version>${winzipaes.version}</version>
- </dependency>
-
<!-- Apache DS -->
<dependency>
<groupId>org.apache.directory.server</groupId>
@@ -702,11 +694,6 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
- <artifactId>keycloak-export-import-zip</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.keycloak</groupId>
<artifactId>keycloak-kerberos-federation</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index e476280..5f2a368 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -86,13 +86,13 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
return;
}
+ context.setClient(client);
+
if (!client.isEnabled()) {
context.failure(AuthenticationFlowError.CLIENT_DISABLED, null);
return;
}
- context.setClient(client);
-
// Skip client_secret validation for public client
if (client.isPublicClient()) {
context.success();
@@ -106,8 +106,8 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
}
if (client.getSecret() == null) {
- Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Client secret setup required for client " + client.getClientId());
- context.challenge(challengeResponse);
+ Response challengeResponse = ClientAuthUtil.errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "unauthorized_client", "Invalid client secret");
+ context.failure(AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS, challengeResponse);
return;
}
diff --git a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
index 2065de0..225491f 100644
--- a/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/ClientAuthenticationFlow.java
@@ -48,12 +48,9 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
AuthenticationProcessor.Result context = processor.createClientAuthenticatorContext(model, authenticator, executions);
authenticator.authenticateClient(context);
- Response response = processResult(context);
- if (response != null) return response;
ClientModel client = processor.getClient();
if (client != null) {
-
String expectedClientAuthType = client.getClientAuthenticatorType();
// Fallback to secret just in case (for backwards compatibility)
@@ -64,12 +61,16 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
// Check if client authentication matches
if (factory.getId().equals(expectedClientAuthType)) {
+ Response response = processResult(context);
+ if (response != null) return response;
+
+ if (!context.getStatus().equals(FlowStatus.SUCCESS)) {
+ throw new AuthenticationFlowException("Expected success, but for an unknown reason the status was " + context.getStatus(), AuthenticationFlowError.INTERNAL_ERROR);
+ }
+
AuthenticationProcessor.logger.debugv("Client {0} authenticated by {1}", client.getClientId(), factory.getId());
processor.getEvent().detail(Details.CLIENT_AUTH_METHOD, factory.getId());
return null;
- } else {
- throw new AuthenticationFlowException("Client " + client.getClientId() + " was authenticated by incorrect method " + factory.getId(),
- AuthenticationFlowError.INVALID_CLIENT_CREDENTIALS);
}
}
}
@@ -116,7 +117,9 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
if (status == FlowStatus.SUCCESS) {
return null;
- } else if (status == FlowStatus.FAILED) {
+ }
+
+ if (status == FlowStatus.FAILED) {
if (result.getChallenge() != null) {
return sendChallenge(result, execution);
} else {
@@ -130,11 +133,9 @@ public class ClientAuthenticationFlow implements AuthenticationFlow {
if (alternativeChallenge == null) {
alternativeChallenge = result.getChallenge();
}
- return null;
+ return sendChallenge(result, execution);
} else if (status == FlowStatus.FAILURE_CHALLENGE) {
return sendChallenge(result, execution);
- } else if (status == FlowStatus.ATTEMPTED) {
- return null;
} else {
AuthenticationProcessor.logger.error("Unknown result status");
throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR);
diff --git a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
index f29ad39..c94e8cb 100755
--- a/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
+++ b/services/src/main/java/org/keycloak/authentication/FormAuthenticationFlow.java
@@ -18,10 +18,7 @@ import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -116,6 +113,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
private class ValidationContextImpl extends FormContextImpl implements ValidationContext {
FormAction action;
+ String error;
private ValidationContextImpl(AuthenticationExecutionModel executionModel, FormAction action) {
super(executionModel);
@@ -131,6 +129,10 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
this.formData = formData;
}
+ public void error(String error) {
+ this.error = error;
+ }
+
@Override
public void success() {
success = true;
@@ -145,6 +147,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
Map<String, ClientSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
List<FormAction> requiredActions = new LinkedList<>();
List<ValidationContextImpl> successes = new LinkedList<>();
+ List<ValidationContextImpl> errors = new LinkedList<>();
for (AuthenticationExecutionModel formActionExecution : formActionExecutions) {
if (!formActionExecution.isEnabled()) {
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
@@ -183,10 +186,26 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
successes.add(result);
} else {
- processor.logFailure();
executionStatus.put(formActionExecution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
- return renderForm(result.formData, result.errors);
+ errors.add(result);
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ processor.logFailure();
+ List<FormMessage> messages = new LinkedList<>();
+ Set<String> fields = new HashSet<>();
+ for (ValidationContextImpl v : errors) {
+ for (FormMessage m : v.errors) {
+ if (!fields.contains(m.getField())) {
+ fields.add(m.getField());
+ messages.add(m);
+ }
+ }
}
+ ValidationContextImpl first = errors.get(0);
+ first.getEvent().error(first.error);
+ return renderForm(first.formData, messages);
}
for (ValidationContextImpl context : successes) {
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
index ade4e00..ff2442f 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationPassword.java
@@ -59,7 +59,7 @@ public class RegistrationPassword implements FormAction, FormActionFactory {
}
if (errors.size() > 0) {
- context.getEvent().error(Errors.INVALID_REGISTRATION);
+ context.error(Errors.INVALID_REGISTRATION);
formData.remove(RegistrationPage.FIELD_PASSWORD);
formData.remove(RegistrationPage.FIELD_PASSWORD_CONFIRM);
context.validationError(formData, errors);
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
index 2fd3c85..3baae6f 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationProfile.java
@@ -56,15 +56,17 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
}
String email = formData.getFirst(Validation.FIELD_EMAIL);
+ boolean emailValid = true;
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
+ emailValid = false;
} else if (!Validation.isEmailValid(email)) {
- formData.remove(Validation.FIELD_EMAIL);
context.getEvent().detail(Details.EMAIL, email);
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.INVALID_EMAIL));
+ emailValid = false;
}
- if (context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
+ if (emailValid && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
eventError = Errors.EMAIL_IN_USE;
formData.remove(Validation.FIELD_EMAIL);
context.getEvent().detail(Details.EMAIL, email);
@@ -72,7 +74,7 @@ public class RegistrationProfile implements FormAction, FormActionFactory {
}
if (errors.size() > 0) {
- context.getEvent().error(eventError);
+ context.error(eventError);
context.validationError(formData, errors);
return;
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationRecaptcha.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationRecaptcha.java
index 3c1817c..eb10250 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationRecaptcha.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationRecaptcha.java
@@ -108,7 +108,7 @@ public class RegistrationRecaptcha implements FormAction, FormActionFactory, Con
} else {
errors.add(new FormMessage(null, Messages.RECAPTCHA_FAILED));
formData.remove(G_RECAPTCHA_RESPONSE);
- context.getEvent().error(Errors.INVALID_REGISTRATION);
+ context.error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
index 40d2fb0..dfc2a89 100755
--- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
+++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java
@@ -56,9 +56,8 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
String usernameField = RegistrationPage.FIELD_USERNAME;
if (context.getRealm().isRegistrationEmailAsUsername()) {
- username = email;
- context.getEvent().detail(Details.USERNAME, username);
- usernameField = RegistrationPage.FIELD_EMAIL;
+ context.getEvent().detail(Details.USERNAME, email);
+
if (Validation.isBlank(email)) {
errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.MISSING_EMAIL));
} else if (!Validation.isEmailValid(email)) {
@@ -66,33 +65,32 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
formData.remove(Validation.FIELD_EMAIL);
}
if (errors.size() > 0) {
- context.getEvent().error(Errors.INVALID_REGISTRATION);
+ context.error(Errors.INVALID_REGISTRATION);
context.validationError(formData, errors);
return;
}
if (email != null && context.getSession().users().getUserByEmail(email, context.getRealm()) != null) {
- context.getEvent().error(Errors.USERNAME_IN_USE);
+ context.error(Errors.EMAIL_IN_USE);
formData.remove(Validation.FIELD_EMAIL);
- errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.USERNAME_EXISTS));
+ errors.add(new FormMessage(RegistrationPage.FIELD_EMAIL, Messages.EMAIL_EXISTS));
context.validationError(formData, errors);
return;
}
} else {
if (Validation.isBlank(username)) {
- context.getEvent().error(Errors.INVALID_REGISTRATION);
+ context.error(Errors.INVALID_REGISTRATION);
errors.add(new FormMessage(RegistrationPage.FIELD_USERNAME, Messages.MISSING_USERNAME));
context.validationError(formData, errors);
return;
}
- }
- if (context.getSession().users().getUserByUsername(username, context.getRealm()) != null) {
- context.getEvent().error(Errors.USERNAME_IN_USE);
- errors.add(new FormMessage(usernameField, Messages.USERNAME_EXISTS));
- formData.remove(Validation.FIELD_USERNAME);
- formData.remove(Validation.FIELD_EMAIL);
- context.validationError(formData, errors);
- return;
+ if (context.getSession().users().getUserByUsername(username, context.getRealm()) != null) {
+ context.error(Errors.USERNAME_IN_USE);
+ errors.add(new FormMessage(usernameField, Messages.USERNAME_EXISTS));
+ formData.remove(Validation.FIELD_USERNAME);
+ context.validationError(formData, errors);
+ return;
+ }
}
context.success();
diff --git a/services/src/main/java/org/keycloak/authentication/ValidationContext.java b/services/src/main/java/org/keycloak/authentication/ValidationContext.java
index b0c456e..ce96b68 100755
--- a/services/src/main/java/org/keycloak/authentication/ValidationContext.java
+++ b/services/src/main/java/org/keycloak/authentication/ValidationContext.java
@@ -21,6 +21,8 @@ public interface ValidationContext extends FormContext {
*/
void validationError(MultivaluedMap<String, String> formData, List<FormMessage> errors);
+ void error(String error);
+
/**
* Mark this validation as sucessful
*
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 8716d73..0888dab 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -32,7 +32,7 @@ import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
-import org.keycloak.services.offline.OfflineTokenUtils;
+import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.util.TokenUtil;
import org.keycloak.util.Time;
@@ -98,7 +98,7 @@ public class TokenManager {
ClientSessionModel clientSession = null;
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
- clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState());
+ clientSession = new UserSessionManager(session).findOfflineClientSession(realm, oldToken.getClientSession(), oldToken.getSessionState());
if (clientSession != null) {
userSession = clientSession.getUserSession();
}
@@ -161,6 +161,15 @@ public class TokenManager {
}
int currentTime = Time.currentTime();
+
+ if (realm.isRevokeRefreshToken() && !refreshToken.getType().equals(TokenUtil.TOKEN_TYPE_OFFLINE)) {
+ if (refreshToken.getIssuedAt() < validation.clientSession.getTimestamp()) {
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale token");
+ }
+
+ validation.clientSession.setTimestamp(currentTime);
+ }
+
validation.userSession.setLastSessionRefresh(currentTime);
AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
@@ -490,14 +499,15 @@ public class TokenManager {
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
boolean offlineTokenRequested = TokenUtil.isOfflineTokenRequested(scopeParam);
if (offlineTokenRequested) {
- if (!OfflineTokenUtils.isOfflineTokenAllowed(realm, clientSession)) {
+ UserSessionManager sessionManager = new UserSessionManager(session);
+ if (!sessionManager.isOfflineTokenAllowed(clientSession)) {
event.error(Errors.NOT_ALLOWED);
throw new ErrorResponseException("not_allowed", "Offline tokens not allowed for the user or client", Response.Status.BAD_REQUEST);
}
refreshToken = new RefreshToken(accessToken);
refreshToken.type(TokenUtil.TOKEN_TYPE_OFFLINE);
- OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession);
+ sessionManager.persistOfflineSession(clientSession, userSession);
} else {
refreshToken = new RefreshToken(accessToken);
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
index 665662d..ec866eb 100755
--- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
+++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java
@@ -2,16 +2,15 @@ package org.keycloak.services;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.ClientConnection;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.KeycloakContext;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.RealmImporter;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
+import org.keycloak.services.util.LocaleHelper;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriInfo;
+import java.util.Locale;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -87,4 +86,9 @@ public class DefaultKeycloakContext implements KeycloakContext {
manager.setContextPath(getContextPath());
return manager;
}
+
+ @Override
+ public Locale resolveLocale(UserModel user) {
+ return LocaleHelper.getLocale(session, realm, user);
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 73d274e..332d71d 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -33,42 +33,24 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.login.LoginFormsProvider;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.RequiredActionProviderModel;
-import org.keycloak.models.UserConsentModel;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.ProtocolMapperModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.RoleModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
+import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.RealmsResource;
-import org.keycloak.services.Urls;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.util.Time;
-import javax.ws.rs.core.Cookie;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.NewCookie;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriInfo;
-
+import javax.ws.rs.core.*;
import java.net.URI;
import java.util.LinkedList;
import java.util.List;
-import java.util.Locale;
import java.util.Set;
-import org.keycloak.freemarker.LocaleHelper;
-import org.keycloak.util.TokenUtil;
/**
* Stateless object that manages authentication
@@ -413,7 +395,8 @@ public class AuthenticationManager {
}
}
- handleLoginLocale(realm, userSession, request, uriInfo);
+ // Updates users locale if required
+ session.getContext().resolveLocale(userSession.getUser());
// refresh the cookies!
createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
@@ -428,17 +411,6 @@ public class AuthenticationManager {
}
- // If a locale has been set on the login screen, associate that locale with the user
- private static void handleLoginLocale(RealmModel realm, UserSessionModel userSession,
- HttpRequest request, UriInfo uriInfo) {
- Cookie localeCookie = request.getHttpHeaders().getCookies().get(LocaleHelper.LOCALE_COOKIE);
- if (localeCookie == null) return;
-
- UserModel user = userSession.getUser();
- Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, request.getHttpHeaders());
- user.setSingleAttribute(UserModel.LOCALE, locale.toLanguageTag());
- }
-
public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession,
ClientConnection clientConnection,
HttpRequest request, UriInfo uriInfo, EventBuilder event) {
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index afe2bdd..edad002 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -11,6 +11,7 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
+import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
@@ -54,6 +55,11 @@ public class ClientManager {
sessions.onClientRemoved(realm, client);
}
+ UserSessionPersisterProvider sessionsPersister = realmManager.getSession().getProvider(UserSessionPersisterProvider.class);
+ if (sessionsPersister != null) {
+ sessionsPersister.onClientRemoved(realm, client);
+ }
+
UserModel serviceAccountUser = realmManager.getSession().users().getUserByServiceAccountClient(client);
if (serviceAccountUser != null) {
realmManager.getSession().users().removeUser(realm, serviceAccountUser);
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 06c19ac..d60d2ef 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -19,6 +19,7 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.enums.SslRequired;
+import org.keycloak.models.session.UserSessionPersisterProvider;
import org.keycloak.models.utils.RealmImporter;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AdminRoles;
@@ -197,6 +198,11 @@ public class RealmManager implements RealmImporter {
sessions.onRealmRemoved(realm);
}
+ UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
+ if (sessionsPersister != null) {
+ sessionsPersister.onRealmRemoved(realm);
+ }
+
// Remove all periodic syncs for configured federation providers
UsersSyncManager usersSyncManager = new UsersSyncManager();
for (final UserFederationProviderModel fedProvider : federationProviders) {
@@ -210,10 +216,10 @@ public class RealmManager implements RealmImporter {
realm.setEventsEnabled(rep.isEventsEnabled());
realm.setEventsExpiration(rep.getEventsExpiration() != null ? rep.getEventsExpiration() : 0);
if (rep.getEventsListeners() != null) {
- realm.setEventsListeners(new HashSet<String>(rep.getEventsListeners()));
+ realm.setEventsListeners(new HashSet<>(rep.getEventsListeners()));
}
if(rep.getEnabledEventTypes() != null) {
- realm.setEnabledEventTypes(new HashSet<String>(rep.getEnabledEventTypes()));
+ realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes()));
}
realm.setAdminEventsEnabled(rep.isAdminEventsEnabled());
diff --git a/services/src/main/java/org/keycloak/services/managers/UserManager.java b/services/src/main/java/org/keycloak/services/managers/UserManager.java
index 3c0252c..686aedb 100755
--- a/services/src/main/java/org/keycloak/services/managers/UserManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/UserManager.java
@@ -4,6 +4,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionProvider;
+import org.keycloak.models.session.UserSessionPersisterProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -21,6 +22,12 @@ public class UserManager {
if (sessions != null) {
sessions.onUserRemoved(realm, user);
}
+
+ UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class);
+ if (sessionsPersister != null) {
+ sessionsPersister.onUserRemoved(realm, user);
+ }
+
if (session.users().removeUser(realm, user)) {
return true;
}
diff --git a/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
new file mode 100644
index 0000000..5759d20
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/UserSessionManager.java
@@ -0,0 +1,139 @@
+package org.keycloak.services.managers;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.jboss.logging.Logger;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+
+/**
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionManager {
+
+ protected static Logger logger = Logger.getLogger(UserSessionManager.class);
+
+ private final KeycloakSession kcSession;
+ private final UserSessionPersisterProvider persister;
+
+ public UserSessionManager(KeycloakSession session) {
+ this.kcSession = session;
+ this.persister = session.getProvider(UserSessionPersisterProvider.class);
+ }
+
+ public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
+ UserModel user = userSession.getUser();
+
+ // Verify if we already have UserSession with this ID. If yes, don't create another one
+ UserSessionModel offlineUserSession = kcSession.sessions().getOfflineUserSession(clientSession.getRealm(), userSession.getId());
+ if (offlineUserSession == null) {
+ offlineUserSession = createOfflineUserSession(user, userSession);
+ }
+
+ // Create clientSession and save to DB.
+ createOfflineClientSession(user, clientSession, offlineUserSession);
+ }
+
+ // userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
+ public ClientSessionModel findOfflineClientSession(RealmModel realm, String clientSessionId, String userSessionId) {
+ ClientSessionModel clientSession = kcSession.sessions().getOfflineClientSession(realm, clientSessionId);
+ if (clientSession == null) {
+ return null;
+ }
+
+ if (!userSessionId.equals(clientSession.getUserSession().getId())) {
+ throw new ModelException("User session don't match. Offline client session " + clientSession.getId() + ", It's user session " + clientSession.getUserSession().getId() +
+ " Wanted user session: " + userSessionId);
+ }
+
+ return clientSession;
+ }
+
+ public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
+ List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
+ Set<ClientModel> clients = new HashSet<>();
+ for (ClientSessionModel clientSession : clientSessions) {
+ clients.add(clientSession.getClient());
+ }
+ return clients;
+ }
+
+ public boolean revokeOfflineToken(UserModel user, ClientModel client) {
+ RealmModel realm = client.getRealm();
+
+ List<ClientSessionModel> clientSessions = kcSession.sessions().getOfflineClientSessions(realm, user);
+ boolean anyRemoved = false;
+ for (ClientSessionModel clientSession : clientSessions) {
+ if (clientSession.getClient().getId().equals(client.getId())) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
+ user.getUsername(), client.getClientId(), clientSession.getId());
+ }
+
+ kcSession.sessions().removeOfflineClientSession(realm, clientSession.getId());
+ persister.removeClientSession(clientSession.getId(), true);
+ checkOfflineUserSessionHasClientSessions(realm, user, clientSession.getUserSession().getId(), clientSessions);
+ anyRemoved = true;
+ }
+ }
+
+ return anyRemoved;
+ }
+
+ public boolean isOfflineTokenAllowed(ClientSessionModel clientSession) {
+ RoleModel offlineAccessRole = clientSession.getRealm().getRole(Constants.OFFLINE_ACCESS_ROLE);
+ if (offlineAccessRole == null) {
+ logger.warnf("Role '%s' not available in realm", Constants.OFFLINE_ACCESS_ROLE);
+ return false;
+ }
+
+ return clientSession.getRoles().contains(offlineAccessRole.getId());
+ }
+
+ private UserSessionModel createOfflineUserSession(UserModel user, UserSessionModel userSession) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
+ }
+
+ UserSessionModel offlineUserSession = kcSession.sessions().createOfflineUserSession(userSession);
+ persister.createUserSession(userSession, true);
+ return offlineUserSession;
+ }
+
+ private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
+ clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
+ }
+
+ ClientSessionModel offlineClientSession = kcSession.sessions().createOfflineClientSession(clientSession);
+ offlineClientSession.setUserSession(userSession);
+ persister.createClientSession(clientSession, true);
+ }
+
+ // Check if userSession has any offline clientSessions attached to it. Remove userSession if not
+ private void checkOfflineUserSessionHasClientSessions(RealmModel realm, UserModel user, String userSessionId, List<ClientSessionModel> clientSessions) {
+ for (ClientSessionModel clientSession : clientSessions) {
+ if (clientSession.getUserSession().getId().equals(userSessionId)) {
+ return;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
+ }
+ kcSession.sessions().removeOfflineUserSession(realm, userSessionId);
+ persister.removeUserSession(userSessionId, true);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 17de6a2..f130e50 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -56,8 +56,8 @@ import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
+import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.util.UriUtils;
@@ -486,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
// Revoke grant in UserModel
UserModel user = auth.getUser();
user.revokeConsentForClient(client.getId());
- OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
+ new UserSessionManager(session).revokeOfflineToken(user, client);
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java
index 43dfc3e..cde38bc 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminMessagesLoader.java
@@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
+import org.jboss.logging.Logger;
import org.keycloak.freemarker.Theme;
/**
@@ -34,18 +35,73 @@ import org.keycloak.freemarker.Theme;
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class AdminMessagesLoader {
- private static final Map<String, Properties> allMessages = new HashMap<String, Properties>();
+ protected static final Logger logger = Logger.getLogger(AdminConsole.class);
+
+ // theme locale bundle
+ protected static final Map<String, Map<String, Properties>> allMessages = new HashMap<String, Map<String, Properties>>();
static Properties getMessages(Theme theme, String strLocale) throws IOException {
- String allMessagesKey = theme.getName() + "_" + strLocale;
- Properties messages = allMessages.get(allMessagesKey);
- if (messages != null) return messages;
+ String themeName = theme.getName();
+ Map bundlesForTheme = allMessages.get(themeName);
+ if (bundlesForTheme == null) {
+ bundlesForTheme = new HashMap<String, Properties>();
+ allMessages.put(themeName, bundlesForTheme);
+ }
+
+ return findMessagesForTheme(theme, strLocale, bundlesForTheme);
+ }
+
+ private static Properties findMessagesForTheme(Theme theme,
+ String strLocale,
+ Map<String, Properties> bundlesForTheme) throws IOException {
+ Properties messages = bundlesForTheme.get(strLocale);
+ if (messages != null) return messages; // use cached bundle
+
+ // load bundle from theme
Locale locale = Locale.forLanguageTag(strLocale);
messages = theme.getMessages("admin-messages", locale);
- if (messages == null) return new Properties();
- allMessages.put(allMessagesKey, messages);
+ String themeName = theme.getName();
+ if (messages == null) throw new NullPointerException(themeName + ": Unable to find admin-messages bundle for locale=" + strLocale);
+
+ if (!bundlesForTheme.isEmpty()) {
+ // use first bundle as the standard
+ String standardLocale = bundlesForTheme.keySet().iterator().next();
+ Properties standardBundle = bundlesForTheme.get(standardLocale);
+ validateMessages(themeName, standardBundle, standardLocale, messages, strLocale);
+ }
+
+ bundlesForTheme.put(strLocale, messages);
return messages;
}
+
+ private static void validateMessages(String themeName, Properties standardBundle, String standardLocale, Properties messages, String strLocale) {
+ if (standardBundle.keySet().containsAll(messages.keySet()) &&
+ (messages.keySet().containsAll(standardBundle.keySet()))) {
+ return; // it all checks out
+ }
+
+ // otherwise, find the offending keys
+ int warnCount = 0;
+ for (Object key : standardBundle.keySet()) {
+ if (!messages.containsKey(key)) {
+ logger.error(themeName + " theme: Key '" + key + "' not found in admin-messages bundle for locale=" + strLocale +
+ ". However, this key exists in previously loaded bundle for locale=" + standardLocale);
+ warnCount++;
+ }
+
+ if (warnCount > 4) return; // There could be lots of these. Don't fill up the log.
+ }
+
+ for (Object key : messages.keySet()) {
+ if (!standardBundle.containsKey(key)) {
+ logger.error(themeName + " theme: Key '" + key + "' was found in admin-messages bundle for locale=" + strLocale +
+ ". However, this key does not exist in previously loaded bundle for locale=" + standardLocale);
+ warnCount++;
+ }
+
+ if (warnCount > 4) return; // There could be lots of these. Don't fill up the log.
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
index 4b523a1..1d441ec 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationManagementResource.java
@@ -22,6 +22,7 @@ import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
+import org.keycloak.services.ErrorResponse;
import org.keycloak.utils.CredentialHelper;
import javax.ws.rs.Consumes;
@@ -277,7 +278,7 @@ public class AuthenticationManagementResource {
this.auth.requireManage();
if (realm.getFlowByAlias(model.getAlias()) != null) {
- return Response.status(Response.Status.CONFLICT).build();
+ return ErrorResponse.exists("Flow " + model.getAlias() + " already exists");
}
realm.addAuthenticationFlow(model);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 71d8ac0..46e2a09 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -7,11 +7,8 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
-import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
@@ -27,8 +24,6 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
-import org.keycloak.services.offline.OfflineClientSessionAdapter;
-import org.keycloak.services.offline.OfflineUserSessionAdapter;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.ErrorResponse;
import org.keycloak.util.JsonSerialization;
@@ -413,7 +408,7 @@ public class ClientResource {
public Map<String, Integer> getOfflineSessionCount() {
auth.requireView();
Map<String, Integer> map = new HashMap<String, Integer>();
- map.put("count", session.users().getOfflineClientSessionsCount(client.getRealm(), client));
+ map.put("count", session.sessions().getOfflineSessionsCount(client.getRealm(), client));
return map;
}
@@ -435,19 +430,9 @@ public class ClientResource {
firstResult = firstResult != null ? firstResult : -1;
maxResults = maxResults != null ? maxResults : -1;
List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
- for (OfflineClientSessionModel offlineClientSession : session.users().getOfflineClientSessions(client.getRealm(), client, firstResult, maxResults)) {
- UserModel user = session.users().getUserById(offlineClientSession.getUserId(), client.getRealm());
- OfflineUserSessionModel offlineUserSession = session.users().getOfflineUserSession(client.getRealm(), user, offlineClientSession.getUserSessionId());
- OfflineUserSessionAdapter sessionAdapter = new OfflineUserSessionAdapter(offlineUserSession, user);
- OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(offlineClientSession, client.getRealm(), client, sessionAdapter);
-
- UserSessionRepresentation rep = new UserSessionRepresentation();
- rep.setId(sessionAdapter.getId());
- rep.setStart(Time.toMillis(clientSessionAdapter.getTimestamp()));
- rep.setUsername(user.getUsername());
- rep.setUserId(user.getId());
- rep.setIpAddress(sessionAdapter.getIpAddress());
-
+ List<UserSessionModel> userSessions = session.sessions().getOfflineUserSessions(client.getRealm(), client, firstResult, maxResults);
+ for (UserSessionModel userSession : userSessions) {
+ UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(userSession);
sessions.add(rep);
}
return sessions;
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
index e780dbf..5c80bca 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientsResource.java
@@ -73,6 +73,7 @@ public class ClientsResource {
ClientRepresentation client = new ClientRepresentation();
client.setId(clientModel.getId());
client.setClientId(clientModel.getClientId());
+ client.setDescription(clientModel.getDescription());
rep.add(client);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 82dc0bd..bd11684 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -368,7 +368,16 @@ public class RealmAdminResource {
public RealmEventsConfigRepresentation getRealmEventsConfig() {
auth.init(RealmAuth.Resource.EVENTS).requireView();
- return ModelToRepresentation.toEventsConfigReprensetation(realm);
+ RealmEventsConfigRepresentation config = ModelToRepresentation.toEventsConfigReprensetation(realm);
+ if (config.getEnabledEventTypes() == null || config.getEnabledEventTypes().isEmpty()) {
+ config.setEnabledEventTypes(new LinkedList<String>());
+ for (EventType e : EventType.values()) {
+ if (e.isSaveByDefault()) {
+ config.getEnabledEventTypes().add(e.name());
+ }
+ }
+ }
+ return config;
}
/**
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index bf535d9..22ebbb5 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -77,7 +77,7 @@ import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.services.managers.BruteForceProtector;
-import org.keycloak.services.offline.OfflineTokenUtils;
+import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.AccountService;
/**
@@ -451,7 +451,7 @@ public class UsersResource {
List<Map<String, Object>> result = new LinkedList<>();
- Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
+ Set<ClientModel> offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
for (ClientModel client : realm.getClients()) {
UserConsentModel consent = user.getConsentByClient(client.getId());
@@ -496,7 +496,7 @@ public class UsersResource {
ClientModel client = realm.getClientByClientId(clientId);
boolean revokedConsent = user.revokeConsentForClient(client.getId());
- boolean revokedOfflineToken = OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
+ boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client);
if (revokedConsent) {
// Logout clientSessions for this user and client
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 6d9fee9..b6e4ce3 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -24,6 +24,7 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
+import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
@@ -40,6 +41,7 @@ import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.ProtocolMapperModel;
@@ -52,10 +54,14 @@ import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.models.utils.FormMessage;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.RestartLoginCookie;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.ErrorPage;
+import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
@@ -341,10 +347,43 @@ public class LoginActionsService {
return resetCredentials(code, execution);
}
+ /**
+ * Endpoint for executing reset credentials flow. If code is null, a client session is created with the account
+ * service as the client. Successful reset sends you to the account page. Note, account service must be enabled.
+ *
+ * @param code
+ * @param execution
+ * @return
+ */
@Path(RESET_CREDENTIALS_PATH)
@GET
public Response resetCredentialsGET(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
+ // we allow applications to link to reset credentials without going through OAuth or SAML handshakes
+ //
+ if (code == null) {
+ if (!realm.isResetPasswordAllowed()) {
+ event.event(EventType.RESET_PASSWORD);
+ event.error(Errors.NOT_ALLOWED);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+
+ }
+ // set up the account service as the endpoint to call.
+ ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
+ ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+ clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
+ //clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
+ clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
+ String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
+ clientSession.setRedirectUri(redirectUri);
+ clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
+ clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
+ clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
+ clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
+ clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
+ return processResetCredentials(null, clientSession, null);
+ }
return resetCredentials(code, execution);
}
@@ -357,6 +396,13 @@ public class LoginActionsService {
final ClientSessionCode clientCode = checks.clientCode;
final ClientSessionModel clientSession = clientCode.getClientSession();
+ if (!realm.isResetPasswordAllowed()) {
+ event.client(clientCode.getClientSession().getClient());
+ event.error(Errors.NOT_ALLOWED);
+ return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
+
+ }
+
return processResetCredentials(execution, clientSession, null);
}
@@ -573,7 +619,7 @@ public class LoginActionsService {
return checks.response;
}
ClientSessionModel clientSession = checks.clientCode.getClientSession();
- clientSession.setNote("END_AFTER_REQUIRED_ACTIONS", "true");
+ clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true");
return AuthenticationManager.nextActionAfterAuthentication(session, clientSession.getUserSession(), clientSession, clientConnection, request, uriInfo, event);
} else {
diff --git a/services/src/main/java/org/keycloak/services/util/LocaleHelper.java b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java
new file mode 100644
index 0000000..1f523d7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/util/LocaleHelper.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * 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.util;
+
+import org.jboss.resteasy.spi.HttpResponse;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.util.ServerCookie;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.UriInfo;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:gerbermichi@me.com">Michael Gerber</a>
+ */
+public class LocaleHelper {
+
+ private static final String LOCALE_COOKIE = "KEYCLOAK_LOCALE";
+ private static final String UI_LOCALES_PARAM = "ui_locales";
+ private static final String KC_LOCALE_PARAM = "kc_locale";
+
+ public static Locale getLocale(KeycloakSession session, RealmModel realm, UserModel user) {
+ if (!realm.isInternationalizationEnabled()) {
+ return Locale.ENGLISH;
+ } else {
+ Locale locale = getUserLocale(session, realm, user);
+ return locale != null ? locale : Locale.forLanguageTag(realm.getDefaultLocale());
+ }
+ }
+
+ private static Locale getUserLocale(KeycloakSession session, RealmModel realm, UserModel user) {
+ UriInfo uriInfo = session.getContext().getUri();
+ HttpHeaders httpHeaders = session.getContext().getRequestHeaders();
+
+ // kc_locale query parameter
+ if (uriInfo != null && uriInfo.getQueryParameters().containsKey(KC_LOCALE_PARAM)) {
+ String localeString = uriInfo.getQueryParameters().getFirst(KC_LOCALE_PARAM);
+ Locale locale = findLocale(realm.getSupportedLocales(), localeString);
+ if (locale != null) {
+ updateLocaleCookie(session, realm, localeString);
+ if (user != null) {
+ updateUsersLocale(user, localeString);
+ }
+ return locale;
+ }
+ }
+
+ // User profile
+ if (user != null && user.getAttributes().containsKey(UserModel.LOCALE)) {
+ String localeString = user.getFirstAttribute(UserModel.LOCALE);
+ Locale locale = findLocale(realm.getSupportedLocales(), localeString);
+ if (locale != null) {
+ updateLocaleCookie(session, realm, localeString);
+ return locale;
+ }
+ }
+
+ // Locale cookie
+ if (httpHeaders != null && httpHeaders.getCookies().containsKey(LOCALE_COOKIE)) {
+ String localeString = httpHeaders.getCookies().get(LOCALE_COOKIE).getValue();
+ Locale locale = findLocale(realm.getSupportedLocales(), localeString);
+ if (locale != null) {
+ if (user != null) {
+ updateUsersLocale(user, localeString);
+ }
+ return locale;
+ }
+ }
+
+ // ui_locales query parameter
+ if (uriInfo != null && uriInfo.getQueryParameters().containsKey(UI_LOCALES_PARAM)) {
+ String localeString = uriInfo.getQueryParameters().getFirst(UI_LOCALES_PARAM);
+ Locale locale = findLocale(realm.getSupportedLocales(), localeString.split(" "));
+ if (locale != null) {
+ return locale;
+ }
+ }
+
+ // Accept-Language http header
+ if (httpHeaders != null && httpHeaders.getAcceptableLanguages() != null && !httpHeaders.getAcceptableLanguages().isEmpty()) {
+ for (Locale l : httpHeaders.getAcceptableLanguages()) {
+ String localeString = l.toLanguageTag();
+ Locale locale = findLocale(realm.getSupportedLocales(), localeString);
+ if (locale != null) {
+ return locale;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static void updateLocaleCookie(KeycloakSession session,
+ RealmModel realm,
+ String locale) {
+ boolean secure = realm.getSslRequired().isRequired(session.getContext().getUri().getRequestUri().getHost());
+ addCookie(LOCALE_COOKIE, locale, AuthenticationManager.getRealmCookiePath(realm, session.getContext().getUri()), null, null, -1, secure, true);
+ }
+
+ private static void addCookie(String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) {
+ HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
+ StringBuffer cookieBuf = new StringBuffer();
+ ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly);
+ String cookie = cookieBuf.toString();
+ response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie);
+ }
+
+ private static Locale findLocale(Set<String> supportedLocales, String... localeStrings) {
+ for (String localeString : localeStrings) {
+ Locale result = null;
+ Locale search = Locale.forLanguageTag(localeString);
+ for (String languageTag : supportedLocales) {
+ Locale locale = Locale.forLanguageTag(languageTag);
+ if (locale.getLanguage().equals(search.getLanguage())) {
+ if (locale.getCountry().equals("") && result == null) {
+ result = locale;
+ }
+ if (locale.getCountry().equals(search.getCountry())) {
+ return locale;
+ }
+ }
+ }
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ private static void updateUsersLocale(UserModel user, String locale) {
+ if (!locale.equals(user.getFirstAttribute("locale"))) {
+ user.setSingleAttribute(UserModel.LOCALE, locale);
+ }
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
index cfc9270..e3d6a57 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
@@ -13,6 +13,8 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.openqa.selenium.WebDriver;
import java.util.Arrays;
import java.util.Collections;
@@ -41,6 +43,7 @@ public abstract class AbstractClientTest {
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
RealmModel testRealm = manager.createRealm(REALM_NAME);
testRealm.setEnabled(true);
+ testRealm.setAccessCodeLifespanUserAction(600);
KeycloakModelUtils.generateRealmKeys(testRealm);
}
});
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
index 2097175..cf2df9d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
@@ -48,6 +48,7 @@ public class ClientTest extends AbstractClientTest {
private String createClient() {
ClientRepresentation rep = new ClientRepresentation();
rep.setClientId("my-app");
+ rep.setDescription("my-app description");
rep.setEnabled(true);
Response response = realm.clients().create(rep);
response.close();
@@ -79,6 +80,19 @@ public class ClientTest extends AbstractClientTest {
assertTrue(rep.isEnabled());
}
+ /**
+ * See <a href="https://issues.jboss.org/browse/KEYCLOAK-1918">KEYCLOAK-1918</a>
+ */
+ @Test
+ public void getClientDescription() {
+
+ String id = createClient();
+
+ ClientRepresentation rep = realm.clients().get(id).toRepresentation();
+ assertEquals(id, rep.getId());
+ assertEquals("my-app description", rep.getDescription());
+ }
+
@Test
public void getClientSessions() throws Exception {
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index de1b2ca..f262f34 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -1,18 +1,37 @@
package org.keycloak.testsuite.admin;
import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
-
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.forms.ResetPasswordTest;
+import org.keycloak.testsuite.pages.LoginPasswordResetPage;
+import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
+import org.keycloak.testsuite.rule.GreenMailRule;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.openqa.selenium.WebDriver;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -27,6 +46,31 @@ import static org.junit.Assert.fail;
*/
public class UserTest extends AbstractClientTest {
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @Rule
+ public GreenMailRule greenMail = new GreenMailRule();
+
+ @WebResource
+ protected LoginPasswordUpdatePage passwordUpdatePage;
+
+ @WebResource
+ protected WebDriver driver;
+
+ @Before
+ public void before() {
+ super.before();
+
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ RealmModel testRealm = manager.getRealm(REALM_NAME);
+ greenMail.configureRealm(testRealm);
+ }
+ });
+ }
+
public String createUser() {
return createUser("user1", "user1@localhost");
}
@@ -397,6 +441,40 @@ public class UserTest extends AbstractClientTest {
}
@Test
+ public void sendResetPasswordEmailSuccess() throws IOException, MessagingException {
+ UserRepresentation userRep = new UserRepresentation();
+ userRep.setEnabled(true);
+ userRep.setUsername("user1");
+ userRep.setEmail("user1@test.com");
+ Response response = realm.users().create(userRep);
+ String id = ApiUtil.getCreatedId(response);
+ response.close();
+ UserResource user = realm.users().get(id);
+ List<String> actions = new LinkedList<>();
+ actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
+ user.executeActionsEmail("account", actions);
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String link = ResetPasswordTest.getPasswordResetEmailLink(message);
+
+ driver.navigate().to(link);
+
+ assertTrue(passwordUpdatePage.isCurrent());
+
+ passwordUpdatePage.changePassword("new-pass", "new-pass");
+
+ assertEquals("Your account has been updated.", driver.getTitle());
+
+ driver.navigate().to(link);
+
+ assertEquals("We're sorry...", driver.getTitle());
+ }
+
+
+ @Test
public void sendVerifyEmail() {
UserRepresentation userRep = new UserRepresentation();
userRep.setUsername("user1");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
index 84557d9..1fc4fc2 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java
@@ -10,7 +10,6 @@ import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.exportimport.dir.DirExportProvider;
import org.keycloak.exportimport.dir.DirExportProviderFactory;
import org.keycloak.exportimport.singlefile.SingleFileExportProviderFactory;
-import org.keycloak.exportimport.zip.ZipExportProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RealmProvider;
@@ -217,30 +216,6 @@ public class ExportImportTest {
}
}
- @Test
- public void testZipFullExportImport() throws Throwable {
- ExportImportConfig.setProvider(ZipExportProviderFactory.PROVIDER_ID);
- String zipFilePath = getExportImportTestDirectory() + File.separator + "export-full.zip";
- new File(zipFilePath).delete();
- ExportImportConfig.setZipFile(zipFilePath);
- ExportImportConfig.setZipPassword("encPassword");
- ExportImportConfig.setUsersPerFile(ExportImportConfig.DEFAULT_USERS_PER_FILE);
-
- testFullExportImport();
- }
-
- @Test
- public void testZipRealmExportImport() throws Throwable {
- ExportImportConfig.setProvider(ZipExportProviderFactory.PROVIDER_ID);
- String zipFilePath = getExportImportTestDirectory() + File.separator + "export-realm.zip";
- new File(zipFilePath).delete();
- ExportImportConfig.setZipFile(zipFilePath);
- ExportImportConfig.setZipPassword("encPassword");
- ExportImportConfig.setUsersPerFile(3);
-
- testRealmExportImport();
- }
-
private void testFullExportImport() {
ExportImportConfig.setAction(ExportImportConfig.ACTION_EXPORT);
ExportImportConfig.setRealmName(null);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
index 33cc037..418a3d6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/CustomFlowTest.java
@@ -205,7 +205,7 @@ public class CustomFlowTest {
PassThroughClientAuthenticator.clientId = "unknown";
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user", "password");
assertEquals(400, response.getStatusCode());
- assertEquals("invalid_client", response.getError());
+ assertEquals("unauthorized_client", response.getError());
events.expectLogin()
.client((String) null)
@@ -214,7 +214,7 @@ public class CustomFlowTest {
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT)
- .error(Errors.CLIENT_NOT_FOUND)
+ .error(Errors.INVALID_CLIENT_CREDENTIALS)
.assertEvent();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
index 9c2852e..55c7e61 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java
@@ -21,10 +21,7 @@
*/
package org.keycloak.testsuite.forms;
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
+import org.junit.*;
import org.keycloak.events.Details;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
@@ -42,6 +39,8 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
+import static org.junit.Assert.assertEquals;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -80,15 +79,15 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerExistingUser@email", "test-user@localhost", "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Username already exists.", registerPage.getError());
+ assertEquals("Username already exists.", registerPage.getError());
// assert form keeps form fields on error
- Assert.assertEquals("firstName", registerPage.getFirstName());
- Assert.assertEquals("lastName", registerPage.getLastName());
- Assert.assertEquals("", registerPage.getEmail());
- Assert.assertEquals("", registerPage.getUsername());
- Assert.assertEquals("", registerPage.getPassword());
- Assert.assertEquals("", registerPage.getPasswordConfirm());
+ assertEquals("firstName", registerPage.getFirstName());
+ assertEquals("lastName", registerPage.getLastName());
+ assertEquals("registerExistingUser@email", registerPage.getEmail());
+ assertEquals("", registerPage.getUsername());
+ assertEquals("", registerPage.getPassword());
+ assertEquals("", registerPage.getPasswordConfirm());
events.expectRegister("test-user@localhost", "registerExistingUser@email")
.removeDetail(Details.EMAIL)
@@ -104,15 +103,15 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerUserInvalidPasswordConfirm@email", "registerUserInvalidPasswordConfirm", "password", "invalid");
registerPage.assertCurrent();
- Assert.assertEquals("Password confirmation doesn't match.", registerPage.getError());
+ assertEquals("Password confirmation doesn't match.", registerPage.getError());
// assert form keeps form fields on error
- Assert.assertEquals("firstName", registerPage.getFirstName());
- Assert.assertEquals("lastName", registerPage.getLastName());
- Assert.assertEquals("registerUserInvalidPasswordConfirm@email", registerPage.getEmail());
- Assert.assertEquals("registerUserInvalidPasswordConfirm", registerPage.getUsername());
- Assert.assertEquals("", registerPage.getPassword());
- Assert.assertEquals("", registerPage.getPasswordConfirm());
+ assertEquals("firstName", registerPage.getFirstName());
+ assertEquals("lastName", registerPage.getLastName());
+ assertEquals("registerUserInvalidPasswordConfirm@email", registerPage.getEmail());
+ assertEquals("registerUserInvalidPasswordConfirm", registerPage.getUsername());
+ assertEquals("", registerPage.getPassword());
+ assertEquals("", registerPage.getPasswordConfirm());
events.expectRegister("registerUserInvalidPasswordConfirm", "registerUserInvalidPasswordConfirm@email")
.removeDetail(Details.USERNAME)
@@ -129,7 +128,7 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerUserMissingPassword@email", "registerUserMissingPassword", null, null);
registerPage.assertCurrent();
- Assert.assertEquals("Please specify password.", registerPage.getError());
+ assertEquals("Please specify password.", registerPage.getError());
events.expectRegister("registerUserMissingPassword", "registerUserMissingPassword@email")
.removeDetail(Details.USERNAME)
@@ -154,7 +153,7 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerPasswordPolicy@email", "registerPasswordPolicy", "pass", "pass");
registerPage.assertCurrent();
- Assert.assertEquals("Invalid password: minimum length 8.", registerPage.getError());
+ assertEquals("Invalid password: minimum length 8.", registerPage.getError());
events.expectRegister("registerPasswordPolicy", "registerPasswordPolicy@email")
.removeDetail(Details.USERNAME)
@@ -162,7 +161,7 @@ public class RegisterTest {
.user((String) null).error("invalid_registration").assertEvent();
registerPage.register("firstName", "lastName", "registerPasswordPolicy@email", "registerPasswordPolicy", "password", "password");
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerPasswordPolicy", "registerPasswordPolicy@email").assertEvent().getUserId();
@@ -186,7 +185,29 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerUserMissingUsername@email", null, "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Please specify username.", registerPage.getError());
+ assertEquals("Please specify username.", registerPage.getError());
+
+ events.expectRegister(null, "registerUserMissingUsername@email")
+ .removeDetail(Details.USERNAME)
+ .removeDetail(Details.EMAIL)
+ .error("invalid_registration").assertEvent();
+ }
+
+ @Test
+ public void registerUserManyErrors() {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
+
+ registerPage.register(null, null, null, null, null, null);
+
+ registerPage.assertCurrent();
+
+ assertEquals("Please specify username.\n" +
+ "Please specify first name.\n" +
+ "Please specify last name.\n" +
+ "Please specify email.\n" +
+ "Please specify password.", registerPage.getError());
events.expectRegister(null, "registerUserMissingUsername@email")
.removeDetail(Details.USERNAME)
@@ -195,21 +216,29 @@ public class RegisterTest {
}
@Test
- public void registerUserMissingOrInvalidEmail() {
+ public void registerUserMissingEmail() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", null, "registerUserMissingEmail", "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Please specify email.", registerPage.getError());
+ assertEquals("Please specify email.", registerPage.getError());
events.expectRegister("registerUserMissingEmail", null)
.removeDetail("email")
.error("invalid_registration").assertEvent();
+ }
+
+ @Test
+ public void registerUserInvalidEmail() {
+ loginPage.open();
+ loginPage.clickRegister();
+ registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "registerUserInvalidEmailemail", "registerUserInvalidEmail", "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Invalid email address.", registerPage.getError());
+ assertEquals("registerUserInvalidEmailemail", registerPage.getEmail());
+ assertEquals("Invalid email address.", registerPage.getError());
events.expectRegister("registerUserInvalidEmail", "registerUserInvalidEmailemail")
.error("invalid_registration").assertEvent();
}
@@ -222,7 +251,7 @@ public class RegisterTest {
registerPage.register("firstName", "lastName", "registerUserSuccess@email", "registerUserSuccess", "password", "password");
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccess", "registerUserSuccess@email").assertEvent().getUserId();
events.expectLogin().detail("username", "registerusersuccess").user(userId).assertEvent();
@@ -233,10 +262,10 @@ public class RegisterTest {
// test that timestamp is current with 10s tollerance
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
// test user info is set from form
- Assert.assertEquals("registerusersuccess", user.getUsername());
- Assert.assertEquals("registerusersuccess@email", user.getEmail());
- Assert.assertEquals("firstName", user.getFirstName());
- Assert.assertEquals("lastName", user.getLastName());
+ assertEquals("registerusersuccess", user.getUsername());
+ assertEquals("registerusersuccess@email", user.getEmail());
+ assertEquals("firstName", user.getFirstName());
+ assertEquals("lastName", user.getLastName());
}
protected UserModel getUser(String userId) {
@@ -261,9 +290,9 @@ public class RegisterTest {
registerPage.registerWithEmailAsUsername("firstName", "lastName", "test-user@localhost", "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Username already exists.", registerPage.getError());
+ assertEquals("Email already exists.", registerPage.getError());
- events.expectRegister("test-user@localhost", "test-user@localhost").user((String) null).error("username_in_use").assertEvent();
+ events.expectRegister("test-user@localhost", "test-user@localhost").user((String) null).error("email_in_use").assertEvent();
} finally {
configureRelamRegistrationEmailAsUsername(false);
}
@@ -280,12 +309,12 @@ public class RegisterTest {
registerPage.registerWithEmailAsUsername("firstName", "lastName", null, "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Please specify email.", registerPage.getError());
+ assertEquals("Please specify email.", registerPage.getError());
events.expectRegister(null, null).removeDetail("username").removeDetail("email").error("invalid_registration").assertEvent();
registerPage.registerWithEmailAsUsername("firstName", "lastName", "registerUserInvalidEmailemail", "password", "password");
registerPage.assertCurrent();
- Assert.assertEquals("Invalid email address.", registerPage.getError());
+ assertEquals("Invalid email address.", registerPage.getError());
events.expectRegister("registerUserInvalidEmailemail", "registerUserInvalidEmailemail").error("invalid_registration").assertEvent();
} finally {
configureRelamRegistrationEmailAsUsername(false);
@@ -303,7 +332,7 @@ public class RegisterTest {
registerPage.registerWithEmailAsUsername("firstName", "lastName", "registerUserSuccessE@email", "password", "password");
- Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
String userId = events.expectRegister("registerUserSuccessE@email", "registerUserSuccessE@email").assertEvent().getUserId();
events.expectLogin().detail("username", "registerusersuccesse@email").user(userId).assertEvent();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
index 04b0c65..ab47f49 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/ResetPasswordTest.java
@@ -36,6 +36,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
@@ -141,6 +142,64 @@ public class ResetPasswordTest {
}
@Test
+ public void resetPasswordLink() throws IOException, MessagingException {
+ String username = "login-test";
+ String resetUri = Constants.AUTH_SERVER_ROOT + "/realms/test/login-actions/reset-credentials";
+ driver.navigate().to(resetUri);
+
+ resetPasswordPage.assertCurrent();
+
+ resetPasswordPage.changePassword(username);
+
+ loginPage.assertCurrent();
+ assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD)
+ .user(userId)
+ .detail(Details.REDIRECT_URI, Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+ .client("account")
+ .detail(Details.USERNAME, username)
+ .detail(Details.EMAIL, "login@test.com")
+ .session((String)null)
+ .assertEvent();
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String changePasswordUrl = getPasswordResetEmailLink(message);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ updatePasswordPage.assertCurrent();
+
+ updatePasswordPage.changePassword("resetPassword", "resetPassword");
+
+ String sessionId = events.expectRequiredAction(EventType.UPDATE_PASSWORD)
+ .detail(Details.REDIRECT_URI, Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+ .client("account")
+ .user(userId).detail(Details.USERNAME, username).assertEvent().getSessionId();
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, username)
+ .detail(Details.REDIRECT_URI, Constants.AUTH_SERVER_ROOT + "/realms/test/account/")
+ .client("account")
+ .session(sessionId).assertEvent();
+
+ oauth.openLogout();
+
+ events.expectLogout(sessionId).user(userId).session(sessionId).assertEvent();
+
+ loginPage.open();
+
+ loginPage.login("login-test", "resetPassword");
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+ assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ }
+
+
+ @Test
public void resetPassword() throws IOException, MessagingException {
resetPassword("login-test");
}
@@ -571,7 +630,7 @@ public class ResetPasswordTest {
}
}
- private String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
+ public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
Multipart multipart = (Multipart) message.getContent();
final String textContentType = multipart.getBodyPart(0).getContentType();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
index 31e11a4..c0ca3e8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/EmailTest.java
@@ -54,7 +54,6 @@ public class EmailTest {
UserModel user = manager.getSession().users().addUser(appRealm, "login-test");
user.setEmail("login@test.com");
user.setEnabled(true);
- user.setSingleAttribute(UserModel.LOCALE, "de");
UserCredentialModel creds = new UserCredentialModel();
creds.setType(CredentialRepresentation.PASSWORD);
@@ -86,7 +85,7 @@ public class EmailTest {
MimeMessage message = greenMail.getReceivedMessages()[0];
- Assert.assertEquals("Passwort zurückzusetzen", message.getSubject());
+ Assert.assertEquals("Reset password", message.getSubject());
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
@@ -106,4 +105,31 @@ public class EmailTest {
Assert.assertEquals("Reset password", message.getSubject());
}
+
+ @Test
+ public void restPasswordEmailGerman() throws IOException, MessagingException {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "de");
+ }
+ });
+
+ loginPage.open();
+ loginPage.resetPassword();
+ resetPasswordPage.changePassword("login-test");
+
+ assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ Assert.assertEquals("Passwort zurückzusetzen", message.getSubject());
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ manager.getSession().users().getUserByUsername("login-test", appRealm).setSingleAttribute(UserModel.LOCALE, "en");
+ }
+ });
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
index 2d0f9eb..c05dac3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/i18n/LoginPageTest.java
@@ -30,7 +30,6 @@ import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.adapters.HttpClientBuilder;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.OAuthClient;
@@ -87,7 +86,7 @@ public class LoginPageTest {
//test if cookie works
oauth.uiLocales("de");
loginPage.open();
- Assert.assertEquals("English", loginPage.getLanguageDropdownText());
+ Assert.assertEquals("Deutsch", loginPage.getLanguageDropdownText());
driver.manage().deleteAllCookies();
loginPage.open();
@@ -113,10 +112,8 @@ public class LoginPageTest {
loginPage.open();
Response response = client.target(driver.getCurrentUrl()).request().acceptLanguage("de").get();
Assert.assertTrue(response.readEntity(String.class).contains("Anmeldung bei test"));
- Assert.assertEquals("de", response.getCookies().get(LocaleHelper.LOCALE_COOKIE).getValue());
response = client.target(driver.getCurrentUrl()).request().acceptLanguage("en").get();
Assert.assertTrue(response.readEntity(String.class).contains("Log in to test"));
- Assert.assertEquals("en", response.getCookies().get(LocaleHelper.LOCALE_COOKIE).getValue());
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java
new file mode 100644
index 0000000..0eabbe1
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/InfinispanCLI.java
@@ -0,0 +1,226 @@
+package org.keycloak.testsuite;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.infinispan.AdvancedCache;
+import org.infinispan.Cache;
+import org.infinispan.context.Flag;
+import org.jboss.logging.Logger;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.KeycloakSessionTask;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UserSessionProvider;
+import org.keycloak.models.UserSessionProviderFactory;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
+import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.util.Time;
+
+/**
+ * HOWTO USE THIS:
+ *
+ * 1) Run KeycloakServer with system properties (assuming mongo up and running on localhost):
+ * -Dkeycloak.realm.provider=mongo -Dkeycloak.user.provider=mongo -Dkeycloak.userSessionPersister.provider=mongo -Dkeycloak.connectionsMongo.db=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dresources -DstartInfinispanCLI
+ *
+ * 2) Write command on STDIN to persist 50000 userSessions to mongo: persistSessions 50000
+ *
+ * 3) Run command "clear" to ensure infinispan cache is cleared. Doublecheck with command "size" is 0
+ *
+ * 4) Write command to load sessions from persistent storage - 100 sessions per worker transaction: loadPersistentSessions 100
+ *
+ * See the progress in log. Finally run command "size" to ensure size is 100001 (50000 userSessions + 50000 clientSessions + 1 initializationState item)
+ *
+ * 5) Alternative to step 3+4 - Kill the server after step 2 and start two KeycloakServer in parallel on ports 8081 and 8082 . See the progress in logs of loading persistent sessions to infinispan.
+ * Kill the coordinator (usually 8081 node) during startup and see the node 8082 became coordinator and took ownership of loading persistent sessions. After node 8082 fully started, the size of infinispan is again 100001
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class InfinispanCLI {
+
+ private static final Logger log = Logger.getLogger(InfinispanCLI.class);
+
+ private final KeycloakSessionFactory sessionFactory;
+
+ public InfinispanCLI(KeycloakServer server) {
+ this.sessionFactory = server.getSessionFactory();
+ }
+
+ // WARNING: Stdin blocking operation
+ public void start() throws IOException {
+ log.info("Starting infinispan CLI. Exit with 'exit'");
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
+ String line;
+ try {
+ while ((line = reader.readLine()) != null) {
+ log.info("Command: " + line);
+
+ if (line.equals("exit")) {
+ return;
+ }
+
+ final String finalLine = line;
+
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
+ Cache<String, SessionEntity> ispnCache = provider.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME);
+ runTask(finalLine, ispnCache);
+ }
+
+ });
+ }
+ } finally {
+ log.info("Exit infinispan CLI");
+ reader.close();
+ }
+ }
+
+ private void runTask(String line, Cache<String, SessionEntity> cache) {
+ try {
+ String[] splits = line.split(" ");
+ if (splits[0].equals("put")) {
+ UserSessionEntity userSession = new UserSessionEntity();
+ String id = splits[1];
+
+ userSession.setId(id);
+ userSession.setRealm(splits[2]);
+ userSession.setLastSessionRefresh(Time.currentTime());
+ cache.put(id, userSession);
+
+ } else if (splits[0].equals("get")) {
+ String id = splits[1];
+ UserSessionEntity userSession = (UserSessionEntity) cache.get(id);
+ printSession(id, userSession);
+ } else if (splits[0].equals("remove")) {
+ String id = splits[1];
+ cache.remove(id);
+ } else if (splits[0].equals("clear")) {
+ cache.clear();
+ log.info("Cache cleared");
+ } else if (splits[0].equals("size")) {
+ log.info("Size: " + cache.size());
+ } else if (splits[0].equals("list")) {
+ for (String id : cache.keySet()) {
+ SessionEntity entity = cache.get(id);
+ if (!(entity instanceof UserSessionEntity)) {
+ continue;
+ }
+ UserSessionEntity userSession = (UserSessionEntity) cache.get(id);
+ log.info("list: key=" + id + ", value=" + toString(userSession));
+ }
+
+ } else if (splits[0].equals("getLocal")) {
+ String id = splits[1];
+ cache = ((AdvancedCache) cache).withFlags(Flag.CACHE_MODE_LOCAL);
+ UserSessionEntity userSession = (UserSessionEntity) cache.get(id);
+ printSession(id, userSession);
+
+ } else if (splits[0].equals("persistSessions")) {
+
+ final int count = Integer.parseInt(splits[1]);
+ final List<String> userSessionIds = new LinkedList<>();
+ final List<String> clientSessionIds = new LinkedList<>();
+
+ // Create sessions in separate transaction first
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName("master");
+ UserModel john = session.users().getUserByUsername("admin", realm);
+ ClientModel testApp = realm.getClientByClientId("security-admin-console");
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+
+ for (int i=0 ; i<count ; i++) {
+ UserSessionModel userSession = session.sessions().createUserSession(realm, john, "john-doh@localhost", "127.0.0.2", "form", true, null, null);
+ ClientSessionModel clientSession = session.sessions().createClientSession(realm, testApp);
+ clientSession.setUserSession(userSession);
+ clientSession.setRedirectUri("http://redirect");
+ clientSession.setNote("foo", "bar-" + i);
+ userSessionIds.add(userSession.getId());
+ clientSessionIds.add(clientSession.getId());
+ }
+ }
+
+ });
+
+ log.info("Sessions created in infinispan storage");
+
+ // Persist them now
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName("master");
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+
+ for (String userSessionId : userSessionIds) {
+ UserSessionModel userSession = session.sessions().getUserSession(realm, userSessionId);
+ persister.createUserSession(userSession, true);
+ }
+
+ log.info("userSessions persisted");
+
+ for (String clientSessionId : clientSessionIds) {
+ ClientSessionModel clientSession = session.sessions().getClientSession(realm, clientSessionId);
+ persister.createClientSession(clientSession, true);
+ }
+
+ log.info("clientSessions persisted");
+ }
+
+ });
+
+ // Persist them now
+ KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() {
+
+ @Override
+ public void run(KeycloakSession session) {
+ RealmModel realm = session.realms().getRealmByName("master");
+ UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class);
+
+ log.info(count + " sessions persisted. Total number of sessions: " + persister.getUserSessionsCount(true));
+ }
+
+ });
+
+ } else if (splits[0].equals("loadPersistentSessions")) {
+
+ int sessionsPerSegment = Integer.parseInt(splits[1]);
+ UserSessionProviderFactory sessionProviderFactory = (UserSessionProviderFactory) sessionFactory.getProviderFactory(UserSessionProvider.class);
+ sessionProviderFactory.loadPersistentSessions(sessionFactory, 10, sessionsPerSegment);
+
+ log.info("All persistent sessions loaded successfully");
+ }
+ } catch (RuntimeException e) {
+ log.error("Error occured during command. ", e);
+ }
+ }
+
+ private void printSession(String id, UserSessionEntity userSession) {
+ if (userSession == null) {
+ log.info("Not found session with Id: " + id);
+ } else {
+ log.info("Found session. ID: " + toString(userSession));
+ }
+ }
+
+ private String toString(UserSessionEntity userSession) {
+ return "ID: " + userSession.getId() + ", realm: " + userSession.getRealm() + ", lastAccessTime: " + Time.toDate(userSession.getLastSessionRefresh()) +
+ ", clientSessions: " + userSession.getClientSessions().size();
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
index 9409805..78bc481 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/KeycloakServer.java
@@ -200,6 +200,10 @@ public class KeycloakServer {
}
});
+ if (System.getProperties().containsKey("startInfinispanCLI")) {
+ new InfinispanCLI(keycloak).start();
+ }
+
return keycloak;
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
index eff25e3..bb47ebe 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ClientModelTest.java
@@ -32,6 +32,7 @@ public class ClientModelTest extends AbstractModelTest {
realm = realmManager.createRealm("original");
client = realm.addClient("application");
client.setName("Application");
+ client.setDescription("Description");
client.setBaseUrl("http://base");
client.setManagementUrl("http://management");
client.setClientId("app-name");
@@ -87,6 +88,7 @@ public class ClientModelTest extends AbstractModelTest {
public static void assertEquals(ClientModel expected, ClientModel actual) {
Assert.assertEquals(expected.getClientId(), actual.getClientId());
Assert.assertEquals(expected.getName(), actual.getName());
+ Assert.assertEquals(expected.getDescription(), actual.getDescription());
Assert.assertEquals(expected.getBaseUrl(), actual.getBaseUrl());
Assert.assertEquals(expected.getManagementUrl(), actual.getManagementUrl());
Assert.assertEquals(expected.getDefaultRoles(), actual.getDefaultRoles());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 6040c72..b13bcdc 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -7,7 +7,6 @@ import org.junit.runners.MethodSorters;
import org.keycloak.constants.KerberosConstants;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapper;
import org.keycloak.federation.ldap.mappers.FullNameLDAPFederationMapperFactory;
-import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
@@ -15,8 +14,8 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
@@ -330,19 +329,19 @@ public class ImportTest extends AbstractModelTest {
Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
- // Test offline sessions
- Collection<OfflineUserSessionModel> offlineUserSessions = session.users().getOfflineUserSessions(realm, admin);
- Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, admin);
- Assert.assertEquals(offlineUserSessions.size(), 1);
- Assert.assertEquals(offlineClientSessions.size(), 1);
- OfflineUserSessionModel offlineSession = offlineUserSessions.iterator().next();
- OfflineClientSessionModel offlineClSession = offlineClientSessions.iterator().next();
- Assert.assertEquals(offlineSession.getData(), "something1");
- Assert.assertEquals(offlineSession.getUserSessionId(), "123");
- Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId());
- Assert.assertEquals(offlineClSession.getUserSessionId(), "123");
- Assert.assertEquals(offlineClSession.getUserId(), admin.getId());
- Assert.assertEquals(offlineClSession.getData(), "something2");
+// // Test offline sessions
+// Collection<PersistentUserSessionModel> offlineUserSessions = session.users().getOfflineUserSessions(realm, admin);
+// Collection<PersistentClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, admin);
+// Assert.assertEquals(offlineUserSessions.size(), 1);
+// Assert.assertEquals(offlineClientSessions.size(), 1);
+// PersistentUserSessionModel offlineSession = offlineUserSessions.iterator().next();
+// PersistentClientSessionModel offlineClSession = offlineClientSessions.iterator().next();
+// Assert.assertEquals(offlineSession.getData(), "something1");
+// Assert.assertEquals(offlineSession.getUserSessionId(), "123");
+// Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId());
+// Assert.assertEquals(offlineClSession.getUserSessionId(), "123");
+// Assert.assertEquals(offlineClSession.getUserId(), admin.getId());
+// Assert.assertEquals(offlineClSession.getData(), "something2");
// Test service accounts
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index 68fa2f8..fee7bde 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -4,8 +4,8 @@ import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.OfflineClientSessionModel;
-import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.models.session.PersistentClientSessionModel;
+import org.keycloak.models.session.PersistentUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
@@ -286,82 +286,82 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertNull(session.users().getUserByUsername("user1", realm));
}
- @Test
- public void testOfflineSessionsRemoved() {
- RealmModel realm = realmManager.createRealm("original");
- ClientModel fooClient = realm.addClient("foo");
- ClientModel barClient = realm.addClient("bar");
-
- UserModel user1 = session.users().addUser(realm, "user1");
- UserModel user2 = session.users().addUser(realm, "user2");
-
- addOfflineUserSession(realm, user1, "123", "something1");
- addOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2");
- addOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3");
-
- addOfflineUserSession(realm, user2, "2123", "something4");
- addOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5");
-
- commit();
-
- // Searching by clients
- Assert.assertEquals(2, session.users().getOfflineClientSessionsCount(realm, fooClient));
- Assert.assertEquals(1, session.users().getOfflineClientSessionsCount(realm, barClient));
-
- Collection<OfflineClientSessionModel> clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10);
- Assert.assertEquals(2, clientSessions.size());
- clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1);
- OfflineClientSessionModel cls = clientSessions.iterator().next();
- assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2");
- clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1);
- cls = clientSessions.iterator().next();
- assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5");
-
- clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10);
- Assert.assertEquals(1, clientSessions.size());
- cls = clientSessions.iterator().next();
- assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3");
-
- realm = realmManager.getRealmByName("original");
- realm.removeClient(barClient.getId());
-
- commit();
-
- realm = realmManager.getRealmByName("original");
- user1 = session.users().getUserByUsername("user1", realm);
- Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData());
- Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData());
- Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
-
- realm.removeClient(fooClient.getId());
-
- commit();
-
- realm = realmManager.getRealmByName("original");
- user1 = session.users().getUserByUsername("user1", realm);
- Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456"));
- Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
- Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123"));
- Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size());
- Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size());
- }
-
- private void addOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(userSessionId);
- model.setData(data);
- session.users().addOfflineUserSession(realm, user, model);
- }
-
- private void addOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(clientSessionId);
- model.setUserSessionId(userSessionId);
- model.setUserId(user.getId());
- model.setClientId(clientId);
- model.setData(data);
- session.users().addOfflineClientSession(realm, model);
- }
+// @Test
+// public void testOfflineSessionsRemoved() {
+// RealmModel realm = realmManager.createRealm("original");
+// ClientModel fooClient = realm.addClient("foo");
+// ClientModel barClient = realm.addClient("bar");
+//
+// UserModel user1 = session.users().addUser(realm, "user1");
+// UserModel user2 = session.users().addUser(realm, "user2");
+//
+// createOfflineUserSession(realm, user1, "123", "something1");
+// createOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2");
+// createOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3");
+//
+// createOfflineUserSession(realm, user2, "2123", "something4");
+// createOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5");
+//
+// commit();
+//
+// // Searching by clients
+// Assert.assertEquals(2, session.users().getOfflineSessionsCount(realm, fooClient));
+// Assert.assertEquals(1, session.users().getOfflineSessionsCount(realm, barClient));
+//
+// Collection<PersistentClientSessionModel> clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10);
+// Assert.assertEquals(2, clientSessions.size());
+// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1);
+// PersistentClientSessionModel cls = clientSessions.iterator().next();
+// assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2");
+// clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1);
+// cls = clientSessions.iterator().next();
+// assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5");
+//
+// clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10);
+// Assert.assertEquals(1, clientSessions.size());
+// cls = clientSessions.iterator().next();
+// assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3");
+//
+// realm = realmManager.getRealmByName("original");
+// realm.removeClient(barClient.getId());
+//
+// commit();
+//
+// realm = realmManager.getRealmByName("original");
+// user1 = session.users().getUserByUsername("user1", realm);
+// Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData());
+// Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData());
+// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
+//
+// realm.removeClient(fooClient.getId());
+//
+// commit();
+//
+// realm = realmManager.getRealmByName("original");
+// user1 = session.users().getUserByUsername("user1", realm);
+// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456"));
+// Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
+// Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123"));
+// Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size());
+// Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size());
+// }
+//
+// private void createOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) {
+// PersistentUserSessionModel model = new PersistentUserSessionModel();
+// model.setUserSessionId(userSessionId);
+// model.setData(data);
+// session.users().createOfflineUserSession(realm, user, model);
+// }
+//
+// private void createOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
+// PersistentClientSessionModel model = new PersistentClientSessionModel();
+// model.setClientSessionId(clientSessionId);
+// model.setUserSessionId(userSessionId);
+// model.setUserId(user.getId());
+// model.setClientId(clientId);
+// model.setData(data);
+// session.users().createOfflineClientSession(realm, model);
+// }
public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername());
@@ -377,7 +377,7 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions);
}
- private static void assertSessionEquals(OfflineClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId,
+ private static void assertSessionEquals(PersistentClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId,
String expectedClientId, String expectedUserId, String expectedData) {
Assert.assertEquals(cls.getData(), expectedData);
Assert.assertEquals(cls.getClientSessionId(), expectedClientSessionId);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
new file mode 100644
index 0000000..cf7cc8a
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionInitializerTest.java
@@ -0,0 +1,162 @@
+package org.keycloak.testsuite.model;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UserSessionProvider;
+import org.keycloak.models.UserSessionProviderFactory;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.managers.UserManager;
+import org.keycloak.services.managers.UserSessionManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.util.Time;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionInitializerTest {
+
+ @ClassRule
+ public static KeycloakRule kc = new KeycloakRule();
+
+ private KeycloakSession session;
+ private RealmModel realm;
+ private UserSessionManager sessionManager;
+
+ @Before
+ public void before() {
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ session.users().addUser(realm, "user1").setEmail("user1@localhost");
+ session.users().addUser(realm, "user2").setEmail("user2@localhost");
+ sessionManager = new UserSessionManager(session);
+ }
+
+ @After
+ public void after() {
+ resetSession();
+ session.sessions().removeUserSessions(realm);
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+ UserManager um = new UserManager(session);
+ um.removeUser(realm, user1);
+ um.removeUser(realm, user2);
+ kc.stopSession(session, true);
+ }
+
+ @Test
+ public void testUserSessionInitializer() {
+ UserSessionModel[] origSessions = createSessions();
+
+ resetSession();
+
+ // Create and persist offline sessions
+ for (UserSessionModel origSession : origSessions) {
+ UserSessionModel userSession = session.sessions().getUserSession(realm, origSession.getId());
+ for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ sessionManager.persistOfflineSession(clientSession, userSession);
+ }
+ }
+
+ resetSession();
+
+ // Delete cache (persisted sessions are still kept)
+ session.sessions().onRealmRemoved(realm);
+
+ // Clear ispn cache to ensure initializerState is removed as well
+ InfinispanConnectionProvider infinispan = session.getProvider(InfinispanConnectionProvider.class);
+ infinispan.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME).clear();
+
+ resetSession();
+
+ ClientModel testApp = realm.getClientByClientId("test-app");
+ ClientModel thirdparty = realm.getClientByClientId("third-party");
+ Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, testApp));
+ Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(realm, thirdparty));
+
+ int started = Time.currentTime();
+
+ try {
+ // Set some offset to ensure lastSessionRefresh will be updated
+ Time.setOffset(10);
+
+ // Load sessions from persister into infinispan/memory
+ UserSessionProviderFactory userSessionFactory = (UserSessionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(UserSessionProvider.class);
+ userSessionFactory.loadPersistentSessions(session.getKeycloakSessionFactory(), 10, 2);
+
+ resetSession();
+
+ // Assert sessions are in
+ testApp = realm.getClientByClientId("test-app");
+ Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp));
+ Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty));
+
+ List<UserSessionModel> loadedSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10);
+ UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
+
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started+10, "test-app", "third-party");
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app");
+ UserSessionPersisterProviderTest.assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started+10, "test-app");
+ } finally {
+ Time.setOffset(0);
+ }
+ }
+
+ private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+ if (userSession != null) clientSession.setUserSession(userSession);
+ clientSession.setRedirectUri(redirect);
+ if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
+ if (roles != null) clientSession.setRoles(roles);
+ if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers);
+ return clientSession;
+ }
+
+ private UserSessionModel[] createSessions() {
+ UserSessionModel[] sessions = new UserSessionModel[3];
+ sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+
+ Set<String> roles = new HashSet<String>();
+ roles.add("one");
+ roles.add("two");
+
+ Set<String> protocolMappers = new HashSet<String>();
+ protocolMappers.add("mapper-one");
+ protocolMappers.add("mapper-two");
+
+ createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
+ createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ return sessions;
+ }
+
+ private void resetSession() {
+ kc.stopSession(session, true);
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ sessionManager = new UserSessionManager(session);
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
new file mode 100644
index 0000000..53480aa
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionPersisterProviderTest.java
@@ -0,0 +1,335 @@
+package org.keycloak.testsuite.model;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.session.UserSessionPersisterProvider;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.UserManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.util.Time;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionPersisterProviderTest {
+
+ @ClassRule
+ public static KeycloakRule kc = new KeycloakRule();
+
+ private KeycloakSession session;
+ private RealmModel realm;
+ private UserSessionPersisterProvider persister;
+
+ @Before
+ public void before() {
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ session.users().addUser(realm, "user1").setEmail("user1@localhost");
+ session.users().addUser(realm, "user2").setEmail("user2@localhost");
+ persister = session.getProvider(UserSessionPersisterProvider.class);
+ }
+
+ @After
+ public void after() {
+ resetSession();
+ session.sessions().removeUserSessions(realm);
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+ UserManager um = new UserManager(session);
+ um.removeUser(realm, user1);
+ um.removeUser(realm, user2);
+ kc.stopSession(session, true);
+ }
+
+ @Test
+ public void testPersistenceWithLoad() {
+ // Create some sessions in infinispan
+ int started = Time.currentTime();
+ UserSessionModel[] origSessions = createSessions();
+
+ resetSession();
+
+ // Persist 3 created userSessions and clientSessions as offline
+ ClientModel testApp = realm.getClientByClientId("test-app");
+ List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, testApp);
+ for (UserSessionModel userSession : userSessions) {
+ persistUserSession(userSession, true);
+ }
+
+ // Persist 1 online session
+ UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[0].getId());
+ persistUserSession(userSession, false);
+
+ resetSession();
+
+ // Assert online session
+ List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(false, 1, 1, 1);
+ UserSessionProviderTest.assertSession(loadedSessions.get(0), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party");
+
+ // Assert offline sessions
+ loadedSessions = loadPersistedSessionsPaginated(true, 2, 2, 3);
+ UserSessionProviderTest.assertSessions(loadedSessions, origSessions);
+
+ assertSessionLoaded(loadedSessions, origSessions[0].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.1", started, started, "test-app", "third-party");
+ assertSessionLoaded(loadedSessions, origSessions[1].getId(), session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started, "test-app");
+ assertSessionLoaded(loadedSessions, origSessions[2].getId(), session.users().getUserByUsername("user2", realm), "127.0.0.3", started, started, "test-app");
+ }
+
+ @Test
+ public void testUpdateAndRemove() {
+ // Create some sessions in infinispan
+ int started = Time.currentTime();
+ UserSessionModel[] origSessions = createSessions();
+
+ resetSession();
+
+ // Persist 1 offline session
+ UserSessionModel userSession = session.sessions().getUserSession(realm, origSessions[1].getId());
+ persistUserSession(userSession, true);
+
+ resetSession();
+
+ // Load offline session
+ List<UserSessionModel> loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1);
+ UserSessionModel persistedSession = loadedSessions.get(0);
+ UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started, "test-app");
+
+ // Update userSession
+ Time.setOffset(10);
+ try {
+ persistedSession.setLastSessionRefresh(Time.currentTime());
+ persistedSession.setNote("foo", "bar");
+ persistedSession.setState(UserSessionModel.State.LOGGING_IN);
+ persister.updateUserSession(persistedSession, true);
+
+ // create new clientSession
+ ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("third-party"), session.sessions().getUserSession(realm, persistedSession.getId()),
+ "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ persister.createClientSession(clientSession, true);
+
+ resetSession();
+
+ // Assert session updated
+ loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1);
+ persistedSession = loadedSessions.get(0);
+ UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started+10, "test-app", "third-party");
+ Assert.assertEquals("bar", persistedSession.getNote("foo"));
+ Assert.assertEquals(UserSessionModel.State.LOGGING_IN, persistedSession.getState());
+
+ // Remove clientSession
+ persister.removeClientSession(clientSession.getId(), true);
+
+ resetSession();
+
+ // Assert clientSession removed
+ loadedSessions = loadPersistedSessionsPaginated(true, 10, 1, 1);
+ persistedSession = loadedSessions.get(0);
+ UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user1", realm), "127.0.0.2", started, started + 10, "test-app");
+
+ // Remove userSession
+ persister.removeUserSession(persistedSession.getId(), true);
+
+ resetSession();
+
+ // Assert nothing found
+ loadPersistedSessionsPaginated(true, 10, 0, 0);
+ } finally {
+ Time.setOffset(0);
+ }
+ }
+
+ @Test
+ public void testOnRealmRemoved() {
+ RealmModel fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ session.users().addUser(fooRealm, "user3");
+
+ UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ // Persist offline session
+ fooRealm = session.realms().getRealm("foo");
+ userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
+ persistUserSession(userSession, true);
+
+ resetSession();
+
+ // Assert session was persisted
+ loadPersistedSessionsPaginated(true, 10, 1, 1);
+
+ // Remove realm
+ RealmManager realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+
+ resetSession();
+
+ // Assert nothing loaded
+ loadPersistedSessionsPaginated(true, 10, 0, 0);
+ }
+
+ @Test
+ public void testOnClientRemoved() {
+ int started = Time.currentTime();
+
+ RealmModel fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ fooRealm.addClient("bar-app");
+ session.users().addUser(fooRealm, "user3");
+
+ UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ // Persist offline session
+ fooRealm = session.realms().getRealm("foo");
+ userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
+ persistUserSession(userSession, true);
+
+ resetSession();
+
+ RealmManager realmMgr = new RealmManager(session);
+ ClientManager clientMgr = new ClientManager(realmMgr);
+ fooRealm = realmMgr.getRealm("foo");
+
+ // Assert session was persisted with both clientSessions
+ UserSessionModel persistedSession = loadPersistedSessionsPaginated(true, 10, 1, 1).get(0);
+ UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "foo-app", "bar-app");
+
+ // Remove foo-app client
+ ClientModel client = fooRealm.getClientByClientId("foo-app");
+ clientMgr.removeClient(fooRealm, client);
+
+ resetSession();
+
+ realmMgr = new RealmManager(session);
+ clientMgr = new ClientManager(realmMgr);
+ fooRealm = realmMgr.getRealm("foo");
+
+ // Assert just one bar-app clientSession persisted now
+ persistedSession = loadPersistedSessionsPaginated(true, 10, 1, 1).get(0);
+ UserSessionProviderTest.assertSession(persistedSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "bar-app");
+
+ // Remove bar-app client
+ client = fooRealm.getClientByClientId("bar-app");
+ clientMgr.removeClient(fooRealm, client);
+
+ resetSession();
+
+ // Assert nothing loaded - userSession was removed as well because it was last userSession
+ loadPersistedSessionsPaginated(true, 10, 0, 0);
+
+ // Cleanup
+ realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+ }
+
+// @Test
+// public void testExpiredUserSessions() {
+//
+// }
+
+
+ private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
+ if (userSession != null) clientSession.setUserSession(userSession);
+ clientSession.setRedirectUri(redirect);
+ if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
+ if (roles != null) clientSession.setRoles(roles);
+ if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers);
+ return clientSession;
+ }
+
+ private UserSessionModel[] createSessions() {
+ UserSessionModel[] sessions = new UserSessionModel[3];
+ sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+
+ Set<String> roles = new HashSet<String>();
+ roles.add("one");
+ roles.add("two");
+
+ Set<String> protocolMappers = new HashSet<String>();
+ protocolMappers.add("mapper-one");
+ protocolMappers.add("mapper-two");
+
+ createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
+ createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ return sessions;
+ }
+
+ private void persistUserSession(UserSessionModel userSession, boolean offline) {
+ persister.createUserSession(userSession, offline);
+ for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ persister.createClientSession(clientSession, offline);
+ }
+ }
+
+ private void resetSession() {
+ kc.stopSession(session, true);
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ persister = session.getProvider(UserSessionPersisterProvider.class);
+ }
+
+ public static void assertSessionLoaded(List<UserSessionModel> sessions, String id, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
+ for (UserSessionModel session : sessions) {
+ if (session.getId().equals(id)) {
+ UserSessionProviderTest.assertSession(session, user, ipAddress, started, lastRefresh, clients);
+ return;
+ }
+ }
+ Assert.fail("Session with ID " + id + " not found in the list");
+ }
+
+ private List<UserSessionModel> loadPersistedSessionsPaginated(boolean offline, int sessionsPerPage, int expectedPageCount, int expectedSessionsCount) {
+ int count = persister.getUserSessionsCount(offline);
+
+ int start = 0;
+ int pageCount = 0;
+ boolean next = true;
+ List<UserSessionModel> result = new ArrayList<>();
+ while (next && start < count) {
+ List<UserSessionModel> sess = persister.loadUserSessions(start, sessionsPerPage, offline);
+ if (sess.size() == 0) {
+ next = false;
+ } else {
+ pageCount++;
+ start += sess.size();
+ result.addAll(sess);
+ }
+ }
+
+ Assert.assertEquals(pageCount, expectedPageCount);
+ Assert.assertEquals(count, expectedSessionsCount);
+ Assert.assertEquals(count, result.size());
+ return result;
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
new file mode 100644
index 0000000..c843928
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderOfflineTest.java
@@ -0,0 +1,348 @@
+package org.keycloak.testsuite.model;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.UserManager;
+import org.keycloak.services.managers.UserSessionManager;
+import org.keycloak.testsuite.rule.KeycloakRule;
+import org.keycloak.util.Time;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class UserSessionProviderOfflineTest {
+
+ @ClassRule
+ public static KeycloakRule kc = new KeycloakRule();
+
+ private KeycloakSession session;
+ private RealmModel realm;
+ private UserSessionManager sessionManager;
+
+ @Before
+ public void before() {
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ session.users().addUser(realm, "user1").setEmail("user1@localhost");
+ session.users().addUser(realm, "user2").setEmail("user2@localhost");
+ sessionManager = new UserSessionManager(session);
+ }
+
+ @After
+ public void after() {
+ resetSession();
+ session.sessions().removeUserSessions(realm);
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+
+ UserManager um = new UserManager(session);
+ um.removeUser(realm, user1);
+ um.removeUser(realm, user2);
+ kc.stopSession(session, true);
+ }
+
+
+ @Test
+ public void testOfflineSessionsCrud() {
+ // Create some online sessions in infinispan
+ int started = Time.currentTime();
+ UserSessionModel[] origSessions = createSessions();
+
+ resetSession();
+
+ Map<String, String> offlineSessions = new HashMap<>();
+
+ // Persist 3 created userSessions and clientSessions as offline
+ ClientModel testApp = realm.getClientByClientId("test-app");
+ List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, testApp);
+ for (UserSessionModel userSession : userSessions) {
+ offlineSessions.putAll(createOfflineSessionIncludeClientSessions(userSession));
+ }
+
+ resetSession();
+
+ // Assert all previously saved offline sessions found
+ for (Map.Entry<String, String> entry : offlineSessions.entrySet()) {
+ Assert.assertTrue(sessionManager.findOfflineClientSession(realm, entry.getKey(), entry.getValue()) != null);
+
+ UserSessionModel offlineSession = session.sessions().getUserSession(realm, entry.getValue());
+ boolean found = false;
+ for (ClientSessionModel clientSession : offlineSession.getClientSessions()) {
+ if (clientSession.getId().equals(entry.getKey())) {
+ found = true;
+ }
+ }
+ Assert.assertTrue(found);
+ }
+
+ // Find clients with offline token
+ UserModel user1 = session.users().getUserByUsername("user1", realm);
+ Set<ClientModel> clients = sessionManager.findClientsWithOfflineToken(realm, user1);
+ Assert.assertEquals(clients.size(), 2);
+ for (ClientModel client : clients) {
+ Assert.assertTrue(client.getClientId().equals("test-app") || client.getClientId().equals("third-party"));
+ }
+
+ UserModel user2 = session.users().getUserByUsername("user2", realm);
+ clients = sessionManager.findClientsWithOfflineToken(realm, user2);
+ Assert.assertEquals(clients.size(), 1);
+ Assert.assertTrue(clients.iterator().next().getClientId().equals("test-app"));
+
+ // Test count
+ testApp = realm.getClientByClientId("test-app");
+ ClientModel thirdparty = realm.getClientByClientId("third-party");
+ Assert.assertEquals(3, session.sessions().getOfflineSessionsCount(realm, testApp));
+ Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty));
+
+ // Revoke "test-app" for user1
+ sessionManager.revokeOfflineToken(user1, testApp);
+
+ resetSession();
+
+ // Assert userSession revoked
+ testApp = realm.getClientByClientId("test-app");
+ thirdparty = realm.getClientByClientId("third-party");
+ Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, testApp));
+ Assert.assertEquals(1, session.sessions().getOfflineSessionsCount(realm, thirdparty));
+
+ List<UserSessionModel> testAppSessions = session.sessions().getOfflineUserSessions(realm, testApp, 0, 10);
+ List<UserSessionModel> thirdpartySessions = session.sessions().getOfflineUserSessions(realm, thirdparty, 0, 10);
+ Assert.assertEquals(1, testAppSessions.size());
+ Assert.assertEquals("127.0.0.3", testAppSessions.get(0).getIpAddress());
+ Assert.assertEquals("user2", testAppSessions.get(0).getUser().getUsername());
+ Assert.assertEquals(1, thirdpartySessions.size());
+ Assert.assertEquals("127.0.0.1", thirdpartySessions.get(0).getIpAddress());
+ Assert.assertEquals("user1", thirdpartySessions.get(0).getUser().getUsername());
+
+ user1 = session.users().getUserByUsername("user1", realm);
+ user2 = session.users().getUserByUsername("user2", realm);
+ clients = sessionManager.findClientsWithOfflineToken(realm, user1);
+ Assert.assertEquals(1, clients.size());
+ Assert.assertEquals("third-party", clients.iterator().next().getClientId());
+ clients = sessionManager.findClientsWithOfflineToken(realm, user2);
+ Assert.assertEquals(1, clients.size());
+ Assert.assertEquals("test-app", clients.iterator().next().getClientId());
+ }
+
+ @Test
+ public void testOnRealmRemoved() {
+ RealmModel fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ session.users().addUser(fooRealm, "user3");
+
+ UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ // Persist offline session
+ fooRealm = session.realms().getRealm("foo");
+ userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
+ clientSession = session.sessions().getClientSession(fooRealm, clientSession.getId());
+ sessionManager.persistOfflineSession(userSession.getClientSessions().get(0), userSession);
+
+ resetSession();
+
+ ClientSessionModel offlineClientSession = sessionManager.findOfflineClientSession(fooRealm, clientSession.getId(), userSession.getId());
+ Assert.assertEquals("foo-app", offlineClientSession.getClient().getClientId());
+ Assert.assertEquals("user3", offlineClientSession.getUserSession().getUser().getUsername());
+ Assert.assertEquals(offlineClientSession.getId(), offlineClientSession.getUserSession().getClientSessions().get(0).getId());
+
+ // Remove realm
+ RealmManager realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+
+ resetSession();
+
+ fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ session.users().addUser(fooRealm, "user3");
+
+ resetSession();
+
+ // Assert nothing loaded
+ fooRealm = session.realms().getRealm("foo");
+ Assert.assertNull(sessionManager.findOfflineClientSession(fooRealm, clientSession.getId(), userSession.getId()));
+ Assert.assertEquals(0, session.sessions().getOfflineSessionsCount(fooRealm, fooRealm.getClientByClientId("foo-app")));
+
+ // Cleanup
+ realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+ }
+
+ @Test
+ public void testOnClientRemoved() {
+ int started = Time.currentTime();
+
+ RealmModel fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ fooRealm.addClient("bar-app");
+ session.users().addUser(fooRealm, "user3");
+
+ UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+ createClientSession(fooRealm.getClientByClientId("bar-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ // Create offline session
+ fooRealm = session.realms().getRealm("foo");
+ userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
+ createOfflineSessionIncludeClientSessions(userSession);
+
+ resetSession();
+
+ RealmManager realmMgr = new RealmManager(session);
+ ClientManager clientMgr = new ClientManager(realmMgr);
+ fooRealm = realmMgr.getRealm("foo");
+
+ // Assert session was persisted with both clientSessions
+ UserSessionModel offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
+ UserSessionProviderTest.assertSession(offlineSession, session.users().getUserByUsername("user3", fooRealm), "127.0.0.1", started, started, "foo-app", "bar-app");
+
+ // Remove foo-app client
+ ClientModel client = fooRealm.getClientByClientId("foo-app");
+ clientMgr.removeClient(fooRealm, client);
+
+ resetSession();
+
+ realmMgr = new RealmManager(session);
+ clientMgr = new ClientManager(realmMgr);
+ fooRealm = realmMgr.getRealm("foo");
+
+ // Assert just one bar-app clientSession persisted now
+ offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
+ Assert.assertEquals(1, offlineSession.getClientSessions().size());
+ Assert.assertEquals("bar-app", offlineSession.getClientSessions().get(0).getClient().getClientId());
+
+ // Remove bar-app client
+ client = fooRealm.getClientByClientId("bar-app");
+ clientMgr.removeClient(fooRealm, client);
+
+ resetSession();
+
+ // Assert nothing loaded - userSession was removed as well because it was last userSession
+ offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
+ Assert.assertEquals(0, offlineSession.getClientSessions().size());
+
+ // Cleanup
+ realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+ }
+
+ @Test
+ public void testOnUserRemoved() {
+ int started = Time.currentTime();
+
+ RealmModel fooRealm = session.realms().createRealm("foo", "foo");
+ fooRealm.addClient("foo-app");
+ session.users().addUser(fooRealm, "user3");
+
+ UserSessionModel userSession = session.sessions().createUserSession(fooRealm, session.users().getUserByUsername("user3", fooRealm), "user3", "127.0.0.1", "form", true, null, null);
+ ClientSessionModel clientSession = createClientSession(fooRealm.getClientByClientId("foo-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ resetSession();
+
+ // Create offline session
+ fooRealm = session.realms().getRealm("foo");
+ userSession = session.sessions().getUserSession(fooRealm, userSession.getId());
+ createOfflineSessionIncludeClientSessions(userSession);
+
+ resetSession();
+
+ RealmManager realmMgr = new RealmManager(session);
+ fooRealm = realmMgr.getRealm("foo");
+ UserModel user3 = session.users().getUserByUsername("user3", fooRealm);
+
+ // Assert session was persisted with both clientSessions
+ UserSessionModel offlineSession = session.sessions().getOfflineUserSession(fooRealm, userSession.getId());
+ UserSessionProviderTest.assertSession(offlineSession, user3, "127.0.0.1", started, started, "foo-app");
+
+ // Remove user3
+ new UserManager(session).removeUser(fooRealm, user3);
+
+ resetSession();
+
+ // Assert userSession removed as well
+ Assert.assertNull(session.sessions().getOfflineUserSession(fooRealm, userSession.getId()));
+ Assert.assertNull(session.sessions().getOfflineClientSession(fooRealm, clientSession.getId()));
+
+ // Cleanup
+ realmMgr = new RealmManager(session);
+ realmMgr.removeRealm(realmMgr.getRealm("foo"));
+
+ }
+
+ private Map<String, String> createOfflineSessionIncludeClientSessions(UserSessionModel userSession) {
+ Map<String, String> offlineSessions = new HashMap<>();
+
+ UserSessionModel offlineUserSession = session.sessions().createOfflineUserSession(userSession);
+ for (ClientSessionModel clientSession : userSession.getClientSessions()) {
+ ClientSessionModel offlineClientSession = session.sessions().createOfflineClientSession(clientSession);
+ offlineClientSession.setUserSession(offlineUserSession);
+ offlineSessions.put(clientSession.getId(), userSession.getId());
+ }
+ return offlineSessions;
+ }
+
+
+
+ private void resetSession() {
+ kc.stopSession(session, true);
+ session = kc.startSession();
+ realm = session.realms().getRealm("test");
+ sessionManager = new UserSessionManager(session);
+ }
+
+ private ClientSessionModel createClientSession(ClientModel client, UserSessionModel userSession, String redirect, String state, Set<String> roles, Set<String> protocolMappers) {
+ ClientSessionModel clientSession = session.sessions().createClientSession(client.getRealm(), client);
+ if (userSession != null) clientSession.setUserSession(userSession);
+ clientSession.setRedirectUri(redirect);
+ if (state != null) clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, state);
+ if (roles != null) clientSession.setRoles(roles);
+ if (protocolMappers != null) clientSession.setProtocolMappers(protocolMappers);
+ return clientSession;
+ }
+
+ private UserSessionModel[] createSessions() {
+ UserSessionModel[] sessions = new UserSessionModel[3];
+ sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
+
+ Set<String> roles = new HashSet<String>();
+ roles.add("one");
+ roles.add("two");
+
+ Set<String> protocolMappers = new HashSet<String>();
+ protocolMappers.add("mapper-one");
+ protocolMappers.add("mapper-two");
+
+ createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
+ createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
+ createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ return sessions;
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
index 7ca33e2..0b099de 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java
@@ -1,6 +1,7 @@
package org.keycloak.testsuite.model;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
@@ -400,6 +401,19 @@ public class UserSessionProviderTest {
assertPaginatedSession(realm, realm.getClientByClientId("test-app"), 30, 10, 0);
}
+ @Test
+ public void testCreateAndGetInSameTransaction() {
+ UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
+ ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("test-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
+
+ Assert.assertNotNull(session.sessions().getUserSession(realm, userSession.getId()));
+ Assert.assertNotNull(session.sessions().getClientSession(realm, clientSession.getId()));
+
+ Assert.assertEquals(userSession.getId(), clientSession.getUserSession().getId());
+ Assert.assertEquals(1, userSession.getClientSessions().size());
+ Assert.assertEquals(clientSession.getId(), userSession.getClientSessions().get(0).getId());
+ }
+
private void assertPaginatedSession(RealmModel realm, ClientModel client, int start, int max, int expectedSize) {
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, client, start, max);
String[] actualIps = new String[sessions.size()];
@@ -515,7 +529,7 @@ public class UserSessionProviderTest {
realm = session.realms().getRealm("test");
}
- public void assertSessions(List<UserSessionModel> actualSessions, UserSessionModel... expectedSessions) {
+ public static void assertSessions(List<UserSessionModel> actualSessions, UserSessionModel... expectedSessions) {
String[] expected = new String[expectedSessions.length];
for (int i = 0; i < expected.length; i++) {
expected[i] = expectedSessions[i].getId();
@@ -532,7 +546,7 @@ public class UserSessionProviderTest {
assertArrayEquals(expected, actual);
}
- public void assertSession(UserSessionModel session, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
+ public static void assertSession(UserSessionModel session, UserModel user, String ipAddress, int started, int lastRefresh, String... clients) {
assertEquals(user.getId(), session.getUser().getId());
assertEquals(ipAddress, session.getIpAddress());
assertEquals(user.getUsername(), session.getLoginUsername());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 24f4911..86a1c25 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -165,6 +165,21 @@ public class AccessTokenTest {
}
@Test
+ public void accessTokenMissingClientCredentials() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code, null);
+ Assert.assertEquals(400, response.getStatusCode());
+
+ AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_client_credentials").clearDetails().user((String) null).session((String) null);
+ expectedEvent.assertEvent();
+ }
+
+ @Test
public void accessTokenInvalidRedirectUri() throws Exception {
oauth.doLogin("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
index 2aa13b2..2ce5f01 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ClientAuthSignedJWTTest.java
@@ -212,7 +212,7 @@ public class ClientAuthSignedJWTTest {
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
- assertError(response, null, "invalid_client", Errors.INVALID_CLIENT);
+ assertError(response, null, "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
@@ -224,7 +224,7 @@ public class ClientAuthSignedJWTTest {
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
- assertError(response, null, "invalid_client", Errors.INVALID_CLIENT);
+ assertError(response, null, "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
@@ -236,7 +236,7 @@ public class ClientAuthSignedJWTTest {
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
- assertError(response, null, "invalid_client", Errors.INVALID_CLIENT_CREDENTIALS);
+ assertError(response, null, "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
@@ -268,7 +268,7 @@ public class ClientAuthSignedJWTTest {
HttpResponse resp = sendRequest(oauth.getServiceAccountUrl(), parameters);
OAuthClient.AccessTokenResponse response = new OAuthClient.AccessTokenResponse(resp);
- assertError(response, "unknown-client", "invalid_client", Errors.CLIENT_NOT_FOUND);
+ assertError(response, "unknown-client", "unauthorized_client", Errors.INVALID_CLIENT_CREDENTIALS);
}
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
index a8c3d9c..9f95f26 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OfflineTokenTest.java
@@ -343,7 +343,7 @@ public class OfflineTokenTest {
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
- // Now retrieve another offline token and verify that previous offline token is not valid anymore
+ // Now retrieve another offline token and verify that previous offline token is still valid
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken());
@@ -360,21 +360,8 @@ public class OfflineTokenTest {
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent();
- // Refresh with old offline token should fail
- OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
- Assert.assertEquals(400, response.getStatusCode());
- Assert.assertEquals("invalid_grant", response.getError());
-
- events.expectRefresh(offlineToken.getId(), offlineToken.getSessionState())
- .error(Errors.INVALID_TOKEN)
- .client("offline-client")
- .user(serviceAccountUserId)
- .removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
- .removeDetail(Details.TOKEN_ID)
- .detail(Details.REFRESH_TOKEN_TYPE, TokenUtil.TOKEN_TYPE_OFFLINE)
- .assertEvent();
-
- // Refresh with new offline token is ok
+ // Refresh with both offline tokens is fine
+ testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 363a1e9..742f5d6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -30,6 +30,7 @@ import org.keycloak.enums.SslRequired;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.Event;
+import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
@@ -181,6 +182,93 @@ public class RefreshTokenTest {
Time.setOffset(0);
}
+ @Test
+ public void refreshTokenReuseTokenWithoutRefreshTokensRevoked() throws Exception {
+ try {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");
+ RefreshToken refreshToken1 = oauth.verifyRefreshToken(response1.getRefreshToken());
+
+ events.expectCodeToToken(codeId, sessionId).assertEvent();
+
+ Time.setOffset(2);
+
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
+ Assert.assertEquals(200, response2.getStatusCode());
+
+ events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
+
+ AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
+
+ Assert.assertEquals(200, response3.getStatusCode());
+
+ events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
+ } finally {
+ Time.setOffset(0);
+ }
+ }
+
+ @Test
+ public void refreshTokenReuseTokenWithRefreshTokensRevoked() throws Exception {
+ try {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setRevokeRefreshToken(true);
+ }
+ });
+
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ AccessTokenResponse response1 = oauth.doAccessTokenRequest(code, "password");
+ RefreshToken refreshToken1 = oauth.verifyRefreshToken(response1.getRefreshToken());
+
+ events.expectCodeToToken(codeId, sessionId).assertEvent();
+
+ Time.setOffset(2);
+
+ AccessTokenResponse response2 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
+ RefreshToken refreshToken2 = oauth.verifyRefreshToken(response2.getRefreshToken());
+
+ Assert.assertEquals(200, response2.getStatusCode());
+
+ events.expectRefresh(refreshToken1.getId(), sessionId).assertEvent();
+
+ AccessTokenResponse response3 = oauth.doRefreshTokenRequest(response1.getRefreshToken(), "password");
+
+ Assert.assertEquals(400, response3.getStatusCode());
+
+ events.expectRefresh(refreshToken1.getId(), sessionId).removeDetail(Details.TOKEN_ID).removeDetail(Details.UPDATED_REFRESH_TOKEN_ID).error("invalid_token").assertEvent();
+
+ oauth.doRefreshTokenRequest(response2.getRefreshToken(), "password");
+
+ events.expectRefresh(refreshToken2.getId(), sessionId).assertEvent();
+ } finally {
+ Time.setOffset(0);
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setRevokeRefreshToken(false);
+ }
+ });
+ }
+ }
+
PrivateKey privateKey;
PublicKey publicKey;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index 2529091..c07766d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -11,10 +11,7 @@ import org.junit.Test;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserCredentialModel;
-import org.keycloak.models.UserModel;
+import org.keycloak.models.*;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.managers.ClientManager;
@@ -24,6 +21,7 @@ import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
+import org.keycloak.util.Time;
import org.openqa.selenium.WebDriver;
import static org.junit.Assert.assertEquals;
@@ -40,6 +38,9 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
ClientModel app = new ClientManager(manager).createClient(appRealm, "resource-owner");
app.setSecret("secret");
+ ClientModel app2 = new ClientManager(manager).createClient(appRealm, "resource-owner-public");
+ app2.setPublicClient(true);
+
UserModel user = session.users().addUser(appRealm, "direct-login");
user.setEmail("direct-login@localhost");
user.setEnabled(true);
@@ -66,16 +67,22 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
@Test
public void grantAccessTokenUsername() throws Exception {
- grantAccessToken("direct-login");
+ grantAccessToken("direct-login", "resource-owner");
}
@Test
public void grantAccessTokenEmail() throws Exception {
- grantAccessToken("direct-login@localhost");
+ grantAccessToken("direct-login@localhost", "resource-owner");
}
- private void grantAccessToken(String login) throws Exception {
- oauth.clientId("resource-owner");
+ @Test
+ public void grantAccessTokenPublic() throws Exception {
+ grantAccessToken("direct-login", "resource-owner-public");
+ }
+
+
+ private void grantAccessToken(String login, String clientId) throws Exception {
+ oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", login, "password");
@@ -85,7 +92,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
events.expectLogin()
- .client("resource-owner")
+ .client(clientId)
.user(userId)
.session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, "token")
@@ -107,7 +114,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
- events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("resource-owner").assertEvent();
+ events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client(clientId).assertEvent();
}
@Test
@@ -147,8 +154,6 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.error(Errors.INVALID_TOKEN).assertEvent();
}
-
-
@Test
public void grantAccessTokenInvalidClientCredentials() throws Exception {
oauth.clientId("resource-owner");
@@ -169,6 +174,25 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
}
@Test
+ public void grantAccessTokenMissingClientCredentials() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(null, "test-user@localhost", "password");
+
+ assertEquals(400, response.getStatusCode());
+
+ assertEquals("unauthorized_client", response.getError());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session((String) null)
+ .clearDetails()
+ .error(Errors.INVALID_CLIENT_CREDENTIALS)
+ .user((String) null)
+ .assertEvent();
+ }
+
+ @Test
public void grantAccessTokenVerifyEmail() throws Exception {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@@ -207,7 +231,47 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
}
+ @Test
+ public void grantAccessTokenExpiredPassword() throws Exception {
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy("forceExpiredPasswordChange(1)"));
+ }
+ });
+ try {
+ Time.setOffset(60 * 60 * 48);
+
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+
+ assertEquals(400, response.getStatusCode());
+
+ assertEquals("invalid_grant", response.getError());
+ assertEquals("Account is not fully set up", response.getErrorDescription());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .session((String) null)
+ .clearDetails()
+ .error(Errors.RESOLVE_REQUIRED_ACTIONS)
+ .user((String) null)
+ .assertEvent();
+ } finally {
+ Time.setOffset(0);
+
+ keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPasswordPolicy(new PasswordPolicy(""));
+ UserModel user = manager.getSession().users().getUserByEmail("test-user@localhost", appRealm);
+ user.removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
+ }
+ });
+ }
+ }
@Test
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
index 087f82f..2a08ca1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java
@@ -24,7 +24,6 @@ package org.keycloak.testsuite;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
-import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URLEncodedUtils;
@@ -37,7 +36,6 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.constants.AdapterConstants;
-import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
@@ -397,7 +395,7 @@ public class OAuthClient {
b.queryParam(OAuth2Constants.STATE, state);
}
if(uiLocales != null){
- b.queryParam(LocaleHelper.UI_LOCALES_PARAM, uiLocales);
+ b.queryParam(OAuth2Constants.UI_LOCALES_PARAM, uiLocales);
}
if (scope != null) {
b.queryParam(OAuth2Constants.SCOPE, scope);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java
index bd52a59..21a35a6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/GreenMailRule.java
@@ -24,10 +24,13 @@ package org.keycloak.testsuite.rule;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
import org.junit.rules.ExternalResource;
+import org.keycloak.models.RealmModel;
import javax.mail.internet.MimeMessage;
import java.lang.Thread.UncaughtExceptionHandler;
import java.net.SocketException;
+import java.util.HashMap;
+import java.util.Map;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -63,6 +66,14 @@ public class GreenMailRule extends ExternalResource {
}
}
+ public void configureRealm(RealmModel realm) {
+ Map<String, String> config = new HashMap<>();
+ config.put("from", "auto@keycloak.org");
+ config.put("host", "localhost");
+ config.put("port", "3025");
+ realm.setSmtpConfig(config);
+ }
+
public MimeMessage[] getReceivedMessages() {
return greenMail.getReceivedMessages();
}
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index cd56ce0..26fec93 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -22,6 +22,16 @@
"provider": "${keycloak.user.provider:jpa}"
},
+ "userSessions": {
+ "infinispan": {
+ "enforceCompat": "${keycloak.connectionsInfinispan.enforceCompat:false}"
+ }
+ },
+
+ "userSessionPersister": {
+ "provider": "${keycloak.userSessionPersister.provider:jpa}"
+ },
+
"timer": {
"provider": "basic"
},
@@ -68,5 +78,13 @@
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}"
}
+ },
+
+ "connectionsInfinispan": {
+ "default": {
+ "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
+ "async": "${keycloak.connectionsInfinispan.async:true}",
+ "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
+ }
}
}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java
index 77e9a4e..bb578ff 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/authentication/PasswordPolicyTest.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.console.authentication;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.testsuite.console.AbstractConsoleTest;
import org.keycloak.testsuite.console.page.authentication.PasswordPolicy;
@@ -30,7 +31,7 @@ import static org.keycloak.testsuite.console.page.authentication.PasswordPolicy.
* @author Petr Mensik
* @author mhajas
*/
-//@Ignore // FIXME still unstable
+@Ignore // FIXME still unstable
public class PasswordPolicyTest extends AbstractConsoleTest {
@Page
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
index 5608261..33cfeb5 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/console/realm/TokensTest.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.console.realm;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.testsuite.console.page.realm.TokenSettings;
@@ -30,6 +31,7 @@ import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
*
* @author Petr Mensik
*/
+@Ignore
public class TokensTest extends AbstractRealmTest {
@Page
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index c760152..4d4c2f6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -22,6 +22,10 @@
"provider": "${keycloak.user.provider:jpa}"
},
+ "userSessionPersister": {
+ "provider": "${keycloak.userSessionPersister.provider:jpa}"
+ },
+
"userSessions": {
"provider" : "${keycloak.userSessions.provider:infinispan}"
},