Skip to content

Commit

Permalink
KEYCLOAK-779
Browse files Browse the repository at this point in the history
Adapter multi-tenancy support
  • Loading branch information
jpkrohling authored and stianst committed Oct 30, 2014
1 parent a56cef4 commit 8e764e6
Show file tree
Hide file tree
Showing 42 changed files with 1,140 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
.settings
.classpath


# NetBeans #
############
nb-configuration.xml

# Compiled source #
###################
*.com
Expand Down
7 changes: 6 additions & 1 deletion core/src/main/java/org/keycloak/KeycloakSecurityContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
Expand All @@ -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() {
Expand All @@ -48,6 +50,9 @@ public String getIdTokenString() {
return idTokenString;
}

public String getRealm() {
return realm;
}

// SERIALIZATION

Expand Down
4 changes: 3 additions & 1 deletion core/src/test/java/org/keycloak/SkeletonKeyTokenTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public void testRSA() throws Exception {

@Test
public void testSerialization() throws Exception {
String realm = "acme";
AccessToken token = createSimpleToken();
IDToken idToken = new IDToken();
idToken.setEmail("joe@email.cz");
Expand All @@ -69,7 +70,7 @@ public void testSerialization() throws Exception {
.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
Expand All @@ -96,6 +97,7 @@ public void testSerialization() throws Exception {
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();
}

Expand Down
8 changes: 8 additions & 0 deletions distribution/examples-docs-zip/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"/>
Expand Down
2 changes: 2 additions & 0 deletions docbook/reference/en/en-US/master.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -88,6 +89,7 @@ This one is short
&JavascriptAdapter;
&InstalledApplications;
&Logout;
&MultiTenancy;
</chapter>

<chapter>
Expand Down
56 changes: 56 additions & 0 deletions docbook/reference/en/en-US/modules/multi-tenancy.xml
Original file line number Diff line number Diff line change
@@ -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>
6 changes: 6 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
2 changes: 1 addition & 1 deletion examples/cors/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
32 changes: 32 additions & 0 deletions examples/multi-tenant/README.md
Original file line number Diff line number Diff line change
@@ -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``

65 changes: 65 additions & 0 deletions examples/multi-tenant/pom.xml
Original file line number Diff line number Diff line change
@@ -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>

Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading

0 comments on commit 8e764e6

Please sign in to comment.