keycloak-aplcache
Changes
.gitignore 5(+5 -0)
connections/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 8(+5 -3)
connections/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 5(+5 -0)
connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AddRealmCodeSecret.java 79(+79 -0)
connections/mongo/src/main/java/org/keycloak/connections/mongo/updater/updates/Update1_1_0_Beta1.java 26(+26 -0)
examples/cors/pom.xml 2(+1 -1)
examples/multi-tenant/pom.xml 65(+65 -0)
examples/multi-tenant/README.md 32(+32 -0)
examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/boundary/ProtectedServlet.java 61(+61 -0)
examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/control/PathBasedKeycloakConfigResolver.java 56(+56 -0)
examples/multi-tenant/tenant1-realm.json 57(+57 -0)
examples/multi-tenant/tenant2-realm.json 57(+57 -0)
examples/pom.xml 1(+1 -0)
examples/README.md 6(+6 -0)
forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java 10(+1 -9)
integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java 42(+36 -6)
integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java 5(+5 -0)
integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java 7(+7 -0)
integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java 15(+12 -3)
integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java 46(+34 -12)
integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java 7(+7 -0)
integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java 15(+12 -3)
integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java 47(+36 -11)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java 44(+36 -8)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java 6(+6 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java 5(+5 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java 6(+6 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java 2(+1 -1)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java 2(+1 -1)
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/RealmAdapter.java 9(+9 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 9(+9 -0)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java 7(+2 -5)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java 22(+22 -0)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenancyTest.java 145(+145 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantResolver.java 40(+40 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantServlet.java 47(+47 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java 28(+27 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserSessionProviderTest.java 15(+15 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java 7(+7 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java 11(+10 -1)
testsuite/tomcat7/pom.xml 2(+1 -1)
Details
.gitignore 5(+5 -0)
diff --git a/.gitignore b/.gitignore
index e9d50ff..319769b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,11 @@
.settings
.classpath
+
+# NetBeans #
+############
+nb-configuration.xml
+
# Compiled source #
###################
*.com
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 a5c7e97..7fa4bc7 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
@@ -96,8 +96,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
Configuration invalidationCacheConfiguration = invalidationConfigBuilder.build();
- cacheManager.defineConfiguration("realms", invalidationCacheConfiguration);
- cacheManager.defineConfiguration("users", invalidationCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.REALM_CACHE_NAME, invalidationCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.USER_CACHE_NAME, invalidationCacheConfiguration);
ConfigurationBuilder sessionConfigBuilder = new ConfigurationBuilder();
if (clustered) {
@@ -115,7 +115,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
.numSegments(config.getInt("sessionsSegments", 60)).build();
}
- cacheManager.defineConfiguration("sessions", sessionConfigBuilder.build());
+ Configuration sessionCacheConfiguration = sessionConfigBuilder.build();
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.SESSION_CACHE_NAME, sessionCacheConfiguration);
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, sessionCacheConfiguration);
}
}
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 098d00a..05cfdb9 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
@@ -8,6 +8,11 @@ import org.keycloak.provider.Provider;
*/
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 LOGIN_FAILURE_CACHE_NAME = "loginFailures";
+
<K, V> Cache<K, V> getCache(String name);
}
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AddRealmCodeSecret.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AddRealmCodeSecret.java
new file mode 100644
index 0000000..76a02a7
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AddRealmCodeSecret.java
@@ -0,0 +1,79 @@
+package org.keycloak.connections.jpa.updater.liquibase.custom;
+
+import liquibase.change.custom.CustomSqlChange;
+import liquibase.database.Database;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.CustomChangeException;
+import liquibase.exception.SetupException;
+import liquibase.exception.ValidationErrors;
+import liquibase.resource.ResourceAccessor;
+import liquibase.statement.SqlStatement;
+import liquibase.statement.core.UpdateStatement;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class AddRealmCodeSecret implements CustomSqlChange {
+
+ private String confirmationMessage;
+
+ @Override
+ public SqlStatement[] generateStatements(Database database) throws CustomChangeException {
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Generated codeSecret for realms: ");
+
+ Connection connection = ((JdbcConnection) (database.getConnection())).getWrappedConnection();
+ ResultSet resultSet = connection.createStatement().executeQuery("SELECT ID FROM REALM WHERE CODE_SECRET IS NULL");
+
+ ArrayList<SqlStatement> statements = new ArrayList<SqlStatement>();
+ while (resultSet.next()) {
+ String id = resultSet.getString(1);
+
+ UpdateStatement statement = new UpdateStatement(null, null, "REALM")
+ .addNewColumnValue("CODE_SECRET", KeycloakModelUtils.generateCodeSecret())
+ .setWhereClause("ID='" + id + "'");
+ statements.add(statement);
+
+ if (!resultSet.isFirst()) {
+ sb.append(", ");
+ }
+ sb.append(id);
+ }
+
+ if (!statements.isEmpty()) {
+ confirmationMessage = sb.toString();
+ }
+
+ return statements.toArray(new SqlStatement[statements.size()]);
+ } catch (Exception e) {
+ throw new CustomChangeException("Failed to add realm code secret", e);
+ }
+ }
+
+ @Override
+ public String getConfirmationMessage() {
+ return confirmationMessage;
+ }
+
+ @Override
+ public void setUp() throws SetupException {
+
+ }
+
+ @Override
+ public void setFileOpener(ResourceAccessor resourceAccessor) {
+
+ }
+
+ @Override
+ public ValidationErrors validate(Database database) {
+ return null;
+ }
+
+}
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
index 8344266..c94b206 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.1.0.Beta1.xml
@@ -43,9 +43,10 @@
</addColumn>
<addColumn tableName="REALM">
<column name="CERTIFICATE" type="VARCHAR(2048)"/>
+ <column name="CODE_SECRET" type="VARCHAR(255)"/>
</addColumn>
<addColumn tableName="CLIENT">
- <column name="NODE_REREG_TIMEOUT" type="INT"/>
+ <column name="NODE_REREG_TIMEOUT" type="INT" defaultValue="0"/>
</addColumn>
<addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/>
@@ -53,5 +54,6 @@
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="APP_NODE_REGISTRATIONS" constraintName="FK8454723BA992F594" referencedColumnNames="ID" referencedTableName="CLIENT"/>
+ <customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.AddRealmCodeSecret"/>
</changeSet>
</databaseChangeLog>
\ No newline at end of file
diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/updater/updates/Update1_1_0_Beta1.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/updater/updates/Update1_1_0_Beta1.java
index 89d372f..f080244 100644
--- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/updater/updates/Update1_1_0_Beta1.java
+++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/updater/updates/Update1_1_0_Beta1.java
@@ -1,5 +1,13 @@
package org.keycloak.connections.mongo.updater.updates;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.Arrays;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -14,6 +22,24 @@ public class Update1_1_0_Beta1 extends Update {
public void update() {
deleteEntries("clientSessions");
deleteEntries("sessions");
+
+ addRealmCodeSecret();
+ }
+
+ private void addRealmCodeSecret() {
+ DBCollection realms = db.getCollection("realms");
+
+ DBObject query = new QueryBuilder()
+ .and("codeSecret").is(null).get();
+
+ DBCursor objects = realms.find(query);
+ while (objects.hasNext()) {
+ DBObject object = objects.next();
+ object.put("codeSecret", KeycloakModelUtils.generateCodeSecret());
+ realms.save(object);
+
+ log.debugv("Added realm.codeSecret, id={0}", object.get("id"));
+ }
}
}
diff --git a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
index 4658c68..830a34e 100755
--- a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
+++ b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
@@ -48,6 +48,10 @@ public class KeycloakSecurityContext implements Serializable {
return idTokenString;
}
+ public String getRealm() {
+ // Assumption that issuer contains realm name
+ return token.getIssuer();
+ }
// SERIALIZATION
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 3346371..b0a0a21 100755
--- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java
@@ -44,6 +44,7 @@ public class RealmRepresentation {
protected String privateKey;
protected String publicKey;
protected String certificate;
+ protected String codeSecret;
protected RolesRepresentation roles;
protected List<String> defaultRoles;
protected Set<String> requiredCredentials;
@@ -229,6 +230,14 @@ public class RealmRepresentation {
this.certificate = certificate;
}
+ public String getCodeSecret() {
+ return codeSecret;
+ }
+
+ public void setCodeSecret(String codeSecret) {
+ this.codeSecret = codeSecret;
+ }
+
public Boolean isPasswordCredentialGrantAllowed() {
return passwordCredentialGrantAllowed;
}
diff --git a/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
index f3a89b6..f2ec502 100755
--- a/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
+++ b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
@@ -96,12 +96,14 @@ public class SkeletonKeyTokenTest {
Assert.assertTrue(token.getResourceAccess("foo").isUserInRole("admin"));
Assert.assertTrue(token.getResourceAccess("bar").isUserInRole("user"));
Assert.assertEquals("joe@email.cz", idToken.getEmail());
+ Assert.assertEquals("acme", ctx.getRealm());
ois.close();
}
private AccessToken createSimpleToken() {
AccessToken token = new AccessToken();
token.id("111");
+ token.issuer("acme");
token.addAccess("foo").addRole("admin");
token.addAccess("bar").addRole("user");
return token;
diff --git a/distribution/examples-docs-zip/build.xml b/distribution/examples-docs-zip/build.xml
index 3e4f1dc..a33d88d 100755
--- a/distribution/examples-docs-zip/build.xml
+++ b/distribution/examples-docs-zip/build.xml
@@ -42,6 +42,14 @@
<exclude name="**/subsystem-config.xml"/>
</fileset>
</copy>
+ <copy todir="target/examples/multi-tenant" overwrite="true">
+ <fileset dir="../../examples/multi-tenant">
+ <exclude name="**/target/**"/>
+ <exclude name="**/*.iml"/>
+ <exclude name="**/*.unconfigured"/>
+ <exclude name="**/subsystem-config.xml"/>
+ </fileset>
+ </copy>
<copy todir="target/examples/themes" overwrite="true">
<fileset dir="../../examples/themes">
<exclude name="**/target/**"/>
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 6a70415..db6dead 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -36,6 +36,7 @@
<!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml">
<!ENTITY Clustering SYSTEM "modules/clustering.xml">
<!ENTITY ApplicationClustering SYSTEM "modules/application-clustering.xml">
+ <!ENTITY MultiTenancy SYSTEM "modules/multi-tenancy.xml">
]>
<book>
@@ -88,6 +89,7 @@ This one is short
&JavascriptAdapter;
&InstalledApplications;
&Logout;
+ &MultiTenancy;
</chapter>
<chapter>
diff --git a/docbook/reference/en/en-US/modules/clustering.xml b/docbook/reference/en/en-US/modules/clustering.xml
index 613316f..8c24dea 100755
--- a/docbook/reference/en/en-US/modules/clustering.xml
+++ b/docbook/reference/en/en-US/modules/clustering.xml
@@ -53,7 +53,7 @@
realm keys and password hashes) from being sent between the nodes.
</para>
<para>
- User sessions supports either distributed caches or fully replicated caches. We recommend using a distributed
+ User sessions and login failures supports either distributed caches or fully replicated caches. We recommend using a distributed
cache.
</para>
<para>
@@ -65,6 +65,7 @@
<invalidation-cache name="realms" mode="SYNC"/>
<invalidation-cache name="users" mode="SYNC"/>
<distributed-cache name="sessions" mode="SYNC" owners="1" />
+ <distributed-cache name="loginFailures" mode="SYNC" owners="1" />
</cache-container>
...
</subsystem>
diff --git a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
index af5d66c..09109b3 100755
--- a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
+++ b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
@@ -1,134 +1,215 @@
<chapter id="Migration_from_older_versions">
<title>Migration from older versions</title>
- <sect1>
- <title>Migrating from 1.0.x.Final to 1.1.Beta1</title>
- <itemizedlist>
- <listitem>UserSessionModel JPA and Mongo storage schema has changed as these interfaces have been refactored</listitem>
- <listitem>
- Upgrade your adapters, old adapters are not compatible with Keycloak 1.1. We interpreted JSON Web Token and OIDC ID Token specification incorrectly. 'aud'
- claim must be the client id, we were storing the realm name in there and validating it.
- </listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 RC-1 to RC-2</title>
- <itemizedlist>
- <listitem>A lot of info level logging has been changed to debug. Also, a realm no longer has the jboss-logging audit listener by default.
- If you want log output when users login, logout, change passwords, etc. enable the jboss-logging audit listener through the admin console.</listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 Beta 4 to RC-1</title>
- <itemizedlist>
- <listitem>
- logout REST API has been refactored. The GET request on the logout URI does not take a session_state
- parameter anymore. You must be logged in in order to log out the session.
- You can also POST to the logout REST URI. This action requires a valid refresh token to perform the logout.
- The signature is the same as refresh token minus the grant type form parameter. See documentation for details.
- </listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 Beta 1 to Beta 4</title>
- <itemizedlist>
- <listitem>
- LDAP/AD configuration is changed. It is no longer under the "Settings" page. It is now under
- Users->Federation. Add Provider will show you an "ldap" option.
- </listitem>
- <listitem>
- Authentication SPI has been removed and rewritten. The new SPI is UserFederationProvider and is
- more flexible.
- </listitem>
- <listitem>
- <literal>ssl-not-required</literal> property in adapter config has been removed. Replaced with
- <literal>ssl-required</literal>, valid values are <literal>all</literal> (require SSL for all requests), <literal>external</literal>
- (require SSL only for external request) and <literal>none</literal> (SSL not required).
- </listitem>
- <listitem>
- DB Schema has changed again.
- </listitem>
- <listitem>
- Created applications now have a full scope by default. This means that you don't have to configure
- the scope of an application if you don't want to.
- </listitem>
- <listitem>
- Format of JSON file for importing realm data was changed. Now role mappings is available under the JSON record of particular
- user.
- </listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 Alpha 4 to Beta 1</title>
- <itemizedlist>
- <listitem>
- DB Schema has changed. We have added export of the database to Beta 1, but not the ability to import
- the database from older versions. This will be supported in future releases.
- </listitem>
- <listitem>
- For all clients except bearer-only applications, you must specify at least one redirect uri. Keycloak
- will not allow you to log in unless you have specified a valid redirect uri for that application.
- </listitem>
- <listitem>
- Resource Owner Password Credentials flow is now disabled by default. It can be enabled by setting the toggle
- for <literal>Direct Grant API</literal> <literal>ON</literal> under realm config in the admin console.
- </listitem>
- <listitem>
- Configuration is now done through <literal>standalone/configuration/keycloak-server.json</literal>. This
- should mainly affect those that use MongoDB.
- </listitem>
- <listitem>
- JavaScript adapter has been refactored. See the <link linkend='javascript-adapter'>JavaScript adapter</link> section for more details.
- </listitem>
- <listitem>
- The "Central Login Lifespan" setting no longer exists. Please see the <link linkend='session-timeouts'>Session Timeout</link> section
- for me details.
- </listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 Alpha 2 to Alpha 3</title>
- <itemizedlist>
- <listitem>
- SkeletonKeyToken, SkeletonKeyScope, SkeletonKeyPrincipal, and SkeletonKeySession have been renamed to:
- AccessToken, AccessScope, KeycloakPrincipal, and KeycloakAuthenticatedSession respectively.
- </listitem>
- <listitem>
- ServleOAuthClient.getBearerToken() method signature has changed. It now returns an AccessTokenResponse
- so that you can obtain a refresh token too.
- </listitem>
- <listitem>
- Adapters now check the access token expiration with every request. If the token is expired, they will
- attempt to invoke a refresh on the auth server using a saved refresh token.
- </listitem>
- <listitem>
- Subject in AccessToken has been changed to the User ID.
- </listitem>
- </itemizedlist>
- </sect1>
- <sect1>
- <title>Migrating from 1.0 Alpha 1 to Alpha 2</title>
- <itemizedlist>
- <listitem>
- DB Schema has changed. We don't have any data migration utilities yet as of Alpha 2.
- </listitem>
- <listitem>
- JBoss and Wildfly adapters are now installed via a JBoss/Wildfly subsystem. Please review the adapter
- installation documentation. Edits to standalone.xml are now required.
- </listitem>
- <listitem>
- There is a new credential type "secret". Unlike other credential types, it is stored in plain text in
- the database and can be viewed in the admin console.
- </listitem>
- <listitem>
- There is no longer required Application or OAuth Client credentials. These client types are now
- hard coded to use the "secret" credential type.
- </listitem>
- <listitem>
- Because of the "secret" credential change to Application and OAuth Client, you'll have to update
- your keycloak.json configuration files and regenarate a secret within the Application or OAuth Client
- credentials tab in the administration console.
- </listitem>
- </itemizedlist>
- </sect1>
+ <para>
+ To upgrade to a new version of Keycloak first download and install the new version of Keycloak. You then have to
+ migrate the database, keycloak-server.json, providers, themes and applications from the old version.
+ </para>
+
+ <section>
+ <title>Migrate database</title>
+ <para>
+ Keycloak provides automatic migration of the database. It's highly recommended that you backup your
+ database prior to upgrading Keycloak.
+ </para>
+ <para>
+ To enable automatic upgrading of the database if you're using a relational database make sure
+ <literal>databaseSchema</literal> is set to <literal>update</literal> for <literal>connectionsJpa</literal>:
+<programlisting>
+"connectionsJpa": {
+ "default": {
+ ...
+ "databaseSchema": "update"
+ }
+}
+</programlisting>
+ </para>
+ <para>
+ For MongoDB do the same, but for <literal>connectionsMongo</literal>:
+<programlisting>
+"connectionsMongo": {
+ "default": {
+ ...
+ "databaseSchema": "update"
+ }
+}
+</programlisting>
+ </para>
+ <para>
+ When you start the server with this setting your database will automatically be migrated if the database
+ schema has changed in the new version.
+ </para>
+ </section>
+
+ <section>
+ <title>Migrate keycloak-server.json</title>
+ <para>
+ You should copy <literal>standalone/configuration/keycloak-server.json</literal> from the old version to
+ make sure any configuration changes you've done are added to the new installation. The version specific
+ section below will list any changes done to this file that you have to do when upgrading from one version
+ to another.
+ </para>
+ </section>
+
+ <section>
+ <title>Migrate providers</title>
+ <para>
+ If you have implemented any SPI providers you need to copy them to the new server. The version
+ specific section below will mention if any of the SPI's have changed. If they have you may have to update
+ your code accordingly.
+ </para>
+ </section>
+
+ <section>
+ <title>Migrate themes</title>
+ <para>
+ If you have created a custom theme you need to copy them to the new server. The version specific section below
+ will mention if changes have been made to themes. If there is you may have to update your themes accordingly.
+ </para>
+ </section>
+
+ <section>
+ <title>Migrate application</title>
+ <para>
+ If you deploy applications directly to the Keycloak server you should copy them to the new server. For any
+ applications including those not deployed directly to the Keycloak server you should upgrade the adapter.
+ The version specific section below will mention if any changes are required to applications.
+ </para>
+ </section>
+
+ <section>
+ <title>Version specific migration</title>
+ <section>
+ <title>Migrating from 1.0.x.Final to 1.1.Beta1</title>
+ <itemizedlist>
+ <listitem>RealmModel JPA and Mongo storage schema has changed</listitem>
+ <listitem>UserSessionModel JPA and Mongo storage schema has changed as these interfaces have been refactored</listitem>
+ <listitem>
+ Upgrade your adapters, old adapters are not compatible with Keycloak 1.1. We interpreted JSON Web Token and OIDC ID Token specification incorrectly. 'aud'
+ claim must be the client id, we were storing the realm name in there and validating it.
+ </listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 RC-1 to RC-2</title>
+ <itemizedlist>
+ <listitem>A lot of info level logging has been changed to debug. Also, a realm no longer has the jboss-logging audit listener by default.
+ If you want log output when users login, logout, change passwords, etc. enable the jboss-logging audit listener through the admin console.</listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 Beta 4 to RC-1</title>
+ <itemizedlist>
+ <listitem>
+ logout REST API has been refactored. The GET request on the logout URI does not take a session_state
+ parameter anymore. You must be logged in in order to log out the session.
+ You can also POST to the logout REST URI. This action requires a valid refresh token to perform the logout.
+ The signature is the same as refresh token minus the grant type form parameter. See documentation for details.
+ </listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 Beta 1 to Beta 4</title>
+ <itemizedlist>
+ <listitem>
+ LDAP/AD configuration is changed. It is no longer under the "Settings" page. It is now under
+ Users->Federation. Add Provider will show you an "ldap" option.
+ </listitem>
+ <listitem>
+ Authentication SPI has been removed and rewritten. The new SPI is UserFederationProvider and is
+ more flexible.
+ </listitem>
+ <listitem>
+ <literal>ssl-not-required</literal> property in adapter config has been removed. Replaced with
+ <literal>ssl-required</literal>, valid values are <literal>all</literal> (require SSL for all requests), <literal>external</literal>
+ (require SSL only for external request) and <literal>none</literal> (SSL not required).
+ </listitem>
+ <listitem>
+ DB Schema has changed again.
+ </listitem>
+ <listitem>
+ Created applications now have a full scope by default. This means that you don't have to configure
+ the scope of an application if you don't want to.
+ </listitem>
+ <listitem>
+ Format of JSON file for importing realm data was changed. Now role mappings is available under the JSON record of particular
+ user.
+ </listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 Alpha 4 to Beta 1</title>
+ <itemizedlist>
+ <listitem>
+ DB Schema has changed. We have added export of the database to Beta 1, but not the ability to import
+ the database from older versions. This will be supported in future releases.
+ </listitem>
+ <listitem>
+ For all clients except bearer-only applications, you must specify at least one redirect uri. Keycloak
+ will not allow you to log in unless you have specified a valid redirect uri for that application.
+ </listitem>
+ <listitem>
+ Resource Owner Password Credentials flow is now disabled by default. It can be enabled by setting the toggle
+ for <literal>Direct Grant API</literal> <literal>ON</literal> under realm config in the admin console.
+ </listitem>
+ <listitem>
+ Configuration is now done through <literal>standalone/configuration/keycloak-server.json</literal>. This
+ should mainly affect those that use MongoDB.
+ </listitem>
+ <listitem>
+ JavaScript adapter has been refactored. See the <link linkend='javascript-adapter'>JavaScript adapter</link> section for more details.
+ </listitem>
+ <listitem>
+ The "Central Login Lifespan" setting no longer exists. Please see the <link linkend='session-timeouts'>Session Timeout</link> section
+ for me details.
+ </listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 Alpha 2 to Alpha 3</title>
+ <itemizedlist>
+ <listitem>
+ SkeletonKeyToken, SkeletonKeyScope, SkeletonKeyPrincipal, and SkeletonKeySession have been renamed to:
+ AccessToken, AccessScope, KeycloakPrincipal, and KeycloakAuthenticatedSession respectively.
+ </listitem>
+ <listitem>
+ ServleOAuthClient.getBearerToken() method signature has changed. It now returns an AccessTokenResponse
+ so that you can obtain a refresh token too.
+ </listitem>
+ <listitem>
+ Adapters now check the access token expiration with every request. If the token is expired, they will
+ attempt to invoke a refresh on the auth server using a saved refresh token.
+ </listitem>
+ <listitem>
+ Subject in AccessToken has been changed to the User ID.
+ </listitem>
+ </itemizedlist>
+ </section>
+ <section>
+ <title>Migrating from 1.0 Alpha 1 to Alpha 2</title>
+ <itemizedlist>
+ <listitem>
+ DB Schema has changed. We don't have any data migration utilities yet as of Alpha 2.
+ </listitem>
+ <listitem>
+ JBoss and Wildfly adapters are now installed via a JBoss/Wildfly subsystem. Please review the adapter
+ installation documentation. Edits to standalone.xml are now required.
+ </listitem>
+ <listitem>
+ There is a new credential type "secret". Unlike other credential types, it is stored in plain text in
+ the database and can be viewed in the admin console.
+
+ </listitem>
+ <listitem>
+ There is no longer required Application or OAuth Client credentials. These client types are now
+ hard coded to use the "secret" credential type.
+ </listitem>
+ <listitem>
+ Because of the "secret" credential change to Application and OAuth Client, you'll have to update
+ your keycloak.json configuration files and regenarate a secret within the Application or OAuth Client
+ credentials tab in the administration console.
+ </listitem>
+ </itemizedlist>
+ </section>
+ </section>
</chapter>
\ No newline at end of file
diff --git a/docbook/reference/en/en-US/modules/multi-tenancy.xml b/docbook/reference/en/en-US/modules/multi-tenancy.xml
new file mode 100644
index 0000000..410621f
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/multi-tenancy.xml
@@ -0,0 +1,56 @@
+<section id="multi_tenancy">
+ <title>Multi Tenancy</title>
+ <para>
+ Multi Tenancy, in our context, means that one single target application (WAR) can be secured by a single (or clustered) Keycloak server, authenticating
+ its users against different realms. In practice, this means that one application needs to use different <literal>keycloak.json</literal> files.
+ For this case, there are two possible solutions:
+ <itemizedlist>
+
+ <listitem>
+ The same WAR file deployed under two different names, each with its own Keycloak configuration (probably via the Keycloak Subsystem).
+ This scenario is suitable when the number of realms is known in advance or when there's a dynamic provision of application instances.
+ One example would be a service provider that dinamically creates servers/deployments for their clients, like a PaaS.
+ </listitem>
+
+ <listitem>
+ A WAR file deployed once (possibly in a cluster), that decides which realm to authenticate against based on the request parameters.
+ This scenario is suitable when there are an undefined number of realms. One example would be a SaaS provider that have only one deployment
+ (perhaps in a cluster) serving several companies, differentiating between clients based on the hostname
+ (<literal>client1.acme.com</literal>, <literal>client2.acme.com</literal>) or path (<literal>/app/client1/</literal>,
+ <literal>/app/client2/</literal>) or even via a special HTTP Header.
+ </listitem>
+
+ </itemizedlist>
+
+ This chapter of the reference guide focus on this second scenario.
+ </para>
+
+ <para>
+ Keycloak provides an extension point for applications that need to evaluate the realm on a request basis. During the authentication
+ and authorization phase of the incoming request, Keycloak queries the application via this extension point and expects the application
+ to return a complete representation of the realm. With this, Keycloak then proceeds the authentication and authorization process,
+ accepting or refusing the request based on the incoming credentials and on the returned realm.
+
+ For this scenario, an application needs to:
+
+ <itemizedlist>
+
+ <listitem>
+ Add a context parameter to the <literal>web.xml</literal>, named <literal>keycloak.config.resolver</literal>.
+ The value of this property should be the fully qualified name of the a class extending
+ <literal>org.keycloak.adapters.KeycloakConfigResolver</literal>.
+ </listitem>
+
+ <listitem>
+ A concrete implementation of <literal>org.keycloak.adapters.KeycloakConfigResolver</literal>. Keycloak will call the
+ <literal>resolve(org.keycloak.adapters.HttpFacade.Request)</literal> method and expects a complete
+ <literal>org.keycloak.adapters.KeycloakDeployment</literal> in response. Note that Keycloak will call this for every request,
+ so, take the usual performance precautions.
+ </listitem>
+
+ </itemizedlist>
+ </para>
+ <para>
+ An implementation of this feature can be found on the examples.
+ </para>
+</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 d41fb81..cd624b4 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -40,6 +40,8 @@ public enum EventType {
SEND_VERIFY_EMAIL_ERROR,
SEND_RESET_PASSWORD,
SEND_RESET_PASSWORD_ERROR,
+ RESET_PASSWORD,
+ RESET_PASSWORD_ERROR,
INVALID_SIGNATURE_ERROR,
REGISTER_NODE,
examples/cors/pom.xml 2(+1 -1)
diff --git a/examples/cors/pom.xml b/examples/cors/pom.xml
index 0317ffa..9b1c048 100755
--- a/examples/cors/pom.xml
+++ b/examples/cors/pom.xml
@@ -6,7 +6,7 @@
<version>1.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
- <name>Examples</name>
+ <name>Keycloak Examples - CORS</name>
<description/>
<modelVersion>4.0.0</modelVersion>
examples/multi-tenant/pom.xml 65(+65 -0)
diff --git a/examples/multi-tenant/pom.xml b/examples/multi-tenant/pom.xml
new file mode 100644
index 0000000..653632c
--- /dev/null
+++ b/examples/multi-tenant/pom.xml
@@ -0,0 +1,65 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.1.0-Alpha1-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+
+ <name>Keycloak Examples - Multi Tenant</name>
+ <artifactId>multitenant</artifactId>
+ <packaging>war</packaging>
+
+ <description>
+ Keycloak Multi Tenants Example
+ </description>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.wildfly.bom</groupId>
+ <artifactId>jboss-javaee-7.0-with-all</artifactId>
+ <version>8.0.0.Final</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-adapter-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.1_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Contains KeycloakDeployment and KeycloakConfigResolver -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-adapter-core</artifactId>
+ </dependency>
+
+ <!-- Contains KeycloakPrincipal -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <finalName>${project.artifactId}</finalName>
+ </build>
+</project>
+
examples/multi-tenant/README.md 32(+32 -0)
diff --git a/examples/multi-tenant/README.md b/examples/multi-tenant/README.md
new file mode 100644
index 0000000..8dd3c03
--- /dev/null
+++ b/examples/multi-tenant/README.md
@@ -0,0 +1,32 @@
+Keycloak Example - Multi Tenancy
+=======================================
+
+The following example was tested on Wildfly 8.1.0.Final and JBoss EAP 6.3. It should be compatible with any JBoss AS, JBoss EAP or Wildfly that supports Java EE 7.
+
+This example demonstrates the simplest possible scenario for Keycloak Multi Tenancy support. Multi Tenancy is understood on this context as a single application (WAR) that is deployed on a single or clustered application server, authenticating users from *different realms* against a single or clustered Keycloak server.
+
+The multi tenancy is achieved by having one realm per tenant on the server side and a per-request decision on which realm to authenticate the request against.
+
+This example contains only the minimal bits required for a multi tenant application.
+
+This example is composed of the following parts:
+
+- ProtectedServlet - A servlet that displays the username and realm from the current user
+- PathBasedKeycloakConfigResolver - A configuration resolver that takes the realm based on the path: /simple-multitenant/tenant2 means that the realm is "tenant2".
+
+Step 1: Setup a basic Keycloak server
+--------------------------------------------------------------
+Install Keycloak server and start it on port 8080. Check the Reference Guide if unsure on how to do it.
+
+Once the Keycloak server is up and running, import the two realms from "src/main/resources/", namely:
+
+- tenant1-realm.json
+- tenant2-realm.json
+
+Step 2: Deploy and run the example
+--------------------------------------------------------------
+
+- Build and deploy this sample's WAR file. For this example, deploy on the same server that is running the Keycloak Server, although this is not required for real world scenarios.
+- Access [http://localhost:8080/multitenant/tenant1](http://localhost:8080/multitenant/tenant1) and login as ``user-tenant1``, password ``user-tenant1``
+- Access [http://localhost:8080/multitenant/tenant2](http://localhost:8080/multitenant/tenant2) and login as ``user-tenant2``, password ``user-tenant2``
+
diff --git a/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/boundary/ProtectedServlet.java b/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/boundary/ProtectedServlet.java
new file mode 100644
index 0000000..991169d
--- /dev/null
+++ b/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/boundary/ProtectedServlet.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2014 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.example.multitenant.boundary;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.keycloak.KeycloakPrincipal;
+
+/**
+ *
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+@WebServlet(urlPatterns = "/*")
+public class ProtectedServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ String realm = req.getPathInfo().split("/")[1];
+ if (realm.contains("?")) {
+ realm = realm.split("\\?")[0];
+ }
+
+ if (req.getPathInfo().contains("logout")) {
+ req.logout();
+ resp.sendRedirect(req.getContextPath() + "/" + realm);
+ return;
+ }
+
+ KeycloakPrincipal principal = (KeycloakPrincipal) req.getUserPrincipal();
+
+ resp.setContentType("text/html");
+ PrintWriter writer = resp.getWriter();
+
+ writer.write("Realm: ");
+ writer.write(principal.getKeycloakSecurityContext().getIdToken().getIssuer());
+
+ writer.write("<br/>User: ");
+ writer.write(principal.getKeycloakSecurityContext().getIdToken().getPreferredUsername());
+
+ writer.write(String.format("<br/><a href=\"/multitenant/%s/logout\">Logout</a>", realm));
+ }
+ }
diff --git a/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/control/PathBasedKeycloakConfigResolver.java b/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/control/PathBasedKeycloakConfigResolver.java
new file mode 100644
index 0000000..4aa2fea
--- /dev/null
+++ b/examples/multi-tenant/src/main/java/org/keycloak/example/multitenant/control/PathBasedKeycloakConfigResolver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 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.example.multitenant.control;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakConfigResolver;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+
+/**
+ *
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
+
+ private final Map<String, KeycloakDeployment> cache = new ConcurrentHashMap<String, KeycloakDeployment>();
+
+ @Override
+ public KeycloakDeployment resolve(HttpFacade.Request request) {
+ String path = request.getURI();
+ String realm = path.substring(path.indexOf("multitenant/")).split("/")[1];
+ if (realm.contains("?")) {
+ realm = realm.split("\\?")[0];
+ }
+
+ KeycloakDeployment deployment = cache.get(realm);
+ if (null == deployment) {
+ // not found on the simple cache, try to load it from the file system
+ InputStream is = getClass().getResourceAsStream("/" + realm + "-keycloak.json");
+ deployment = KeycloakDeploymentBuilder.build(is);
+ cache.put(realm, deployment);
+ }
+
+ return deployment;
+ }
+
+}
diff --git a/examples/multi-tenant/src/main/resources/tenant1-keycloak.json b/examples/multi-tenant/src/main/resources/tenant1-keycloak.json
new file mode 100644
index 0000000..57be277
--- /dev/null
+++ b/examples/multi-tenant/src/main/resources/tenant1-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "tenant1",
+ "resource" : "multi-tenant",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/examples/multi-tenant/src/main/resources/tenant2-keycloak.json b/examples/multi-tenant/src/main/resources/tenant2-keycloak.json
new file mode 100644
index 0000000..4f221dc
--- /dev/null
+++ b/examples/multi-tenant/src/main/resources/tenant2-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "tenant2",
+ "resource" : "multi-tenant",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA0oJjgPQJhnVhOo51KauQGfLLreMFu64OJdKXRnfvAQJQTuKNwc5JrR63l/byyW1B6FgclABF818TtLvMCAkn4EuFwQZCZhg3x3+lFGiB/IzC6UAt4Bi0JQrTbdh83/U97GIPegvaDqiqEiQESEkbCZWxM6sh/34hQaAhCaFpMwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml b/examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml
new file mode 100644
index 0000000..1693f7a
--- /dev/null
+++ b/examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 Juraci Paixão Kröhling <juraci at kroehling.de>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program 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 General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<jboss:jboss
+ xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
+ xmlns:s="urn:security:1.1"
+ version="3.1" impl-version="2.0">
+
+ <assembly-descriptor>
+ <s:security>
+ <ejb-name>*</ejb-name>
+ <s:security-domain>keycloak</s:security-domain>
+ </s:security>
+ </assembly-descriptor>
+</jboss:jboss>
\ No newline at end of file
diff --git a/examples/multi-tenant/src/main/webapp/WEB-INF/web.xml b/examples/multi-tenant/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..adfc73f
--- /dev/null
+++ b/examples/multi-tenant/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+ <display-name>Multi Tenant Example</display-name>
+
+ <context-param>
+ <param-name>keycloak.config.resolver</param-name>
+ <param-value>org.keycloak.example.multitenant.control.PathBasedKeycloakConfigResolver</param-value>
+ </context-param>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>REST endpoints</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>*</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <login-config>
+ <auth-method>KEYCLOAK</auth-method>
+ <realm-name>not-important</realm-name>
+ </login-config>
+ <security-role>
+ <role-name>user</role-name>
+ </security-role>
+</web-app>
\ No newline at end of file
examples/multi-tenant/tenant1-realm.json 57(+57 -0)
diff --git a/examples/multi-tenant/tenant1-realm.json b/examples/multi-tenant/tenant1-realm.json
new file mode 100644
index 0000000..76acce8
--- /dev/null
+++ b/examples/multi-tenant/tenant1-realm.json
@@ -0,0 +1,57 @@
+{
+ "id": "tenant1",
+ "realm": "tenant1",
+ "enabled": true,
+ "accessTokenLifespan": 3000,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "social": false,
+ "passwordCredentialGrantAllowed": true,
+ "updateProfileOnInitialSocialLogin": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "user-tenant1",
+ "enabled": true,
+ "credentials" : [
+ { "type" : "password",
+ "value" : "user-tenant1" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ }
+ ]
+ },
+ "scopeMappings": [
+ {
+ "client": "multi-tenant",
+ "roles": ["user"]
+ }
+
+ ],
+ "applications": [
+ {
+ "name": "multi-tenant",
+ "enabled": true,
+ "adminUrl": "http://localhost:8080/multitenant/tenant1",
+ "baseUrl": "http://localhost:8080/multitenant/tenant1",
+ "redirectUris": [
+ "http://localhost:8080/multitenant/tenant1/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
examples/multi-tenant/tenant2-realm.json 57(+57 -0)
diff --git a/examples/multi-tenant/tenant2-realm.json b/examples/multi-tenant/tenant2-realm.json
new file mode 100644
index 0000000..295cb3f
--- /dev/null
+++ b/examples/multi-tenant/tenant2-realm.json
@@ -0,0 +1,57 @@
+{
+ "id": "tenant2",
+ "realm": "tenant2",
+ "enabled": true,
+ "accessTokenLifespan": 3000,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "social": false,
+ "passwordCredentialGrantAllowed": true,
+ "updateProfileOnInitialSocialLogin": false,
+ "privateKey": "MIICXQIBAAKBgQDA0oJjgPQJhnVhOo51KauQGfLLreMFu64OJdKXRnfvAQJQTuKNwc5JrR63l/byyW1B6FgclABF818TtLvMCAkn4EuFwQZCZhg3x3+lFGiB/IzC6UAt4Bi0JQrTbdh83/U97GIPegvaDqiqEiQESEkbCZWxM6sh/34hQaAhCaFpMwIDAQABAoGADwFSvEOQuh0IjWRtKZjwjOo4BrmlbRDJ3rf6x2LoemTttSouXzGxx/H87fSZdxNNuU9HbBHoY4ko4POzmZEWhS0gV6UjM7VArc4YjID6Hh2tfU9vCbuuKZrRs7RjxL70b51WxycKc49PQ4JiR3g04punrpq2UzToPrm66zI+ICECQQD2Jauo6cXXoxHR0QychQf4dityZwFXUoR/8oI/YFiu9XwcWgSMwrFKUdWWNKYmrIRNqCBzrGyeiGdaAjsw41T3AkEAyIpn+XL7bek/uLno5/7ULauf2dFI6MEaHJixQJD7S6Tfo/CGuDK93H4K0GAdjgR0LA0tCnB09yyPCd5NmAYKpQJBAO7+BH4s/PsyScr+vs/6GpMTqXuap6KxbBUO0YfXdEPr9mVQwboqDxmp+0esNua1+n+sDlZBw/TpW+/42p/NGmECQF0sOQyjyH+TfGCmN7j6I7ioYZeA7h/9/9TDeK8n7SmDC8kOanlQUfgMs5eG4JRoK1WANaoA/8cLc9XA7EoynGUCQQDx/Gjg6qyWheVujxjKufH1XkqDNiQHClDRM1ntChCmGq/RmpVmce+mYeOYZ9eofv7UJUCBdamllRlB+056Ld2h",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA0oJjgPQJhnVhOo51KauQGfLLreMFu64OJdKXRnfvAQJQTuKNwc5JrR63l/byyW1B6FgclABF818TtLvMCAkn4EuFwQZCZhg3x3+lFGiB/IzC6UAt4Bi0JQrTbdh83/U97GIPegvaDqiqEiQESEkbCZWxM6sh/34hQaAhCaFpMwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "user-tenant2",
+ "enabled": true,
+ "credentials" : [
+ { "type" : "password",
+ "value" : "user-tenant2" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ }
+ ]
+ },
+ "scopeMappings": [
+ {
+ "client": "multi-tenant",
+ "roles": ["user"]
+ }
+
+ ],
+ "applications": [
+ {
+ "name": "multi-tenant",
+ "enabled": true,
+ "adminUrl": "http://localhost:8080/multitenant/tenant2",
+ "baseUrl": "http://localhost:8080/multitenant/tenant2",
+ "redirectUris": [
+ "http://localhost:8080/multitenant/tenant2/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
examples/pom.xml 1(+1 -0)
diff --git a/examples/pom.xml b/examples/pom.xml
index ea6fac8..7ec8a31 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -29,5 +29,6 @@
<module>demo-template</module>
<module>providers</module>
<module>js-console</module>
+ <module>multi-tenant</module>
</modules>
</project>
examples/README.md 6(+6 -0)
diff --git a/examples/README.md b/examples/README.md
index 74970e9..cc42354 100755
--- a/examples/README.md
+++ b/examples/README.md
@@ -46,3 +46,9 @@ Themes
------
Example themes to change the look and feel of login forms, account management console and admin console. For more information look at `themes/README.md`.
+
+
+Multi tenancy
+-------------
+
+A complete application, showing how to achieve multi tenancy of web applications by using one realm per account. For more information look at `multi-tenant/README.md`
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
index 8597d84..e6378ce 100755
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java
@@ -211,6 +211,12 @@ public class LDAPFederationProvider implements UserFederationProvider {
return null;
}
+ // KEYCLOAK-808: Should we allow case-sensitivity to be configurable?
+ if (!username.equals(picketlinkUser.getLoginName())) {
+ logger.warnf("User found in LDAP but with different username. LDAP username: %s, Searched username: %s", username, picketlinkUser.getLoginName());
+ return null;
+ }
+
return importUserFromPicketlink(realm, picketlinkUser);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
@@ -223,6 +229,11 @@ public class LDAPFederationProvider implements UserFederationProvider {
protected UserModel importUserFromPicketlink(RealmModel realm, User picketlinkUser) {
String email = (picketlinkUser.getEmail() != null && picketlinkUser.getEmail().trim().length() > 0) ? picketlinkUser.getEmail() : null;
+
+ if (picketlinkUser.getLoginName() == null) {
+ throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. ID of user from LDAP: " + picketlinkUser.getId());
+ }
+
UserModel imported = session.userStorage().addUser(realm, picketlinkUser.getLoginName());
imported.setEnabled(true);
imported.setEmail(email);
@@ -247,6 +258,13 @@ public class LDAPFederationProvider implements UserFederationProvider {
if (picketlinkUser == null) {
return null;
}
+
+ // KEYCLOAK-808: Should we allow case-sensitivity to be configurable?
+ if (!email.equals(picketlinkUser.getEmail())) {
+ logger.warnf("User found in LDAP but with different email. LDAP email: %s, Searched email: %s", email, picketlinkUser.getEmail());
+ return null;
+ }
+
return importUserFromPicketlink(realm, picketlinkUser);
} catch (IdentityManagementException ie) {
throw convertIDMException(ie);
diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
index 3e1eb51..4fd2de9 100755
--- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
+++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java
@@ -50,8 +50,6 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setClient(ClientModel client);
- LoginFormsProvider setVerifyCode(String code);
-
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams);
public LoginFormsProvider setFormData(MultivaluedMap<String, String> formData);
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 9a76e78..744d588 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
@@ -51,7 +51,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
- private String verifyCode;
private String message;
private String accessCode;
private Response.Status status = Response.Status.OK;
@@ -110,8 +109,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
case VERIFY_EMAIL:
try {
UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri());
- builder.queryParam("code", accessCode);
- builder.queryParam("key", verifyCode);
+ builder.queryParam("key", accessCode);
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@@ -312,12 +310,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
@Override
- public LoginFormsProvider setVerifyCode(String code) {
- this.verifyCode = code;
- return this;
- }
-
- @Override
public LoginFormsProvider setQueryParams(MultivaluedMap<String, String> queryParams) {
this.queryParams = queryParams;
return this;
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
index 9fff2b2..9c107ae 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java
@@ -27,27 +27,54 @@ import java.util.Map;
public class AdapterDeploymentContext {
private static final Logger log = Logger.getLogger(AdapterDeploymentContext.class);
protected KeycloakDeployment deployment;
+ protected KeycloakConfigResolver configResolver;
public AdapterDeploymentContext() {
}
+ /**
+ * For single-tenant deployments, this constructor is to be used, as a
+ * full KeycloakDeployment is known at deployment time and won't change
+ * during the application deployment's life cycle.
+ *
+ * @param deployment A KeycloakConfigResolver, possibly missing the Auth
+ * Server URL and/or Realm Public Key
+ */
public AdapterDeploymentContext(KeycloakDeployment deployment) {
this.deployment = deployment;
}
- public KeycloakDeployment getDeployment() {
- return deployment;
+ /**
+ * For multi-tenant deployments, this constructor is to be used, as a
+ * KeycloakDeployment is not known at deployment time. It defers the
+ * resolution of a KeycloakDeployment to a KeycloakConfigResolver,
+ * to be implemented by the target application.
+ *
+ * @param configResolver A KeycloakConfigResolver that will be used
+ * to resolve a KeycloakDeployment
+ */
+ public AdapterDeploymentContext(KeycloakConfigResolver configResolver) {
+ this.configResolver = configResolver;
}
/**
- * Resolve adapter deployment based on partial adapter configuration.
- * This will resolve a relative auth server url based on the current request
- * This will lazily resolve the public key of the realm if it is not set already.
+ * For single-tenant deployments, it complements KeycloakDeployment
+ * by resolving a relative Auth Server's URL based on the current request
+ * and, if needed, will lazily resolve the Realm's Public Key.
+ *
+ * For multi-tenant deployments, defers the resolution of KeycloakDeployment
+ * to the KeycloakConfigResolver .
*
+ * @param facade the Request/Response Façade , used to either determine
+ * the Auth Server URL (single tenant) or pass thru to the
+ * KeycloakConfigResolver.
* @return
*/
public KeycloakDeployment resolveDeployment(HttpFacade facade) {
- KeycloakDeployment deployment = this.deployment;
+ if (null != configResolver) {
+ return configResolver.resolve(facade.getRequest());
+ }
+
if (deployment == null) return null;
if (deployment.getAuthServerBaseUrl() == null) return deployment;
@@ -411,6 +438,9 @@ public class AdapterDeploymentContext {
}
public void updateDeployment(AdapterConfig config) {
+ if (null != configResolver) {
+ throw new IllegalStateException("Cannot parse an adapter config and build an updated deployment when on a multi-tenant scenario.");
+ }
deployment = KeycloakDeploymentBuilder.build(config);
}
}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java
new file mode 100644
index 0000000..8ba4143
--- /dev/null
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakConfigResolver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014 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.adapters;
+
+import org.keycloak.adapters.HttpFacade.Request;
+
+/**
+ * On multi-tenant scenarios, Keycloak will defer the resolution of a
+ * KeycloakDeployment to the target application at the request-phase.
+ *
+ * A Request object is passed to the resolver and callers expect a complete
+ * KeycloakDeployment. Based on this KeycloakDeployment, Keycloak will resume
+ * authenticating and authorizing the request.
+ *
+ * The easiest way to build a KeycloakDeployment is to use
+ * KeycloakDeploymentBuilder , passing the InputStream of an existing
+ * keycloak.json to the build() method.
+ *
+ * @see KeycloakDeploymentBuilder
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+public interface KeycloakConfigResolver {
+
+ public KeycloakDeployment resolve(Request facade);
+
+}
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
index aea33dd..099697f 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java
@@ -67,7 +67,7 @@ public class PreAuthActionsHandler {
public boolean preflightCors() {
// don't need to resolve deployment on cors requests. Just need to know local cors config.
- KeycloakDeployment deployment = deploymentContext.getDeployment();
+ KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (!deployment.isCors()) return false;
log.debugv("checkCorsPreflight {0}", facade.getRequest().getURI());
if (!facade.getRequest().getMethod().equalsIgnoreCase("OPTIONS")) {
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
index 24ac814..8c0dc91 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
@@ -83,6 +83,11 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
if (this.deployment == null || refreshToken == null) return false; // Might be serialized in HttpSession?
+ if (!this.getRealm().equals(this.deployment.getRealm())) {
+ // this should not happen, but let's check it anyway
+ return false;
+ }
+
if (log.isTraceEnabled()) {
log.trace("Doing refresh");
}
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java
index 406b786..359e84b 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaCookieTokenStore.java
@@ -49,6 +49,12 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
if (authenticatedPrincipal != null) {
log.debug("remote logged in already. Establish state from cookie");
RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext();
+
+ if (!securityContext.getRealm().equals(deployment.getRealm())) {
+ log.debug("Account from cookie is from a different realm than for the request.");
+ return false;
+ }
+
securityContext.setCurrentRequestInfo(deployment, this);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext);
@@ -92,6 +98,7 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
}
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
+
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal;
boolean success = session.refreshExpiredToken(false);
if (success && session.isActive()) return principal;
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java
index 0146883..41266cf 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaSessionTokenStore.java
@@ -37,8 +37,10 @@ public class CatalinaSessionTokenStore implements AdapterTokenStore {
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return;
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
if (session == null) return;
+
// just in case session got serialized
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
+
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
@@ -62,16 +64,23 @@ public class CatalinaSessionTokenStore implements AdapterTokenStore {
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
return false;
log.debug("remote logged in already. Establish state from session");
- GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
- request.setUserPrincipal(principal);
- request.setAuthType("KEYCLOAK");
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
if (securityContext != null) {
+
+ if (!deployment.getRealm().equals(securityContext.getRealm())) {
+ log.debug("Account from cookie is from a different realm than for the request.");
+ return false;
+ }
+
securityContext.setCurrentRequestInfo(deployment, this);
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
}
+ GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
+ request.setUserPrincipal(principal);
+ request.setAuthType("KEYCLOAK");
+
((CatalinaRequestAuthenticator)authenticator).restoreRequest();
return true;
}
diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
index c7d51d0..7b04d7c 100755
--- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
+++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/KeycloakAuthenticatorValve.java
@@ -6,27 +6,23 @@ import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Manager;
-import org.apache.catalina.Session;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.jboss.logging.Logger;
-import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
-import org.keycloak.adapters.CookieTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler;
-import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.enums.TokenStore;
import javax.servlet.ServletContext;
@@ -37,6 +33,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import org.keycloak.adapters.KeycloakConfigResolver;
/**
* Web deployment whose security is managed by a remote OAuth Skeleton Key authentication server
@@ -69,7 +66,6 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
public void logout(Request request) throws ServletException {
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
- request.removeAttribute(KeycloakSecurityContext.class.getName());
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (ksc instanceof RefreshableKeycloakSecurityContext) {
@@ -78,6 +74,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
+ request.removeAttribute(KeycloakSecurityContext.class.getName());
}
super.logout(request);
}
@@ -120,16 +117,41 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
}
+ @SuppressWarnings("UseSpecificCatch")
protected void init() {
- InputStream configInputStream = getConfigInputStream(context);
- KeycloakDeployment kd = null;
- if (configInputStream == null) {
- log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
- kd = new KeycloakDeployment();
+ // Possible scenarios:
+ // 1) The deployment has a keycloak.config.resolver specified and it exists:
+ // Outcome: adapter uses the resolver
+ // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exists, isn't a resolver, ...) :
+ // Outcome: adapter is left unconfigured
+ // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent)
+ // Outcome: adapter uses it
+ // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent)
+ // Outcome: adapter is left unconfigured
+
+ String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver");
+ if (configResolverClass != null) {
+ try {
+ KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
+ deploymentContext = new AdapterDeploymentContext(configResolver);
+ log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis.");
+ } catch (Exception ex) {
+ log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage());
+ deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
+ }
} else {
- kd = KeycloakDeploymentBuilder.build(configInputStream);
+ InputStream configInputStream = getConfigInputStream(context);
+ KeycloakDeployment kd;
+ if (configInputStream == null) {
+ log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
+ kd = new KeycloakDeployment();
+ } else {
+ kd = KeycloakDeploymentBuilder.build(configInputStream);
+ }
+ deploymentContext = new AdapterDeploymentContext(kd);
+ log.debug("Keycloak is using a per-deployment configuration.");
}
- deploymentContext = new AdapterDeploymentContext(kd);
+
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer(), getController());
setNext(actions);
diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java
index 8ce88dc..c0c2274 100755
--- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java
+++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaCookieTokenStore.java
@@ -47,6 +47,12 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
if (authenticatedPrincipal != null) {
log.fine("remote logged in already. Establish state from cookie");
RefreshableKeycloakSecurityContext securityContext = authenticatedPrincipal.getKeycloakSecurityContext();
+
+ if (!securityContext.getRealm().equals(deployment.getRealm())) {
+ log.fine("Account from cookie is from a different realm than for the request.");
+ return false;
+ }
+
securityContext.setCurrentRequestInfo(deployment, this);
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), authenticatedPrincipal, roles, securityContext);
@@ -89,6 +95,7 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
}
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
+
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return principal;
boolean success = session.refreshExpiredToken(false);
if (success && session.isActive()) return principal;
diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java
index 99ef859..500adf1 100755
--- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java
+++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/CatalinaSessionTokenStore.java
@@ -35,8 +35,10 @@ public class CatalinaSessionTokenStore implements AdapterTokenStore {
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null) return;
RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
if (session == null) return;
+
// just in case session got serialized
if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
+
if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
// FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will
@@ -60,16 +62,23 @@ public class CatalinaSessionTokenStore implements AdapterTokenStore {
if (request.getSessionInternal(false) == null || request.getSessionInternal().getPrincipal() == null)
return false;
log.fine("remote logged in already. Establish state from session");
- GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
- request.setUserPrincipal(principal);
- request.setAuthType("KEYCLOAK");
RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) request.getSessionInternal().getNote(KeycloakSecurityContext.class.getName());
if (securityContext != null) {
+
+ if (!deployment.getRealm().equals(securityContext.getRealm())) {
+ log.fine("Account from cookie is from a different realm than for the request.");
+ return false;
+ }
+
securityContext.setCurrentRequestInfo(deployment, this);
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
}
+ GenericPrincipal principal = (GenericPrincipal) request.getSessionInternal().getPrincipal();
+ request.setUserPrincipal(principal);
+ request.setAuthType("KEYCLOAK");
+
((CatalinaRequestAuthenticator)authenticator).restoreRequest();
return true;
}
diff --git a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
index 408c4ce..80018f7 100755
--- a/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
+++ b/integration/tomcat7/adapter/src/main/java/org/keycloak/adapters/tomcat7/KeycloakAuthenticatorValve.java
@@ -6,7 +6,6 @@ import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Manager;
-import org.apache.catalina.Session;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
@@ -23,8 +22,6 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler;
-import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
-import org.keycloak.adapters.ServerRequest;
import org.keycloak.enums.TokenStore;
import javax.servlet.ServletContext;
@@ -35,7 +32,9 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.util.logging.Level;
import java.util.logging.Logger;
+import org.keycloak.adapters.KeycloakConfigResolver;
/**
* Web deployment whose security is managed by a remote OAuth Skeleton Key authentication server
@@ -74,7 +73,6 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
public void logout(Request request) throws ServletException {
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
- request.removeAttribute(KeycloakSecurityContext.class.getName());
CatalinaHttpFacade facade = new CatalinaHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (ksc instanceof RefreshableKeycloakSecurityContext) {
@@ -83,6 +81,7 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
+ request.removeAttribute(KeycloakSecurityContext.class.getName());
}
super.logout(request);
}
@@ -94,16 +93,42 @@ public class KeycloakAuthenticatorValve extends FormAuthenticator implements Lif
cache = false;
}
+ @SuppressWarnings("UseSpecificCatch")
+ @Override
public void initInternal() {
- InputStream configInputStream = getConfigInputStream(context);
- KeycloakDeployment kd = null;
- if (configInputStream == null) {
- log.warning("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
- kd = new KeycloakDeployment();
+ // Possible scenarios:
+ // 1) The deployment has a keycloak.config.resolver specified and it exists:
+ // Outcome: adapter uses the resolver
+ // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exists, isn't a resolver, ...) :
+ // Outcome: adapter is left unconfigured
+ // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent)
+ // Outcome: adapter uses it
+ // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent)
+ // Outcome: adapter is left unconfigured
+
+ String configResolverClass = context.getServletContext().getInitParameter("keycloak.config.resolver");
+ if (configResolverClass != null) {
+ try {
+ KeycloakConfigResolver configResolver = (KeycloakConfigResolver) context.getLoader().getClassLoader().loadClass(configResolverClass).newInstance();
+ deploymentContext = new AdapterDeploymentContext(configResolver);
+ log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
+ } catch (Exception ex) {
+ log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
+ deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
+ }
} else {
- kd = KeycloakDeploymentBuilder.build(configInputStream);
+ InputStream configInputStream = getConfigInputStream(context);
+ KeycloakDeployment kd;
+ if (configInputStream == null) {
+ log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
+ kd = new KeycloakDeployment();
+ } else {
+ kd = KeycloakDeploymentBuilder.build(configInputStream);
+ }
+ deploymentContext = new AdapterDeploymentContext(kd);
+ log.fine("Keycloak is using a per-deployment configuration.");
}
- deploymentContext = new AdapterDeploymentContext(kd);
+
context.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer());
setNext(actions);
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
index 5c7a16b..9484021 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java
@@ -44,6 +44,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.Map;
+import org.keycloak.adapters.KeycloakConfigResolver;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -94,22 +95,49 @@ public class KeycloakServletExtension implements ServletExtension {
@Override
+ @SuppressWarnings("UseSpecificCatch")
public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) {
if (!isAuthenticationMechanismPresent(deploymentInfo, "KEYCLOAK")) {
log.debug("auth-method is not keycloak!");
return;
}
log.debug("KeycloakServletException initialization");
- InputStream is = getConfigInputStream(servletContext);
- final KeycloakDeployment deployment;
- if (is == null) {
- log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
- deployment = new KeycloakDeployment();
- } else {
- deployment = KeycloakDeploymentBuilder.build(is);
+ // Possible scenarios:
+ // 1) The deployment has a keycloak.config.resolver specified and it exists:
+ // Outcome: adapter uses the resolver
+ // 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exists, isn't a resolver, ...) :
+ // Outcome: adapter is left unconfigured
+ // 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent)
+ // Outcome: adapter uses it
+ // 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent)
+ // Outcome: adapter is left unconfigured
+
+ KeycloakConfigResolver configResolver;
+ String configResolverClass = servletContext.getInitParameter("keycloak.config.resolver");
+ AdapterDeploymentContext deploymentContext;
+ if (configResolverClass != null) {
+ try {
+ configResolver = (KeycloakConfigResolver) deploymentInfo.getClassLoader().loadClass(configResolverClass).newInstance();
+ deploymentContext = new AdapterDeploymentContext(configResolver);
+ log.info("Using " + configResolverClass + " to resolve Keycloak configuration on a per-request basis.");
+ } catch (Exception ex) {
+ log.warn("The specified resolver " + configResolverClass + " could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: " + ex.getMessage());
+ deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
+ }
+ } else {
+ InputStream is = getConfigInputStream(servletContext);
+ final KeycloakDeployment deployment;
+ if (is == null) {
+ log.warn("No adapter configuration. Keycloak is unconfigured and will deny all requests.");
+ deployment = new KeycloakDeployment();
+ } else {
+ deployment = KeycloakDeploymentBuilder.build(is);
+ }
+ deploymentContext = new AdapterDeploymentContext(deployment);
+ log.debug("Keycloak is using a per-deployment configuration.");
}
- AdapterDeploymentContext deploymentContext = new AdapterDeploymentContext(deployment);
+
servletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
final NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
index fe0c6c9..3dccf8c 100644
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
@@ -53,6 +53,12 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
log.debug("Account was not in session, returning null");
return false;
}
+
+ if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) {
+ log.debug("Account in session belongs to a different realm than for this request.");
+ return false;
+ }
+
account.setCurrentRequestInfo(deployment, this);
if (account.checkActive()) {
log.debug("Cached account found");
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
index 4946ea5..65b6ab2 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
@@ -45,6 +45,11 @@ public class UndertowCookieTokenStore implements AdapterTokenStore {
}
KeycloakUndertowAccount account = new KeycloakUndertowAccount(principal);
+ if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) {
+ log.debug("Account in session belongs to a different realm than for this request.");
+ return false;
+ }
+
if (account.checkActive()) {
log.debug("Cached account found");
securityContext.authenticationComplete(account, "KEYCLOAK", false);
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
index 6362e43..cc9e3d9 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
@@ -50,6 +50,12 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
log.debug("Account was not in session, returning null");
return false;
}
+
+ if (!deployment.getRealm().equals(account.getKeycloakSecurityContext().getRealm())) {
+ log.debug("Account in session belongs to a different realm than for this request.");
+ return false;
+ }
+
account.setCurrentRequestInfo(deployment, this);
if (account.checkActive()) {
log.debug("Cached account found");
diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
index 2c0f9dd..bdbc5c4 100755
--- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java
@@ -47,6 +47,7 @@ public interface ClientSessionModel {
UPDATE_PROFILE,
CONFIGURE_TOTP,
UPDATE_PASSWORD,
+ RECOVER_PASSWORD,
AUTHENTICATE,
SOCIAL_CALLBACK
}
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 4371e26..ef38009 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
@@ -41,6 +41,7 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private String publicKeyPem;
private String privateKeyPem;
private String certificatePem;
+ private String codeSecret;
private String loginTheme;
private String accountTheme;
@@ -271,6 +272,14 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.privateKeyPem = privateKeyPem;
}
+ public String getCodeSecret() {
+ return codeSecret;
+ }
+
+ public void setCodeSecret(String codeSecret) {
+ this.codeSecret = codeSecret;
+ }
+
public String getLoginTheme() {
return loginTheme;
}
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 d1895ef..4601fcc 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmModel.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java
@@ -97,6 +97,10 @@ public interface RealmModel extends RoleContainerModel {
void setPublicKey(PublicKey publicKey);
+ String getCodeSecret();
+
+ void setCodeSecret(String codeSecret);
+
X509Certificate getCertificate();
void setCertificate(X509Certificate certificate);
String getCertificatePem();
diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
index f5a1b3a..09111b4 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java
@@ -119,6 +119,8 @@ public final class KeycloakModelUtils {
throw new RuntimeException(e);
}
realm.setCertificate(certificate);
+
+ realm.setCodeSecret(generateCodeSecret());
}
public static void generateRealmCertificate(RealmModel realm) {
@@ -161,6 +163,10 @@ public final class KeycloakModelUtils {
return secret;
}
+ public static String generateCodeSecret() {
+ return UUID.randomUUID().toString();
+ }
+
public static ApplicationModel createApplication(RealmModel realm, String name) {
ApplicationModel app = realm.addApplication(name);
generateSecret(app);
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 85331ac..7d431c3 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
@@ -91,6 +91,7 @@ public class ModelToRepresentation {
KeycloakModelUtils.generateRealmCertificate(realm);
}
rep.setCertificate(realm.getCertificatePem());
+ rep.setCodeSecret(realm.getCodeSecret());
rep.setPasswordCredentialGrantAllowed(realm.isPasswordCredentialGrantAllowed());
rep.setRegistrationAllowed(realm.isRegistrationAllowed());
rep.setRememberMe(realm.isRememberMe());
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 9610395..5ba8d06 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
@@ -62,7 +62,7 @@ public class RepresentationToModel {
else newRealm.setAccessTokenLifespan(300);
if (rep.getSsoSessionIdleTimeout() != null) newRealm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
- else newRealm.setSsoSessionIdleTimeout(600);
+ else newRealm.setSsoSessionIdleTimeout(1800);
if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
else newRealm.setSsoSessionMaxLifespan(36000);
@@ -92,6 +92,12 @@ public class RepresentationToModel {
} else {
newRealm.setCertificatePem(rep.getCertificate());
}
+ if (rep.getCodeSecret() == null) {
+ newRealm.setCodeSecret(KeycloakModelUtils.generateCodeSecret());
+ } else {
+ newRealm.setCodeSecret(rep.getCodeSecret());
+ }
+
if (rep.getLoginTheme() != null) newRealm.setLoginTheme(rep.getLoginTheme());
if (rep.getAccountTheme() != null) newRealm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) newRealm.setAdminTheme(rep.getAdminTheme());
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
index 67b1927..9bbbed9 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheRealmProviderFactory.java
@@ -21,7 +21,7 @@ public class InfinispanCacheRealmProviderFactory implements CacheRealmProviderFa
@Override
public CacheRealmProvider create(KeycloakSession session) {
- Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache("realms");
+ Cache<String, Object> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.REALM_CACHE_NAME);
RealmCache realmCache = new InfinispanRealmCache(cache, realmLookup);
return new DefaultCacheRealmProvider(realmCache, session);
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
index c0ce985..cb88815 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/InfinispanCacheUserProviderFactory.java
@@ -41,7 +41,7 @@ public class InfinispanCacheUserProviderFactory implements CacheUserProviderFact
synchronized (this) {
if (userCache == null) {
checkIspnVersion();
- Cache<String, CachedUser> cache = session.getProvider(InfinispanConnectionProvider.class).getCache("users");
+ Cache<String, CachedUser> cache = session.getProvider(InfinispanConnectionProvider.class).getCache(InfinispanConnectionProvider.USER_CACHE_NAME);
cache.addListener(new CacheListener());
userCache = new InfinispanUserCache(cache, usernameLookup, emailLookup);
}
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 d96a9ea..76c9204 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
@@ -57,6 +57,7 @@ public class CachedRealm {
private String publicKeyPem;
private String privateKeyPem;
private String certificatePem;
+ private String codeSecret;
private String loginTheme;
private String accountTheme;
@@ -115,6 +116,7 @@ public class CachedRealm {
publicKeyPem = model.getPublicKeyPem();
privateKeyPem = model.getPrivateKeyPem();
certificatePem = model.getCertificatePem();
+ codeSecret = model.getCodeSecret();
loginTheme = model.getLoginTheme();
accountTheme = model.getAccountTheme();
@@ -267,6 +269,10 @@ public class CachedRealm {
return privateKeyPem;
}
+ public String getCodeSecret() {
+ return codeSecret;
+ }
+
public List<RequiredCredentialModel> getRequiredCredentials() {
return requiredCredentials;
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
index d471b4a..eaf9fca 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java
@@ -374,7 +374,16 @@ public class RealmAdapter implements RealmModel {
setPrivateKeyPem(privateKeyPem);
}
+ @Override
+ public String getCodeSecret() {
+ return updated != null ? updated.getCodeSecret() : cached.getCodeSecret();
+ }
+ @Override
+ public void setCodeSecret(String codeSecret) {
+ getDelegateForUpdate();
+ updated.setCodeSecret(codeSecret);
+ }
@Override
public List<RequiredCredentialModel> getRequiredCredentials() {
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 bcb5ad9..c78e916 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
@@ -82,6 +82,8 @@ public class RealmEntity {
protected String privateKeyPem;
@Column(name="CERTIFICATE", length = 2048)
protected String certificatePem;
+ @Column(name="CODE_SECRET", length = 255)
+ protected String codeSecret;
@Column(name="LOGIN_THEME")
protected String loginTheme;
@@ -284,6 +286,14 @@ public class RealmEntity {
this.privateKeyPem = privateKeyPem;
}
+ public String getCodeSecret() {
+ return codeSecret;
+ }
+
+ public void setCodeSecret(String codeSecret) {
+ this.codeSecret = codeSecret;
+ }
+
public Collection<RequiredCredentialEntity> getRequiredCredentials() {
return requiredCredentials;
}
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 5578b88..ee97080 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
@@ -434,6 +434,16 @@ public class RealmAdapter implements RealmModel {
setPrivateKeyPem(privateKeyPem);
}
+ @Override
+ public String getCodeSecret() {
+ return realm.getCodeSecret();
+ }
+
+ @Override
+ public void setCodeSecret(String codeSecret) {
+ realm.setCodeSecret(codeSecret);
+ }
+
protected RequiredCredentialModel initRequiredCredentialModel(String type) {
RequiredCredentialModel model = RequiredCredentialModel.BUILT_IN.get(type);
if (model == null) {
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 ba51c13..5963ab0 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
@@ -420,6 +420,17 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
}
@Override
+ public String getCodeSecret() {
+ return realm.getCodeSecret();
+ }
+
+ @Override
+ public void setCodeSecret(String codeSecret) {
+ realm.setCodeSecret(codeSecret);
+ updateRealm();
+ }
+
+ @Override
public String getLoginTheme() {
return realm.getLoginTheme();
}
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 617f686..96e7f85 100644
--- 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
@@ -210,6 +210,15 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
for (String id : map.keySet()) {
removeUserSession(realm, id);
}
+
+ map = new MapReduceTask(sessionCache)
+ .mappedWith(ClientSessionMapper.create(realm.getId()).expiredRefresh(expiredRefresh).requireNullUserSession(true).emitKey())
+ .reducedWith(new FirstResultReducer())
+ .execute();
+
+ for (String id : map.keySet()) {
+ tx.remove(sessionCache, id);
+ }
}
@Override
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 977eb3d..7d76335 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
@@ -15,14 +15,11 @@ import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
*/
public class InfinispanUserSessionProviderFactory implements UserSessionProviderFactory {
- private static final String SESSION_CACHE_NAME = "sessions";
- private static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures";
-
@Override
public UserSessionProvider create(KeycloakSession session) {
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache<String, SessionEntity> cache = connections.getCache(SESSION_CACHE_NAME);
- Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(LOGIN_FAILURE_CACHE_NAME);
+ Cache<String, SessionEntity> cache = connections.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME);
+ Cache<LoginFailureKey, LoginFailureEntity> loginFailures = connections.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME);
return new InfinispanUserSessionProvider(session, cache, loginFailures);
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
index 369b7a6..1502b2c 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
@@ -28,6 +28,10 @@ public class ClientSessionMapper implements Mapper<String, SessionEntity, String
private String userSession;
+ private Long expiredRefresh;
+
+ private Boolean requireNullUserSession = false;
+
public static ClientSessionMapper create(String realm) {
return new ClientSessionMapper(realm);
}
@@ -52,6 +56,16 @@ public class ClientSessionMapper implements Mapper<String, SessionEntity, String
return this;
}
+ public ClientSessionMapper expiredRefresh(long expiredRefresh) {
+ this.expiredRefresh = expiredRefresh;
+ return this;
+ }
+
+ public ClientSessionMapper requireNullUserSession(boolean requireNullUserSession) {
+ this.requireNullUserSession = requireNullUserSession;
+ return this;
+ }
+
@Override
public void map(String key, SessionEntity e, Collector collector) {
if (!realm.equals(e.getRealm())) {
@@ -72,6 +86,14 @@ public class ClientSessionMapper implements Mapper<String, SessionEntity, String
return;
}
+ if (requireNullUserSession && entity.getUserSession() != null) {
+ return;
+ }
+
+ if (expiredRefresh != null && entity.getTimestamp() > expiredRefresh) {
+ return;
+ }
+
switch (emit) {
case KEY:
collector.emit(key, key);
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
index a19af58..cd5eef1 100755
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
@@ -194,7 +194,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
Iterator<ClientSessionEntity> citr = clientSessions.values().iterator();
while (citr.hasNext()) {
ClientSessionEntity c = citr.next();
- if (c.getSession() == null && c.getTimestamp() < Time.currentTime() - realm.getSsoSessionIdleTimeout()) {
+ if (c.getSession() == null && c.getRealmId().equals(realm.getId()) && c.getTimestamp() < Time.currentTime() - realm.getSsoSessionIdleTimeout()) {
citr.remove();
}
}
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
index 1c8c317..5bcbb87 100755
--- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
+++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java
@@ -16,6 +16,7 @@ import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
@@ -201,6 +202,7 @@ public class SamlService {
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
+ clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(SamlProtocol.SAML_BINDING, getBindingType());
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
clientSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
index 96c3ef6..0c60ccb 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
@@ -550,7 +550,7 @@ public class OpenIDConnectService {
String[] parts = code.split("\\.");
if (parts.length == 2) {
try {
- event.detail(Details.CODE_ID, new String(Base64Url.decode(parts[1])));
+ event.detail(Details.CODE_ID, new String(parts[1]));
} catch (Throwable t) {
}
}
@@ -776,6 +776,7 @@ public class OpenIDConnectService {
clientSession.setAuthMethod(OpenIDConnect.LOGIN_PROTOCOL);
clientSession.setRedirectUri(redirect);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE);
+ clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(OpenIDConnect.STATE_PARAM, state);
if (scopeParam != null) clientSession.setNote(OpenIDConnect.SCOPE_PARAM, scopeParam);
if (responseType != null) clientSession.setNote(OpenIDConnect.RESPONSE_TYPE_PARAM, responseType);
diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
index cf5978f..655b148 100755
--- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
+++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java
@@ -49,7 +49,7 @@ public class ApplianceBootstrap {
realm.setName(adminRealmName);
realm.setEnabled(true);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
- realm.setSsoSessionIdleTimeout(300);
+ realm.setSsoSessionIdleTimeout(1800);
realm.setAccessTokenLifespan(60);
realm.setSsoSessionMaxLifespan(36000);
realm.setAccessCodeLifespan(60);
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 9dbfbea..4856c02 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -290,9 +290,6 @@ public class AuthenticationManager {
LoginFormsProvider loginFormsProvider = Flows.forms(session, realm, client, uriInfo).setClientSessionCode(accessCode.getCode()).setUser(user);
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) {
- String key = UUID.randomUUID().toString();
- clientSession.setNote("key", key);
- loginFormsProvider.setVerifyCode(key);
event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success();
}
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 46bba42..5574cbb 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -1,8 +1,5 @@
package org.keycloak.services.managers;
-import org.keycloak.OAuthErrorException;
-import org.keycloak.jose.jws.Algorithm;
-import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@@ -11,11 +8,10 @@ import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.util.Base64Url;
import org.keycloak.util.Time;
-import java.nio.ByteBuffer;
import java.security.MessageDigest;
-import java.security.Signature;
import java.util.HashSet;
import java.util.Set;
+import java.util.UUID;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -23,6 +19,10 @@ import java.util.Set;
*/
public class ClientSessionCode {
+ public static final String ACTION_KEY = "action_key";
+
+ private static final byte[] HASH_SEPERATOR = "//".getBytes();
+
private final RealmModel realm;
private final ClientSessionModel clientSession;
@@ -34,14 +34,14 @@ public class ClientSessionCode {
public static ClientSessionCode parse(String code, KeycloakSession session) {
try {
String[] parts = code.split("\\.");
- String id = new String(Base64Url.decode(parts[1]));
+ String id = parts[1];
ClientSessionModel clientSession = session.sessions().getClientSession(id);
if (clientSession == null) {
return null;
}
- String hash = createSignatureHash(clientSession.getRealm(), clientSession);
+ String hash = createHash(clientSession.getRealm(), clientSession);
if (!hash.equals(parts[0])) {
return null;
}
@@ -56,14 +56,14 @@ public class ClientSessionCode {
public static ClientSessionCode parse(String code, KeycloakSession session, RealmModel realm) {
try {
String[] parts = code.split("\\.");
- String id = new String(Base64Url.decode(parts[1]));
+ String id = parts[1];
ClientSessionModel clientSession = session.sessions().getClientSession(realm, id);
if (clientSession == null) {
return null;
}
- String hash = createSignatureHash(realm, clientSession);
+ String hash = createHash(realm, clientSession);
if (!hash.equals(parts[0])) {
return null;
}
@@ -78,10 +78,6 @@ public class ClientSessionCode {
return clientSession;
}
- public boolean isValid(RequiredAction requiredAction) {
- return isValid(convertToAction(requiredAction));
- }
-
public boolean isValid(ClientSessionModel.Action requestedAction) {
ClientSessionModel.Action action = clientSession.getAction();
if (action == null) {
@@ -111,6 +107,7 @@ public class ClientSessionCode {
public void setAction(ClientSessionModel.Action action) {
clientSession.setAction(action);
+ clientSession.setNote(ACTION_KEY, UUID.randomUUID().toString());
clientSession.setTimestamp(Time.currentTime());
}
@@ -138,29 +135,24 @@ public class ClientSessionCode {
}
private static String generateCode(RealmModel realm, ClientSessionModel clientSession) {
- String hash = createSignatureHash(realm, clientSession);
+ String hash = createHash(realm, clientSession);
StringBuilder sb = new StringBuilder();
sb.append(hash);
sb.append(".");
- sb.append(Base64Url.encode(clientSession.getId().getBytes()));
+ sb.append(clientSession.getId());
return sb.toString();
}
- private static String createSignatureHash(RealmModel realm, ClientSessionModel clientSession) {
+ private static String createHash(RealmModel realm, ClientSessionModel clientSession) {
try {
- Signature signature = Signature.getInstance(RSAProvider.getJavaAlgorithm(Algorithm.RS256));
- signature.initSign(realm.getPrivateKey());
- signature.update(clientSession.getId().getBytes());
- signature.update(ByteBuffer.allocate(4).putInt(clientSession.getTimestamp()));
- if (clientSession.getAction() != null) {
- signature.update(clientSession.getAction().toString().getBytes());
- }
- byte[] sign = signature.sign();
-
- MessageDigest digest = MessageDigest.getInstance("sha-1");
- digest.update(sign);
+ MessageDigest digest = MessageDigest.getInstance("sha-256");
+ digest.update(realm.getCodeSecret().getBytes());
+ digest.update(HASH_SEPERATOR);
+ digest.update(clientSession.getId().getBytes());
+ digest.update(HASH_SEPERATOR);
+ digest.update(clientSession.getNote(ACTION_KEY).getBytes());
return Base64Url.encode(digest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
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 5eff07b..2d91533 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
@@ -709,9 +709,6 @@ public class UsersResource {
try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
builder.queryParam("code", accessCode.getCode());
- String key = UUID.randomUUID().toString();
- clientSession.setNote("key", key);
- builder.queryParam("key", key);
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
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 7310d74..b970471 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -271,9 +271,9 @@ public class LoginActionsService {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown code, please login again through your application.");
}
ClientSessionModel clientSession = clientCode.getClientSession();
- if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) {
+ if (!(clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientCode.isValid(ClientSessionModel.Action.RECOVER_PASSWORD))) {
clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE);
- event.client(clientSession.getClient()).error(Errors.INVALID_USER_CREDENTIALS);
+ event.client(clientSession.getClient()).error(Errors.INVALID_CODE);
return Flows.forms(this.session, realm, clientSession.getClient(), uriInfo).setError(Messages.INVALID_USER)
.setClientSessionCode(clientCode.getCode())
.createLogin();
@@ -714,22 +714,17 @@ public class LoginActionsService {
@Path("email-verification")
@GET
- public Response emailVerification(@QueryParam("code") String code) {
+ public Response emailVerification(@QueryParam("code") String code, @QueryParam("key") String key) {
event.event(EventType.VERIFY_EMAIL);
- if (uriInfo.getQueryParameters().containsKey("key")) {
+ if (key != null) {
Checks checks = new Checks();
- if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) {
+ if (!checks.check(key, ClientSessionModel.Action.VERIFY_EMAIL)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
UserSessionModel userSession = clientSession.getUserSession();
UserModel user = userSession.getUser();
- String key = uriInfo.getQueryParameters().getFirst("key");
- String keyNote = clientSession.getNote("key");
- if (key == null || !key.equals(keyNote)) {
- return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your email.");
- }
initEvent(clientSession);
user.setEmailVerified(true);
@@ -745,16 +740,11 @@ public class LoginActionsService {
}
ClientSessionCode accessCode = checks.clientCode;
ClientSessionModel clientSession = accessCode.getClientSession();
- String verifyCode = UUID.randomUUID().toString();
- clientSession.setNote("key", verifyCode);
UserSessionModel userSession = clientSession.getUserSession();
- UserModel user = userSession.getUser();
-
initEvent(clientSession);
return Flows.forms(session, realm, null, uriInfo)
.setClientSessionCode(accessCode.getCode())
- .setVerifyCode(verifyCode)
.setUser(userSession.getUser())
.createResponse(RequiredAction.VERIFY_EMAIL);
}
@@ -762,22 +752,15 @@ public class LoginActionsService {
@Path("password-reset")
@GET
- public Response passwordReset(@QueryParam("code") String code) {
- event.event(EventType.SEND_RESET_PASSWORD);
- if (uriInfo.getQueryParameters().containsKey("key")) {
+ public Response passwordReset(@QueryParam("code") String code, @QueryParam("key") String key) {
+ event.event(EventType.RESET_PASSWORD);
+ if (key != null) {
Checks checks = new Checks();
- if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD)) {
+ if (!checks.check(key, ClientSessionModel.Action.RECOVER_PASSWORD)) {
return checks.response;
}
ClientSessionCode accessCode = checks.clientCode;
- ClientSessionModel clientSession = accessCode.getClientSession();
- UserSessionModel userSession = clientSession.getUserSession();
- UserModel user = userSession.getUser();
- String key = uriInfo.getQueryParameters().getFirst("key");
- String keyNote = clientSession.getNote("key");
- if (key == null || !key.equals(keyNote)) {
- return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Somebody is trying to illegally change your password.");
- }
+ accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD);
return Flows.forms(session, realm, null, uriInfo)
.setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.UPDATE_PASSWORD);
@@ -838,14 +821,11 @@ public class LoginActionsService {
event.session(userSession);
TokenManager.attachClientSession(userSession, clientSession);
- accessCode.setRequiredAction(RequiredAction.UPDATE_PASSWORD);
+ accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD);
try {
UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri());
- builder.queryParam("code", accessCode.getCode());
- String verifyCode = UUID.randomUUID().toString();
- clientSession.setNote("key", verifyCode);
- builder.queryParam("key", verifyCode);
+ builder.queryParam("key", accessCode.getCode());
String link = builder.build(realm.getName()).toString();
long expiration = TimeUnit.SECONDS.toMinutes(realm.getAccessCodeLifespanUserAction());
@@ -861,7 +841,7 @@ public class LoginActionsService {
}
}
- return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").createPasswordReset();
+ return Flows.forms(session, realm, client, uriInfo).setSuccess("emailSent").setClientSessionCode(accessCode.getCode()).createPasswordReset();
}
private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) {
diff --git a/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh b/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh
index 137e5c6..26de114 100644
--- a/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh
+++ b/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh
@@ -20,7 +20,9 @@ sed -i -e 's/<\/periodic-rotating-file-handler>/&\n <logger category=\"org.keycl
sed -i -e 's/<subsystem xmlns=\"urn:jboss:domain:infinispan:[0-9]\.[0-9]\">/&\n <cache-container name=\"keycloak\" jndi-name=\"infinispan\/Keycloak\" start=\"EAGER\"> \
\n <transport lock-timeout=\"60000\"\/>\n <distributed-cache name=\"sessions\" mode=\"SYNC\" owners=\"2\" segments=\"60\"\/> \
-\n <invalidation-cache name=\"realms\" mode=\"SYNC\"\/>\n <\/cache-container>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml
+\n <distributed-cache name=\"loginFailures\" mode=\"SYNC\" owners=\"2\" segments=\"60\"\/> \
+\n <invalidation-cache name=\"realms\" mode=\"SYNC\"\/>\n \
+\n <invalidation-cache name=\"users\" mode=\"SYNC\"\/>\n <\/cache-container>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml
sed -i "s|<mod-cluster-config .*>|<mod-cluster-config advertise-socket=\"modcluster\" proxy-list=\"\$\{httpd.proxyList\}\" proxy-url=\"\/\" balancer=\"mycluster\" advertise=\"false\" connector=\"ajp\" sticky-session=\"true\">|" $JBOSS_HOME/standalone/configuration/standalone-ha.xml
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
index f2b8b26..0f2b13f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionEmailVerificationTest.java
@@ -122,7 +122,7 @@ public class RequiredActionEmailVerificationTest {
String mailCodeId = sendEvent.getDetails().get(Details.CODE_ID);
- //Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1]);
+ Assert.assertEquals(mailCodeId, verificationUrl.split("key=")[1].split("\\.")[1]);
driver.navigate().to(verificationUrl.trim());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenancyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenancyTest.java
new file mode 100644
index 0000000..c6df988
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenancyTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2014 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.testsuite.adapter;
+
+import javax.ws.rs.core.UriBuilder;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OpenIDConnectService;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.keycloak.testutils.KeycloakServer;
+import org.openqa.selenium.WebDriver;
+
+/**
+ *
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+public class MultiTenancyTest {
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @WebResource
+ protected WebDriver driver;
+
+ @ClassRule
+ public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
+ @Override
+ protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
+ RealmRepresentation tenant1 = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/tenant1-realm.json"), RealmRepresentation.class);
+ manager.importRealm(tenant1);
+
+ RealmRepresentation tenant2 = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/tenant2-realm.json"), RealmRepresentation.class);
+ manager.importRealm(tenant2);
+
+ deployApplication("multi-tenant", "/multi-tenant", MultiTenantServlet.class, null, "user", true, MultiTenantResolver.class);
+ }
+ };
+
+ /**
+ * Simplest scenario: one user, one realm. The user is not logged in at
+ * any other realm
+ * @throws Exception
+ */
+ @Test
+ public void testTenantsLoggingOut() throws Exception {
+ doTenantRequests("tenant1", true);
+ doTenantRequests("tenant2", true);
+ }
+
+ /**
+ * This tests the adapter's ability to deal with multiple sessions
+ * from the same user, one for each realm. It should not mixup and return
+ * a session from tenant1 to tenant2
+ * @throws Exception
+ */
+ @Test
+ public void testTenantsWithoutLoggingOut() throws Exception {
+ doTenantRequests("tenant1", true);
+ doTenantRequests("tenant2", true);
+
+ doTenantRequests("tenant1", false);
+ doTenantRequests("tenant2", true);
+ }
+
+ /**
+ * This test simulates an user that is not logged in yet, and tris to login
+ * into tenant1 using an account from tenant2.
+ * On this scenario, the user should be shown the login page again.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testUnauthorizedAccessNotLoggedIn() throws Exception {
+ String keycloakServerBaseUrl = "http://localhost:8081/auth";
+
+ driver.navigate().to("http://localhost:8081/multi-tenant?realm=tenant1");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(keycloakServerBaseUrl));
+
+ loginPage.login("user-tenant2", "user-tenant2");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(keycloakServerBaseUrl));
+ }
+
+ /**
+ * This test simulates an user which is already logged in into tenant1
+ * and tries to access a resource on tenant2.
+ * On this scenario, the user should be shown the login page again.
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testUnauthorizedAccessLoggedIn() throws Exception {
+ String keycloakServerBaseUrl = "http://localhost:8081/auth";
+ doTenantRequests("tenant1", false);
+
+ driver.navigate().to("http://localhost:8081/multi-tenant?realm=tenant2");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(keycloakServerBaseUrl));
+ }
+
+ private void doTenantRequests(String tenant, boolean logout) {
+ String tenantLoginUrl = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build(tenant).toString();
+
+ driver.navigate().to("http://localhost:8081/multi-tenant?realm="+tenant);
+ System.out.println("Current url: " + driver.getCurrentUrl());
+
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(tenantLoginUrl));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+
+ Assert.assertEquals("http://localhost:8081/multi-tenant?realm="+tenant, driver.getCurrentUrl());
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+
+ Assert.assertTrue(pageSource.contains("Username: bburke@redhat.com"));
+ Assert.assertTrue(pageSource.contains("Realm: "+tenant));
+
+ if (logout) {
+ driver.manage().deleteAllCookies();
+ }
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantResolver.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantResolver.java
new file mode 100644
index 0000000..1acf95d
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantResolver.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014 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.testsuite.adapter;
+
+import java.io.InputStream;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakConfigResolver;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+
+/**
+ *
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+public class MultiTenantResolver implements KeycloakConfigResolver {
+
+ @Override
+ public KeycloakDeployment resolve(HttpFacade.Request request) {
+ String realm = request.getQueryParamValue("realm");
+ InputStream is = getClass().getResourceAsStream("/adapter-test/"+realm+"-keycloak.json");
+
+ KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(is);
+ return deployment;
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantServlet.java
new file mode 100644
index 0000000..5e04cf2
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/MultiTenantServlet.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2014 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.testsuite.adapter;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.keycloak.KeycloakSecurityContext;
+
+/**
+ *
+ * @author Juraci Paixão Kröhling <juraci at kroehling.de>
+ */
+public class MultiTenantServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ KeycloakSecurityContext context = (KeycloakSecurityContext)req.getAttribute(KeycloakSecurityContext.class.getName());
+
+ pw.print("Username: ");
+ pw.println(context.getIdToken().getPreferredUsername());
+
+ pw.print("<br/>Realm: ");
+ pw.println(context.getRealm());
+
+ pw.flush();
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
index 8f94592..d0a8db9 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/FederationProvidersIntegrationTest.java
@@ -148,6 +148,15 @@ public class FederationProvidersIntegrationTest {
}
@Test
+ public void loginLdapWithEmail() {
+ loginPage.open();
+ loginPage.login("john@email.org", "Password1");
+
+ Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+ }
+
+ @Test
public void XdeleteLink() {
loginLdap();
{
@@ -200,6 +209,11 @@ public class FederationProvidersIntegrationTest {
loginPage.open();
loginPage.login("johnkeycloak", "New-password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
+
+ // Change password back to previous value
+ changePasswordPage.open();
+ changePasswordPage.changePassword("New-password1", "Password1", "Password1");
+ Assert.assertEquals("Your password has been updated", profilePage.getSuccess());
}
@Test
@@ -370,7 +384,7 @@ public class FederationProvidersIntegrationTest {
Assert.assertTrue(session.users().validCredentials(appRealm, user, cred));
// LDAP password is still unchanged
- Assert.assertTrue(LDAPUtils.validatePassword(getPartitionManager(session, model), "johnkeycloak", "New-password1"));
+ Assert.assertTrue(LDAPUtils.validatePassword(getPartitionManager(session, model), "johnkeycloak", "Password1"));
// ATM it's not permitted to delete user in unsynced mode. Should be user deleted just locally instead?
Assert.assertFalse(session.users().removeUser(appRealm, user));
@@ -387,6 +401,18 @@ public class FederationProvidersIntegrationTest {
}
}
+ @Test
+ public void testCaseSensitiveSearch() {
+ loginPage.open();
+
+ // This should fail for now due to case-sensitivity
+ loginPage.login("johnKeycloak", "Password1");
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+
+ loginPage.login("John@email.org", "Password1");
+ Assert.assertEquals("Invalid username or password.", loginPage.getError());
+ }
+
static PartitionManager getPartitionManager(KeycloakSession keycloakSession, UserFederationProviderModel ldapFedModel) {
PartitionManagerProvider partitionManagerProvider = keycloakSession.getProvider(PartitionManagerProvider.class);
return partitionManagerProvider.getPartitionManager(ldapFedModel);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
index 8a922af..4673165 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java
@@ -166,7 +166,7 @@ public class LoginTotpTest {
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
- AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().error("invalid_user_credentials")
+ AssertEvents.ExpectedEvent expectedEvent = events.expectLogin().error("invalid_code")
.user((String)null)
.clearDetails()
.session((String) null);
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 22ec3c1..774d683 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
@@ -38,6 +38,7 @@ import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
+import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
@@ -97,6 +98,9 @@ public class ResetPasswordTest {
protected LoginPage loginPage;
@WebResource
+ protected ErrorPage errorPage;
+
+ @WebResource
protected LoginPasswordResetPage resetPasswordPage;
@WebResource
@@ -111,6 +115,42 @@ public class ResetPasswordTest {
}
@Test
+ public void resetPasswordCancel() throws IOException, MessagingException {
+ loginPage.open();
+ loginPage.resetPassword();
+
+ resetPasswordPage.assertCurrent();
+
+ resetPasswordPage.changePassword("login-test");
+
+ resetPasswordPage.assertCurrent();
+
+ events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).detail(Details.USERNAME, "login-test").detail(Details.EMAIL, "login@test.com").assertEvent().getSessionId();
+
+ resetPasswordPage.backToLogin();
+
+ Assert.assertTrue(loginPage.isCurrent());
+
+ loginPage.login("login-test", "password");
+
+ events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+
+ String body = (String) message.getContent();
+ String changePasswordUrl = MailUtil.getLink(body);
+
+ driver.navigate().to(changePasswordUrl.trim());
+
+ events.expect(EventType.RESET_PASSWORD_ERROR).client((String) null).user((String) null).error("invalid_code").clearDetails().assertEvent();
+
+ Assert.assertTrue(errorPage.isCurrent());
+ Assert.assertEquals("Unknown code, please login again through your application.", errorPage.getError());
+ }
+
+ @Test
public void resetPasswordByEmail() throws IOException, MessagingException {
resetPassword("login@test.com");
}
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 c68ae7d..420d66c 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
@@ -244,12 +244,15 @@ public class UserSessionProviderTest {
@Test
public void testRemoveUserSessionsByExpired() {
session.sessions().getUserSessions(realm, session.users().getUserByUsername("user1", realm));
+ ClientModel client = realm.findClient("test-app");
try {
Set<String> expired = new HashSet<String>();
+ Set<String> expiredClientSessions = new HashSet<String>();
Time.setOffset(-(realm.getSsoSessionMaxLifespan() + 1));
expired.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true).getId());
+ expiredClientSessions.add(session.sessions().createClientSession(realm, client).getId());
Time.setOffset(0);
UserSessionModel s = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true);
@@ -257,9 +260,15 @@ public class UserSessionProviderTest {
s.setLastSessionRefresh(0);
expired.add(s.getId());
+ ClientSessionModel clSession = session.sessions().createClientSession(realm, client);
+ clSession.setUserSession(s);
+ expiredClientSessions.add(clSession.getId());
+
Set<String> valid = new HashSet<String>();
+ Set<String> validClientSessions = new HashSet<String>();
valid.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true).getId());
+ validClientSessions.add(session.sessions().createClientSession(realm, client).getId());
resetSession();
@@ -269,10 +278,16 @@ public class UserSessionProviderTest {
for (String e : expired) {
assertNull(session.sessions().getUserSession(realm, e));
}
+ for (String e : expiredClientSessions) {
+ assertNull(session.sessions().getClientSession(realm, e));
+ }
for (String v : valid) {
assertNotNull(session.sessions().getUserSession(realm, v));
}
+ for (String e : validClientSessions) {
+ assertNotNull(session.sessions().getClientSession(realm, e));
+ }
} finally {
Time.setOffset(0);
}
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 fcd51b3..ca4b7ee 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
@@ -139,7 +139,7 @@ public class RefreshTokenTest {
Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
int actual = refreshToken.getExpiration() - Time.currentTime();
- Assert.assertThat(actual, allOf(greaterThanOrEqualTo(559), lessThanOrEqualTo(600)));
+ Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800)));
Assert.assertEquals(sessionId, refreshToken.getSessionState());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
index 8817c9c..a244c32 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPasswordResetPage.java
@@ -41,6 +41,9 @@ public class LoginPasswordResetPage extends AbstractPage {
@FindBy(className = "feedback-error")
private WebElement emailErrorMessage;
+ @FindBy(partialLinkText = "Back to Login")
+ private WebElement backToLogin;
+
public void changePassword(String username) {
usernameInput.sendKeys(username);
@@ -63,4 +66,8 @@ public class LoginPasswordResetPage extends AbstractPage {
return emailErrorMessage != null ? emailErrorMessage.getText() : null;
}
+ public void backToLogin() {
+ backToLogin.click();
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
index 06e793f..0808dcb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
@@ -24,6 +24,7 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
+import org.keycloak.adapters.KeycloakConfigResolver;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -133,9 +134,17 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
}
public void deployApplication(String name, String contextPath, Class<? extends Servlet> servletClass, String adapterConfigPath, String role, boolean isConstrained) {
+ deployApplication(name, contextPath, servletClass, adapterConfigPath, role, isConstrained, null);
+ }
+
+ public void deployApplication(String name, String contextPath, Class<? extends Servlet> servletClass, String adapterConfigPath, String role, boolean isConstrained, Class<? extends KeycloakConfigResolver> keycloakConfigResolver) {
String constraintUrl = "/*";
DeploymentInfo di = createDeploymentInfo(name, contextPath, servletClass);
- di.addInitParameter("keycloak.config.file", adapterConfigPath);
+ if (null == keycloakConfigResolver) {
+ di.addInitParameter("keycloak.config.file", adapterConfigPath);
+ } else {
+ di.addInitParameter("keycloak.config.resolver", keycloakConfigResolver.getCanonicalName());
+ }
if (isConstrained) {
SecurityConstraint constraint = new SecurityConstraint();
WebResourceCollection collection = new WebResourceCollection();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
index 302fc91..6c104fb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/saml/SamlBindingTest.java
@@ -71,8 +71,7 @@ public class SamlBindingTest {
@WebResource
protected LoginPage loginPage;
- @Test
- @Ignore
+ //@Test
public void runit() throws Exception {
Thread.sleep(10000000);
}
diff --git a/testsuite/integration/src/test/resources/adapter-test/tenant1-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/tenant1-keycloak.json
new file mode 100644
index 0000000..80bff8e
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/tenant1-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "tenant1",
+ "resource" : "multi-tenant",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8081/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/resources/adapter-test/tenant1-realm.json b/testsuite/integration/src/test/resources/adapter-test/tenant1-realm.json
new file mode 100644
index 0000000..783776f
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/tenant1-realm.json
@@ -0,0 +1,75 @@
+{
+ "id": "tenant1",
+ "realm": "tenant1",
+ "enabled": true,
+ "accessTokenLifespan": 3000,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "social": false,
+ "passwordCredentialGrantAllowed": true,
+ "updateProfileOnInitialSocialLogin": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "bburke@redhat.com",
+ "enabled": true,
+ "email" : "bburke@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ },
+ {
+ "username" : "user-tenant1",
+ "enabled": true,
+ "email" : "user-tenant1@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "user-tenant1" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ }
+ ]
+ },
+ "scopeMappings": [
+ {
+ "client": "multi-tenant",
+ "roles": ["user"]
+ }
+
+ ],
+ "applications": [
+ {
+ "name": "multi-tenant",
+ "enabled": true,
+ "adminUrl": "http://localhost:8081/multi-tenant",
+ "baseUrl": "http://localhost:8081/multi-tenant",
+ "redirectUris": [
+ "http://localhost:8081/multi-tenant/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
diff --git a/testsuite/integration/src/test/resources/adapter-test/tenant2-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/tenant2-keycloak.json
new file mode 100644
index 0000000..deb538d
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/tenant2-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "tenant2",
+ "resource" : "multi-tenant",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8081/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/resources/adapter-test/tenant2-realm.json b/testsuite/integration/src/test/resources/adapter-test/tenant2-realm.json
new file mode 100644
index 0000000..1c17f11
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/tenant2-realm.json
@@ -0,0 +1,75 @@
+{
+ "id": "tenant2",
+ "realm": "tenant2",
+ "enabled": true,
+ "accessTokenLifespan": 3000,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "social": false,
+ "passwordCredentialGrantAllowed": true,
+ "updateProfileOnInitialSocialLogin": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "bburke@redhat.com",
+ "enabled": true,
+ "email" : "bburke@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ },
+ {
+ "username" : "user-tenant2",
+ "enabled": true,
+ "email" : "user-tenant2@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "user-tenant2" }
+ ],
+ "realmRoles": [ "user" ],
+ "applicationRoles": {
+ "multi-tenant": [ "user" ]
+ }
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ }
+ ]
+ },
+ "scopeMappings": [
+ {
+ "client": "multi-tenant",
+ "roles": ["user"]
+ }
+
+ ],
+ "applications": [
+ {
+ "name": "multi-tenant",
+ "enabled": true,
+ "adminUrl": "http://localhost:8081/multi-tenant",
+ "baseUrl": "http://localhost:8081/multi-tenant",
+ "redirectUris": [
+ "http://localhost:8081/multi-tenant/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
testsuite/tomcat7/pom.xml 2(+1 -1)
diff --git a/testsuite/tomcat7/pom.xml b/testsuite/tomcat7/pom.xml
index f667d87..f53a489 100755
--- a/testsuite/tomcat7/pom.xml
+++ b/testsuite/tomcat7/pom.xml
@@ -10,7 +10,7 @@
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-testsuite-tomcat7</artifactId>
- <name>Keycloak Tomcat 7Integration TestSuite</name>
+ <name>Keycloak Tomcat 7 Integration TestSuite</name>
<description />
<dependencies>
diff --git a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/Tomcat7Test.java b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/Tomcat7Test.java
index e0603fc..791e0d3 100755
--- a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/Tomcat7Test.java
+++ b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/Tomcat7Test.java
@@ -60,6 +60,7 @@ import java.io.OutputStream;
import java.net.URL;
import java.security.Principal;
import java.util.Map;
+import java.util.regex.Matcher;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -106,12 +107,11 @@ public class Tomcat7Test {
@BeforeClass
public static void initTomcat() throws Exception {
-
- String webappDirLocation = "src/test/resources/tomcat-test/webapp/";
URL dir = Tomcat7Test.class.getResource("/tomcat-test/webapp/META-INF/context.xml");
File webappDir = new File(dir.getFile()).getParentFile().getParentFile();
tomcat = new Tomcat();
-
+ String baseDir = getBaseDirectory();
+ tomcat.setBaseDir(baseDir);
tomcat.setPort(8080);
tomcat.addWebapp("/customer-portal", webappDir.toString());
@@ -167,7 +167,24 @@ public class Tomcat7Test {
}
+ private static String getBaseDirectory() {
+ String dirPath = null;
+ String relativeDirPath = "testsuite" + File.separator + "tomcat7" + File.separator + "target";
+
+ if (System.getProperties().containsKey("maven.home")) {
+ dirPath = System.getProperty("user.dir").replaceFirst("testsuite.tomcat7.*", Matcher.quoteReplacement(relativeDirPath));
+ } else {
+ for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) {
+ if (c.contains(File.separator + "testsuite" + File.separator + "tomcat7")) {
+ dirPath = c.replaceFirst("testsuite.tomcat7.*", Matcher.quoteReplacement(relativeDirPath));
+ break;
+ }
+ }
+ }
+ String absolutePath = new File(dirPath).getAbsolutePath();
+ return absolutePath;
+ }