keycloak-uncached

Changes

.gitignore 5(+5 -0)

examples/pom.xml 1(+1 -0)

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/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
index 4658c68..e9809e3 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 f3a89b6..951a6a0 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 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/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/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>
 
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>
+
diff --git a/examples/multi-tenant/README.md b/examples/multi-tenant/README.md
new file mode 100644
index 0000000..71c9b8c
--- /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/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
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"
+        }
+    ]
+}
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>
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/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..28746e5 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 26fd307..ddf06ae 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 6fe8c59..b5361ce 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 9d25e92..843b039 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 5d700c1..08a0a33 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<KeycloakSecurityContext> principal = new KeycloakPrincipal<KeycloakSecurityContext>(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 dec12b9..1678dd9 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 81a765b..6cc3ce6 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 48b3c4e..984f4d9 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 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 5085919..295c586 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 9284884..37325a8 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 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/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/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"
+        }
+    ]
+}