diff --git a/.gitignore b/.gitignore
index e9d50ff8be..319769bf2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,11 @@
.settings
.classpath
+
+# NetBeans #
+############
+nb-configuration.xml
+
# Compiled source #
###################
*.com
diff --git a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
index 4658c68157..e9809e3753 100755
--- a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
+++ b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
@@ -17,6 +17,7 @@ import java.io.Serializable;
public class KeycloakSecurityContext implements Serializable {
protected String tokenString;
protected String idTokenString;
+ protected String realm;
// Don't store parsed tokens into HTTP session
protected transient AccessToken token;
@@ -25,11 +26,12 @@ public class KeycloakSecurityContext implements Serializable {
public KeycloakSecurityContext() {
}
- public KeycloakSecurityContext(String tokenString, AccessToken token, String idTokenString, IDToken idToken) {
+ public KeycloakSecurityContext(String tokenString, AccessToken token, String idTokenString, IDToken idToken, String realm) {
this.tokenString = tokenString;
this.token = token;
this.idToken = idToken;
this.idTokenString = idTokenString;
+ this.realm = realm;
}
public AccessToken getToken() {
@@ -48,6 +50,9 @@ public class KeycloakSecurityContext implements Serializable {
return idTokenString;
}
+ public String getRealm() {
+ return realm;
+ }
// SERIALIZATION
diff --git a/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
index f3a89b6d1a..951a6a0d1d 100755
--- a/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
+++ b/core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
@@ -56,6 +56,7 @@ public class SkeletonKeyTokenTest {
@Test
public void testSerialization() throws Exception {
+ String realm = "acme";
AccessToken token = createSimpleToken();
IDToken idToken = new IDToken();
idToken.setEmail("joe@email.cz");
@@ -69,7 +70,7 @@ public class SkeletonKeyTokenTest {
.jsonContent(idToken)
.rsa256(keyPair.getPrivate());
- KeycloakSecurityContext ctx = new KeycloakSecurityContext(encoded, token, encodedIdToken, idToken);
+ KeycloakSecurityContext ctx = new KeycloakSecurityContext(encoded, token, encodedIdToken, idToken, realm);
KeycloakPrincipal principal = new KeycloakPrincipal("joe", ctx);
// Serialize
@@ -96,6 +97,7 @@ 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();
}
diff --git a/distribution/examples-docs-zip/build.xml b/distribution/examples-docs-zip/build.xml
index 3e4f1dcf52..a33d88d58c 100755
--- a/distribution/examples-docs-zip/build.xml
+++ b/distribution/examples-docs-zip/build.xml
@@ -42,6 +42,14 @@
+
+
+
+
+
+
+
+
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 6a704151ec..db6dead9ce 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -36,6 +36,7 @@
+
]>
@@ -88,6 +89,7 @@ This one is short
&JavascriptAdapter;
&InstalledApplications;
&Logout;
+ &MultiTenancy;
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 0000000000..410621f049
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/multi-tenancy.xml
@@ -0,0 +1,56 @@
+
+ Multi Tenancy
+
+ 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 keycloak.json files.
+ For this case, there are two possible solutions:
+
+
+
+ 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.
+
+
+
+ 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
+ (client1.acme.com, client2.acme.com) or path (/app/client1/,
+ /app/client2/) or even via a special HTTP Header.
+
+
+
+
+ This chapter of the reference guide focus on this second scenario.
+
+
+
+ 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:
+
+
+
+
+ Add a context parameter to the web.xml, named keycloak.config.resolver.
+ The value of this property should be the fully qualified name of the a class extending
+ org.keycloak.adapters.KeycloakConfigResolver.
+
+
+
+ A concrete implementation of org.keycloak.adapters.KeycloakConfigResolver. Keycloak will call the
+ resolve(org.keycloak.adapters.HttpFacade.Request) method and expects a complete
+ org.keycloak.adapters.KeycloakDeployment in response. Note that Keycloak will call this for every request,
+ so, take the usual performance precautions.
+
+
+
+
+
+ An implementation of this feature can be found on the examples.
+
+
diff --git a/examples/README.md b/examples/README.md
index 74970e937a..cc4235483d 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/examples/cors/pom.xml b/examples/cors/pom.xml
index 0317ffa82a..9b1c0487ee 100755
--- a/examples/cors/pom.xml
+++ b/examples/cors/pom.xml
@@ -6,7 +6,7 @@
1.1.0-Alpha1-SNAPSHOT
../../pom.xml
- Examples
+ Keycloak Examples - CORS
4.0.0
diff --git a/examples/multi-tenant/README.md b/examples/multi-tenant/README.md
new file mode 100644
index 0000000000..71c9b8c7ea
--- /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 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/pom.xml b/examples/multi-tenant/pom.xml
new file mode 100644
index 0000000000..653632c5de
--- /dev/null
+++ b/examples/multi-tenant/pom.xml
@@ -0,0 +1,65 @@
+
+ 4.0.0
+
+
+ keycloak-parent
+ org.keycloak
+ 1.1.0-Alpha1-SNAPSHOT
+ ../../pom.xml
+
+
+ Keycloak Examples - Multi Tenant
+ multitenant
+ war
+
+
+ Keycloak Multi Tenants Example
+
+
+
+
+
+ org.wildfly.bom
+ jboss-javaee-7.0-with-all
+ 8.0.0.Final
+ pom
+ import
+
+
+
+ org.keycloak
+ keycloak-core
+ ${project.version}
+
+
+ org.keycloak
+ keycloak-adapter-core
+ ${project.version}
+
+
+
+
+
+
+ org.jboss.spec.javax.servlet
+ jboss-servlet-api_3.1_spec
+ provided
+
+
+
+
+ org.keycloak
+ keycloak-adapter-core
+
+
+
+
+ org.keycloak
+ keycloak-core
+
+
+
+ ${project.artifactId}
+
+
+
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 0000000000..991169dc29
--- /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
+ */
+@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("
User: ");
+ writer.write(principal.getKeycloakSecurityContext().getIdToken().getPreferredUsername());
+
+ writer.write(String.format("
Logout", 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 0000000000..4aa2fea2d0
--- /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
+ */
+public class PathBasedKeycloakConfigResolver implements KeycloakConfigResolver {
+
+ private final Map cache = new ConcurrentHashMap();
+
+ @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 0000000000..57be2774e7
--- /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 0000000000..4f221dc66d
--- /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 0000000000..1693f7acbc
--- /dev/null
+++ b/examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ *
+ keycloak
+
+
+
\ 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 0000000000..adfc73f5a5
--- /dev/null
+++ b/examples/multi-tenant/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,29 @@
+
+ Multi Tenant Example
+
+
+ keycloak.config.resolver
+ org.keycloak.example.multitenant.control.PathBasedKeycloakConfigResolver
+
+
+
+
+ REST endpoints
+ /*
+
+
+ *
+
+
+
+
+ KEYCLOAK
+ not-important
+
+
+ user
+
+
\ No newline at end of file
diff --git a/examples/multi-tenant/tenant1-realm.json b/examples/multi-tenant/tenant1-realm.json
new file mode 100644
index 0000000000..76acce85ff
--- /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"
+ }
+ ]
+}
diff --git a/examples/multi-tenant/tenant2-realm.json b/examples/multi-tenant/tenant2-realm.json
new file mode 100644
index 0000000000..295cb3f4d3
--- /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"
+ }
+ ]
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index ea6fac8122..7ec8a316fd 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -29,5 +29,6 @@
demo-template
providers
js-console
+ multi-tenant
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 9fff2b26c8..9c107ae8c4 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 0000000000..8ba41437e2
--- /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
+ */
+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 aea33dd305..099697f398 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 24ac81445e..28746e5387 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
@@ -26,7 +26,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
}
public RefreshableKeycloakSecurityContext(KeycloakDeployment deployment, AdapterTokenStore tokenStore, String tokenString, AccessToken token, String idTokenString, IDToken idToken, String refreshToken) {
- super(tokenString, token, idTokenString, idToken);
+ super(tokenString, token, idTokenString, idToken, deployment.getRealm());
this.deployment = deployment;
this.tokenStore = tokenStore;
this.refreshToken = refreshToken;
@@ -67,6 +67,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
public void setCurrentRequestInfo(KeycloakDeployment deployment, AdapterTokenStore tokenStore) {
this.deployment = deployment;
this.tokenStore = tokenStore;
+ this.realm = deployment.getRealm();
}
/**
@@ -83,6 +84,11 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
if (this.deployment == null || refreshToken == null) return false; // Might be serialized in HttpSession?
+ if (!this.realm.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 26fd307826..ddf06aee3a 100644
--- 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
@@ -96,6 +96,12 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
}
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
+
+ if (!session.getRealm().equals(deployment.getRealm())) {
+ log.debug("Account from cookie is from a different realm than for the request.");
+ return null;
+ }
+
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 6fe8c5999e..b5361ce94b 100644
--- 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,15 @@ 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;
+
+ if (!deployment.getRealm().equals(session.getRealm())) {
+ log.debug("Account from cookie is from a different realm than for the request.");
+ 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
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 9d25e92267..843b039488 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
@@ -117,16 +114,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 = (String) context.getServletContext().getAttribute("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/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java
index 5d700c1a8b..08a0a331c2 100755
--- a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java
+++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilter.java
@@ -71,7 +71,7 @@ public class JaxrsBearerTokenFilter implements ContainerRequestFilter {
try {
AccessToken token = RSATokenVerifier.verifyToken(tokenString, realmPublicKey, realm);
- KeycloakSecurityContext skSession = new KeycloakSecurityContext(tokenString, token, null, null);
+ KeycloakSecurityContext skSession = new KeycloakSecurityContext(tokenString, token, null, null, realm);
ResteasyProviderFactory.pushContext(KeycloakSecurityContext.class, skSession);
final KeycloakPrincipal principal = new KeycloakPrincipal(token.getSubject(), skSession);
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 dec12b917d..1678dd9895 100644
--- 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
@@ -94,6 +94,12 @@ public class CatalinaCookieTokenStore implements AdapterTokenStore {
}
RefreshableKeycloakSecurityContext session = principal.getKeycloakSecurityContext();
+
+ if (!session.getRealm().equals(deployment.getRealm())) {
+ log.fine("Account from cookie is from a different realm than for the request.");
+ return null;
+ }
+
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 81a765bb1c..6cc3ce699f 100644
--- 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,15 @@ 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;
+
+ if (!deployment.getRealm().equals(session.getRealm())) {
+ log.fine("Account from cookie is from a different realm than for the request.");
+ 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
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 48b3c4e0f8..984f4d93b0 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
@@ -91,16 +90,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 = (String) context.getServletContext().getAttribute("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 5c7a16b4bd..948402195e 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 Bill Burke
@@ -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 fe0c6c9a98..3dccf8c1dd 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 50859190fb..295c586022 100644
--- 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 92848847ec..37325a8cdb 100644
--- 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/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 0000000000..c6df988c07
--- /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
+ */
+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 0000000000..1acf95da7d
--- /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
+ */
+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 0000000000..5e04cf29e3
--- /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
+ */
+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("
Realm: ");
+ pw.println(context.getRealm());
+
+ pw.flush();
+ }
+}
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 06e793f92a..0808dcb50a 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 Bill Burke
@@ -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/resources/adapter-test/tenant1-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/tenant1-keycloak.json
new file mode 100644
index 0000000000..80bff8eaac
--- /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 0000000000..783776f24e
--- /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 0000000000..deb538d2b8
--- /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 0000000000..1c17f118d5
--- /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"
+ }
+ ]
+}