KEYCLOAK-779
Adapter multi-tenancy support
This commit is contained in:
parent
a56cef47f2
commit
8e764e642f
42 changed files with 1140 additions and 42 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -9,6 +9,11 @@
|
|||
.settings
|
||||
.classpath
|
||||
|
||||
|
||||
# NetBeans #
|
||||
############
|
||||
nb-configuration.xml
|
||||
|
||||
# Compiled source #
|
||||
###################
|
||||
*.com
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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/**"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
56
docbook/reference/en/en-US/modules/multi-tenancy.xml
Normal file
56
docbook/reference/en/en-US/modules/multi-tenancy.xml
Normal file
|
@ -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>
|
|
@ -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`
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
32
examples/multi-tenant/README.md
Normal file
32
examples/multi-tenant/README.md
Normal file
|
@ -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
examples/multi-tenant/pom.xml
Normal file
65
examples/multi-tenant/pom.xml
Normal file
|
@ -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>
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
31
examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml
Normal file
31
examples/multi-tenant/src/main/webapp/WEB-INF/jboss-ejb3.xml
Normal file
|
@ -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>
|
29
examples/multi-tenant/src/main/webapp/WEB-INF/web.xml
Normal file
29
examples/multi-tenant/src/main/webapp/WEB-INF/web.xml
Normal file
|
@ -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>
|
57
examples/multi-tenant/tenant1-realm.json
Normal file
57
examples/multi-tenant/tenant1-realm.json
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
57
examples/multi-tenant/tenant2-realm.json
Normal file
57
examples/multi-tenant/tenant2-realm.json
Normal file
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -29,5 +29,6 @@
|
|||
<module>demo-template</module>
|
||||
<module>providers</module>
|
||||
<module>js-console</module>
|
||||
<module>multi-tenant</module>
|
||||
</modules>
|
||||
</project>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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")) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue