keycloak-aplcache
Changes
docbook/reference/en/en-US/modules/clustering.xml 217(+217 -0)
docbook/reference/en/en-US/modules/themes.xml 76(+48 -28)
forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java 23(+13 -10)
integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 3(+2 -1)
integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java 17(+10 -7)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java 41(+39 -2)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java 3(+2 -1)
model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java 4(+3 -1)
model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java 2(+1 -1)
testsuite/docker-cluster/as7/Dockerfile 13(+13 -0)
testsuite/docker-cluster/assembly.xml 16(+14 -2)
testsuite/docker-cluster/eap63/Dockerfile 13(+13 -0)
testsuite/docker-cluster/fig.yml 4(+2 -2)
testsuite/docker-cluster/fig-as7.yml 31(+31 -0)
testsuite/docker-cluster/fig-eap63.yml 31(+31 -0)
testsuite/docker-cluster/pom.xml 16(+15 -1)
testsuite/docker-cluster/README.md 30(+26 -4)
testsuite/docker-cluster/wildfly/Dockerfile 36(+12 -24)
testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java 19(+9 -10)
Details
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index acb4243..fd54ce2 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -10,14 +10,14 @@ import org.codehaus.jackson.annotate.JsonPropertyOrder;
* @version $Revision: 1 $
*/
@JsonPropertyOrder({"realm", "realm-public-key", "auth-server-url", "ssl-required",
- "resource", "credentials",
+ "resource", "public-client", "credentials",
"use-resource-role-mappings",
"enable-cors", "cors-max-age", "cors-allowed-methods",
"expose-token", "bearer-only",
"connection-pool-size",
"allow-any-hostname", "disable-trust-manager", "truststore", "truststore-password",
"client-keystore", "client-keystore-password", "client-key-password",
- "use-hostname-for-local-requests", "local-requests-scheme", "local-requests-port"
+ "auth-server-url-for-backend-requests"
})
public class AdapterConfig extends BaseAdapterConfig {
@@ -37,12 +37,8 @@ public class AdapterConfig extends BaseAdapterConfig {
protected String clientKeyPassword;
@JsonProperty("connection-pool-size")
protected int connectionPoolSize = 20;
- @JsonProperty("use-hostname-for-local-requests")
- protected boolean useHostnameForLocalRequests;
- @JsonProperty("local-requests-scheme")
- protected String localRequestsScheme = "http";
- @JsonProperty("local-requests-port")
- protected int localRequestsPort = 8080;
+ @JsonProperty("auth-server-url-for-backend-requests")
+ protected String authServerUrlForBackendRequests;
public boolean isAllowAnyHostname() {
return allowAnyHostname;
@@ -108,27 +104,11 @@ public class AdapterConfig extends BaseAdapterConfig {
this.connectionPoolSize = connectionPoolSize;
}
- public boolean isUseHostnameForLocalRequests() {
- return useHostnameForLocalRequests;
+ public String getAuthServerUrlForBackendRequests() {
+ return authServerUrlForBackendRequests;
}
- public void setUseHostnameForLocalRequests(boolean useHostnameForLocalRequests) {
- this.useHostnameForLocalRequests = useHostnameForLocalRequests;
- }
-
- public String getLocalRequestsScheme() {
- return localRequestsScheme;
- }
-
- public void setLocalRequestsScheme(String localRequestsScheme) {
- this.localRequestsScheme = localRequestsScheme;
- }
-
- public int getLocalRequestsPort() {
- return localRequestsPort;
- }
-
- public void setLocalRequestsPort(int localRequestsPort) {
- this.localRequestsPort = localRequestsPort;
+ public void setAuthServerUrlForBackendRequests(String authServerUrlForBackendRequests) {
+ this.authServerUrlForBackendRequests = authServerUrlForBackendRequests;
}
}
diff --git a/core/src/main/java/org/keycloak/util/JsonSerialization.java b/core/src/main/java/org/keycloak/util/JsonSerialization.java
index d0bf6a0..49a4502 100755
--- a/core/src/main/java/org/keycloak/util/JsonSerialization.java
+++ b/core/src/main/java/org/keycloak/util/JsonSerialization.java
@@ -17,6 +17,7 @@ import java.io.OutputStream;
public class JsonSerialization {
public static final ObjectMapper mapper = new ObjectMapper();
public static final ObjectMapper prettyMapper = new ObjectMapper();
+ public static final ObjectMapper sysPropertiesAwareMapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
static {
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
@@ -46,7 +47,15 @@ public class JsonSerialization {
}
public static <T> T readValue(InputStream bytes, Class<T> type) throws IOException {
- return mapper.readValue(bytes, type);
+ return readValue(bytes, type, false);
+ }
+
+ public static <T> T readValue(InputStream bytes, Class<T> type, boolean replaceSystemProperties) throws IOException {
+ if (replaceSystemProperties) {
+ return sysPropertiesAwareMapper.readValue(bytes, type);
+ } else {
+ return mapper.readValue(bytes, type);
+ }
}
diff --git a/core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java b/core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java
new file mode 100644
index 0000000..52f893d
--- /dev/null
+++ b/core/src/main/java/org/keycloak/util/SystemPropertiesJsonParserFactory.java
@@ -0,0 +1,51 @@
+package org.keycloak.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+
+import org.codehaus.jackson.JsonParser;
+import org.codehaus.jackson.io.IOContext;
+import org.codehaus.jackson.map.MappingJsonFactory;
+import org.codehaus.jackson.util.JsonParserDelegate;
+
+/**
+ * Provides replacing of system properties for parsed values
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SystemPropertiesJsonParserFactory extends MappingJsonFactory {
+
+ @Override
+ protected JsonParser _createJsonParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
+ JsonParser delegate = super._createJsonParser(data, offset, len, ctxt);
+ return new SystemPropertiesAwareJsonParser(delegate);
+ }
+
+ @Override
+ protected JsonParser _createJsonParser(Reader r, IOContext ctxt) throws IOException {
+ JsonParser delegate = super._createJsonParser(r, ctxt);
+ return new SystemPropertiesAwareJsonParser(delegate);
+ }
+
+ @Override
+ protected JsonParser _createJsonParser(InputStream in, IOContext ctxt) throws IOException {
+ JsonParser delegate = super._createJsonParser(in, ctxt);
+ return new SystemPropertiesAwareJsonParser(delegate);
+ }
+
+
+
+ public static class SystemPropertiesAwareJsonParser extends JsonParserDelegate {
+
+ public SystemPropertiesAwareJsonParser(JsonParser d) {
+ super(d);
+ }
+
+ @Override
+ public String getText() throws IOException {
+ String orig = super.getText();
+ return StringPropertyReplacer.replaceProperties(orig);
+ }
+ }
+}
diff --git a/core/src/test/java/org/keycloak/JsonParserTest.java b/core/src/test/java/org/keycloak/JsonParserTest.java
new file mode 100644
index 0000000..265b401
--- /dev/null
+++ b/core/src/test/java/org/keycloak/JsonParserTest.java
@@ -0,0 +1,33 @@
+package org.keycloak;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class JsonParserTest {
+
+ @Test
+ public void testParsingSystemProps() throws IOException {
+ System.setProperty("my.host", "foo");
+ System.setProperty("con.pool.size", "200");
+ System.setProperty("allow.any.hostname", "true");
+
+ InputStream is = getClass().getClassLoader().getResourceAsStream("keycloak.json");
+
+ AdapterConfig config = JsonSerialization.readValue(is, AdapterConfig.class, true);
+ Assert.assertEquals("http://foo:8080/auth", config.getAuthServerUrl());
+ Assert.assertEquals("external", config.getSslRequired());
+ Assert.assertEquals("angular-product${non.existing}", config.getResource());
+ Assert.assertTrue(config.isPublicClient());
+ Assert.assertTrue(config.isAllowAnyHostname());
+ Assert.assertEquals(100, config.getCorsMaxAge());
+ Assert.assertEquals(200, config.getConnectionPoolSize());
+ }
+}
diff --git a/core/src/test/resources/keycloak.json b/core/src/test/resources/keycloak.json
new file mode 100644
index 0000000..b0a8935
--- /dev/null
+++ b/core/src/test/resources/keycloak.json
@@ -0,0 +1,9 @@
+{
+ "auth-server-url" : "http://${my.host}:8080/auth",
+ "ssl-required" : "external",
+ "resource" : "angular-product${non.existing}",
+ "public-client" : true,
+ "allow-any-hostname": "${allow.any.hostname}",
+ "cors-max-age": 100,
+ "connection-pool-size": "${con.pool.size}"
+}
\ No newline at end of file
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index 39515ef..3487d3c 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -32,6 +32,7 @@
<!ENTITY ExportImport SYSTEM "modules/export-import.xml">
<!ENTITY ServerCache SYSTEM "modules/cache.xml">
<!ENTITY SecurityVulnerabilities SYSTEM "modules/security-vulnerabilities.xml">
+ <!ENTITY Clustering SYSTEM "modules/clustering.xml">
]>
<book>
@@ -121,6 +122,7 @@ This one is short
&ExportImport;
&ServerCache;
&SecurityVulnerabilities;
+ &Clustering;
&Migration;
</book>
diff --git a/docbook/reference/en/en-US/modules/adapter-config.xml b/docbook/reference/en/en-US/modules/adapter-config.xml
index c5cb55a..8f66847 100755
--- a/docbook/reference/en/en-US/modules/adapter-config.xml
+++ b/docbook/reference/en/en-US/modules/adapter-config.xml
@@ -19,10 +19,10 @@
"expose-token" : true,
"credentials" : {
"secret" : "234234-234234-234234"
- }
+ },
"connection-pool-size" : 20,
- "disable-trust-manager" false,
+ "disable-trust-manager": false,
"allow-any-hostname" : false,
"truststore" : "path/to/truststore.jks",
"truststore-password" : "geheim",
docbook/reference/en/en-US/modules/clustering.xml 217(+217 -0)
diff --git a/docbook/reference/en/en-US/modules/clustering.xml b/docbook/reference/en/en-US/modules/clustering.xml
new file mode 100755
index 0000000..613316f
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/clustering.xml
@@ -0,0 +1,217 @@
+<chapter id="clustering">
+ <title>Clustering</title>
+
+ <para>To improve availability and scalability Keycloak can be deployed in a cluster.</para>
+
+ <para>It's fairly straightforward to configure a Keycloak cluster, the steps required are:
+ <itemizedlist>
+ <listitem>
+ <para>
+ Configure a shared database
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Configure Infinispan
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Enable realm and user cache invalidation
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Enable distributed user sessions
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Start in HA mode
+ </para>
+ </listitem>
+ </itemizedlist>
+ </para>
+
+ <section>
+ <title>Configure a shared database</title>
+ <para>
+ Keycloak doesn't replicate realms and users, but instead relies on all nodes using the same
+ database. This can be a relational database or Mongo. To make sure your database doesn't become a single
+ point of failure you may also want to deploy your database to a cluster.
+ </para>
+ </section>
+
+ <section>
+ <title id="cluster-configure-infinispan">Configure Infinispan</title>
+ <para>
+ Keycloak uses <ulink url="http://www.infinispan.org/">Infinispan</ulink> caches to share information between nodes.
+ </para>
+ <para>
+ For realm and users Keycloak uses a invalidation cache. An invalidation cache doesn't share any data, but simply
+ removes stale data from remote caches. This reduces network traffic, as well as preventing sensitive data (such as
+ realm keys and password hashes) from being sent between the nodes.
+ </para>
+ <para>
+ User sessions supports either distributed caches or fully replicated caches. We recommend using a distributed
+ cache.
+ </para>
+ <para>
+ To configure the required Infinspan caches open <literal>standalone/configuration/standalone-ha.xml</literal> and add:
+<programlisting>
+<![CDATA[
+<subsystem xmlns="urn:jboss:domain:infinispan:2.0">
+ <cache-container name="keycloak" jndi-name="infinispan/Keycloak" start="EAGER">
+ <invalidation-cache name="realms" mode="SYNC"/>
+ <invalidation-cache name="users" mode="SYNC"/>
+ <distributed-cache name="sessions" mode="SYNC" owners="1" />
+ </cache-container>
+ ...
+</subsystem>
+]]>
+</programlisting>
+ </para>
+ <para>
+ For more advanced options refer to the
+ <ulink url="http://docs.jboss.org/author/display/WFLY8/Infinispan+Subsystem">Infinispan Subsystem</ulink>
+ and
+ <ulink url="http://www.infinispan.org/docs/6.0.x/user_guide/user_guide.html">Infinispan</ulink>
+ documentation.
+ </para>
+ <para>
+ Next open <literal>standalone/configuration/keycloak-server.json</literal> and add:
+<programlisting>
+"connectionsInfinispan": {
+ "default" : {
+ "cacheContainer" : "java:jboss/infinispan/Keycloak"
+ }
+}
+</programlisting>
+ </para>
+ </section>
+
+ <section>
+ <title>Enable realm and user cache invalidation</title>
+ <para>
+ To reduce number of requests to the database Keycloak caches realm and user data. In cluster mode
+ Keycloak uses an Infinispan invalidation cache to make sure all nodes re-load data from the database
+ when it is changed. Using an invalidation cache instead of a replicated cache reduces the network traffic
+ generated by the cluster, but more importantly prevents sensitive data from being sent.
+ </para>
+ <para>
+ To enable realm and user cache invalidation open <literal>keycloak-server.json</literal> and change
+ the <literal>realmCache</literal> and <literal>userCache</literal> providers to <literal>infinispan</literal>:
+<programlisting>
+"realmCache": {
+ "provider": "infinispan"
+},
+
+"userCache": {
+ "provider": "infinispan"
+}
+</programlisting>
+ </para>
+ </section>
+
+ <section>
+ <title>Enable distributed user sessions</title>
+ <para>
+ To help distribute the load of user sessions Keycloak uses an Infinispan distributed cache. A distributed
+ cache splits user sessions into segments where each node holds one or more segment. It is possible
+ to replicate each segment to multiple nodes, but this is not strictly necessary since the failure of a node
+ will only result in users having to log in again. If you need to prevent node failures from requiring users to
+ log in again, set the <literal>owners</literal> attribute to 2 or more for the <literal>sessions</literal> cache
+ (see <link linkend='cluster-configure-infinispan'>Configure Infinispan</link>).
+ </para>
+ <para>
+ To enable the Infinispan user sessions provider open <literal>keycloak-server.json</literal> and change the
+ userSessions provider to <literal>infinispan</literal>:
+<programlisting>
+"userSessions": {
+ "provider": "infinispan"
+}
+</programlisting>
+ </para>
+ </section>
+
+ <section>
+ <title>Start in HA mode</title>
+ <para>
+ To start the server in HA mode, start it with:
+ <programlisting># bin/standalone --server-config=standalone-ha.xml</programlisting>
+ </para>
+ <para>
+ Alternatively you can copy <literal>standalone/config/standalone-ha.xml</literal> to <literal>standalone/config/standalone.xml</literal>
+ to make it the default server config.
+ </para>
+ </section>
+
+ <section>
+ <title>Enabling cluster security</title>
+ <para>
+ By default there's nothing to prevent unauthorized nodes from joining the cluster and sending potentially malicious
+ messages to the cluster. However, as there's no sensitive data sent there's not much that can be achieved.
+ For realms and users all that can be done is to send invalidation messages to make nodes load data from the
+ database more frequently. For user sessions it would be possible to modify existing user sessions, but creating
+ new sessions would have no affect as they would not be linked to any access tokens. There's not to much that
+ can be achieved by modifying user sessions. For example it would be possible to prevent sessions from expiring,
+ by changing the creation time. However, it would for example have no effect adding additional permissions to the
+ sessions as these are rechecked against the user and application when the token is created or refreshed.
+ </para>
+ <para>
+ In either case your cluster nodes should be in a private network, with a firewall protecting them from outside
+ attacks. Ideally isolated from workstations and laptops. You can also enable encryption of cluster messages,
+ this could for example be useful if you can't isolate cluster nodes from workstations and laptops on your private
+ network. However, encryption will obviously come at a cost of reduced performance.
+ </para>
+ <para>
+ To enable encryption of cluster messages you first have to create a shared keystore (change the key and store passwords!):
+<programlisting>
+<![CDATA[
+# keytool -genseckey -alias keycloak -keypass <PASSWORD> -storepass <PASSWORD> \
+ -keyalg Blowfish -keysize 56 -keystore defaultStore.keystore -storetype JCEKS
+]]>
+</programlisting>
+ </para>
+ <para>
+ Copy this keystore to all nodes (for example to standalone/configuration). Then configure JGroups to encrypt all
+ messages by adding the <literal>ENCRYPT</literal> protocol to the JGroups sub-system (this should be added after
+ the <literal>pbcast.GMS</literal> protocol):
+<programlisting>
+<![CDATA[
+<subsystem xmlns="urn:jboss:domain:jgroups:2.0" default-stack="udp">
+ <stack name="udp">
+ ...
+ <protocol type="pbcast.GMS"/>
+ <protocol type="ENCRYPT">
+ <property name="key_store_name">
+ ${jboss.server.config.dir}/defaultStore.keystore
+ </property>
+ <property name="key_password">PASSWORD</property>
+ <property name="store_password">PASSWORD</property>
+ <property name="alias">keycloak</property>
+ </protocol>
+ ...
+ </stack>
+ <stack name="tcp">
+ ...
+ <protocol type="pbcast.GMS"/>
+ <protocol type="ENCRYPT">
+ <property name="key_store_name">
+ ${jboss.server.config.dir}/defaultStore.keystore
+ </property>
+ <property name="key_password">PASSWORD</property>
+ <property name="store_password">PASSWORD</property>
+ <property name="alias">keycloak</property>
+ </protocol>
+ ...
+ </stack>
+ ...
+</subsystem>
+]]>
+</programlisting>
+ See the <ulink url="http://www.jgroups.org/manual/index.html#ENCRYPT">JGroups manual</ulink> for more details.
+ </para>
+ </section>
+
+</chapter>
diff --git a/docbook/reference/en/en-US/modules/server-installation.xml b/docbook/reference/en/en-US/modules/server-installation.xml
index aadfba8..9e595a1 100755
--- a/docbook/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/reference/en/en-US/modules/server-installation.xml
@@ -122,7 +122,7 @@ keycloak-war-dist-all-&project.version;/
</para>
</section>
<section>
- <title>Configuring the Server</title>
+ <title id="configure-server">Configuring the Server</title>
<para>
Although the Keycloak Server is designed to run out of the box, there's some things you'll need
to configure before you go into production. Specifically:
docbook/reference/en/en-US/modules/themes.xml 76(+48 -28)
diff --git a/docbook/reference/en/en-US/modules/themes.xml b/docbook/reference/en/en-US/modules/themes.xml
index 4d1e621..af30453 100755
--- a/docbook/reference/en/en-US/modules/themes.xml
+++ b/docbook/reference/en/en-US/modules/themes.xml
@@ -7,11 +7,37 @@
</para>
<section>
+ <title>Theme types</title>
+ <para>
+ There are several types of themes in Keycloak:
+ <itemizedlist>
+ <listitem>Account - Account management</listitem>
+ <listitem>Admin - Admin console</listitem>
+ <listitem>Common - Shared resources for themes</listitem>
+ <listitem>Email - Emails</listitem>
+ <listitem>Login - Login forms</listitem>
+ <listitem>Welcome - Welcome pages</listitem>
+ </itemizedlist>
+ </para>
+ </section>
+
+ <section>
<title>Configure theme</title>
<para>
- To configure the theme used by a realm open the <literal>Keycloak Admin Console</literal>, select your realm
+ All theme types, except welcome, is configured through <literal>Keycloak Admin Console</literal>. To change
+ the theme used for a realm open the open the <literal>Keycloak Admin Console</literal>, select your realm
from the drop-down box in the top left corner. Under <literal>Settings</literal> click on <literal>Theme</literal>.
</para>
+ <para>
+ To change the welcome theme you need to edit <literal>standalone/configuration/keycloak-server.json</literal>
+ and add <literal>welcomeTheme</literal> to the theme element, for example:
+<programlisting>
+"theme": {
+ ...
+ "welcomeTheme": "custom-theme"
+}
+</programlisting>
+ </para>
</section>
<section>
@@ -26,25 +52,14 @@
<section>
<title>Creating a theme</title>
<para>
- There are several types of themes in Keycloak:
- <itemizedlist>
- <listitem>Account - Account management</listitem>
- <listitem>Admin - Admin console</listitem>
- <listitem>Common - Shared resources for themes</listitem>
- <listitem>Email - Emails</listitem>
- <listitem>Login - Login forms</listitem>
- </itemizedlist>
- </para>
-
- <para>
A theme consists of:
<itemizedlist>
- <listitem><para><ulink url="http://freemarker.org">FreeMarker</ulink> templates</para></listitem>
- <listitem><para>Stylesheets</para></listitem>
- <listitem><para>Scripts</para></listitem>
- <listitem><para>Images</para></listitem>
- <listitem><para>Message bundles</para></listitem>
- <listitem><para>Theme properties</para></listitem>
+ <listitem><ulink url="http://freemarker.org">FreeMarker</ulink> templates</listitem>
+ <listitem>Stylesheets</listitem>
+ <listitem>Scripts</listitem>
+ <listitem>Images</listitem>
+ <listitem>Message bundles</listitem>
+ <listitem>Theme properties</listitem>
</itemizedlist>
</para>
<para>
@@ -132,10 +147,9 @@
<section>
<title>Theme SPI</title>
<para>
- The Theme SPI allows creating different mechanisms to providing themes for the default FreeMarker based
+ The Theme SPI allows creating different mechanisms to load themes for the default FreeMarker based
implementations of login forms and account management. To create a theme provider you will need to implement
- <literal>org.keycloak.freemarker.ThemeProvider</literal> and <literal>org.keycloak.freemarker.Theme</literal> in
- <literal>forms/common-freemarker</literal>.
+ <literal>org.keycloak.freemarker.ThemeProviderFactory</literal> and <literal>org.keycloak.freemarker.ThemeProvider</literal>.
</para>
<para>
Keycloak comes with two theme providers, one that loads themes from the classpath (used by default themes)
@@ -149,12 +163,15 @@
<para>
The Account SPI allows implementing the account management pages using whatever web framework or templating
engine you want. To create an Account provider implement <literal>org.keycloak.account.AccountProviderFactory</literal>
- and <literal>org.keycloak.account.AccountProvider</literal> in <literal>forms/account-api</literal>.
+ and <literal>org.keycloak.account.AccountProvider</literal>.
</para>
<para>
- Keycloaks default account management provider is built on the FreeMarker template engine (<literal>forms/account-freemarker</literal>).
- To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-account-freemarker-&project.version;.jar</literal>
- or disable it with the system property <literal>org.keycloak.account.freemarker.FreeMarkerAccountProviderFactory</literal>.
+ Once you have deployed your account provider to Keycloak you need to configure <literal>keycloak-server.json</literal>to specify which provider should be used:
+<programlisting>
+"account": {
+ "provider": "custom-provider"
+}
+</programlisting>
</para>
</section>
<section>
@@ -165,9 +182,12 @@
and <literal>org.keycloak.login.LoginFormsProvider</literal> in <literal>forms/login-api</literal>.
</para>
<para>
- Keycloaks default login forms provider is built on the FreeMarker template engine (<literal>forms/login-freemarker</literal>).
- To make sure your provider is loaded you will either need to delete <literal>standalone/deployments/auth-server.war/WEB-INF/lib/keycloak-login-freemarker-&project.version;.jar</literal>
- or disable it with the system property <literal>org.keycloak.login.freemarker.FreeMarkerLoginFormsProviderFactory</literal>.
+ Once you have deployed your account provider to Keycloak you need to configure <literal>keycloak-server.json</literal>to specify which provider should be used:
+<programlisting>
+"login": {
+ "provider": "custom-provider"
+}
+</programlisting>
</para>
</section>
</section>
diff --git a/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json
index ca0707e..c2241b3 100755
--- a/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json
@@ -7,6 +7,5 @@
"expose-token": true,
"credentials": {
"secret": "password"
- },
- "use-hostname-for-local-requests": false
+ }
}
diff --git a/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json
index c1ae517..0a86c04 100755
--- a/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json
@@ -6,6 +6,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
- },
- "use-hostname-for-local-requests": false
+ }
}
diff --git a/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
index 14bbd79..559df05 100755
--- a/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
@@ -5,6 +5,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
- },
- "use-hostname-for-local-requests": false
+ }
}
\ No newline at end of file
diff --git a/examples/demo-template/third-party-cdi/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/third-party-cdi/src/main/webapp/WEB-INF/keycloak.json
index 14bbd79..559df05 100755
--- a/examples/demo-template/third-party-cdi/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/demo-template/third-party-cdi/src/main/webapp/WEB-INF/keycloak.json
@@ -5,6 +5,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
- },
- "use-hostname-for-local-requests": false
+ }
}
\ No newline at end of file
diff --git a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
index eacf266..533b631 100644
--- a/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
+++ b/forms/common-freemarker/src/main/java/org/keycloak/freemarker/ExtendingThemeManager.java
@@ -1,5 +1,6 @@
package org.keycloak.freemarker;
+import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
@@ -21,17 +22,17 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class ExtendingThemeManager implements ThemeProvider {
+ private static final Logger log = Logger.getLogger(ExtendingThemeManager.class);
+
private final KeycloakSession session;
private final ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache;
private List<ThemeProvider> providers;
private String defaultTheme;
- private int staticMaxAge;
public ExtendingThemeManager(KeycloakSession session, ConcurrentHashMap<ExtendingThemeManagerFactory.ThemeKey, Theme> themeCache) {
this.session = session;
this.themeCache = themeCache;
this.defaultTheme = Config.scope("theme").get("default", "keycloak");
- this.staticMaxAge = Config.scope("theme").getInt("staticMaxAge", -1);
}
private List<ThemeProvider> getProviders() {
@@ -57,10 +58,6 @@ public class ExtendingThemeManager implements ThemeProvider {
return providers;
}
- public int getStaticMaxAge() {
- return staticMaxAge;
- }
-
@Override
public int getProviderPriority() {
return 0;
@@ -77,7 +74,13 @@ public class ExtendingThemeManager implements ThemeProvider {
Theme theme = themeCache.get(key);
if (theme == null) {
theme = loadTheme(name, type);
- if (themeCache.putIfAbsent(key, theme) != null) {
+ if (theme == null) {
+ theme = loadTheme("keycloak", type);
+ if (theme == null) {
+ theme = loadTheme("base", type);
+ }
+ log.errorv("Failed to find {0} theme {1}, using built-in themes", type, name);
+ } else if (themeCache.putIfAbsent(key, theme) != null) {
theme = themeCache.get(key);
}
}
@@ -89,7 +92,7 @@ public class ExtendingThemeManager implements ThemeProvider {
private Theme loadTheme(String name, Theme.Type type) throws IOException {
Theme theme = findTheme(name, type);
- if (theme.getParentName() != null) {
+ if (theme != null && theme.getParentName() != null) {
List<Theme> themes = new LinkedList<Theme>();
themes.add(theme);
@@ -144,11 +147,11 @@ public class ExtendingThemeManager implements ThemeProvider {
try {
return p.getTheme(name, type);
} catch (IOException e) {
- throw new RuntimeException("Failed to create " + type.toString().toLowerCase() + " theme", e);
+ log.errorv(e, p.getClass() + " failed to load theme, type={0}, name={1}", type, name);
}
}
}
- throw new RuntimeException(type.toString().toLowerCase() + " theme '" + name + "' not found");
+ return null;
}
public static class ExtendingTheme implements Theme {
diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
index 6c711d3..9dfa90a 100755
--- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
+++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
@@ -38,4 +38,6 @@ socialRedirectError=Failed to redirect to social provider
socialProviderRemoved=Social provider removed successfully
accountDisabled=Account is disabled, contact admin\
-accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
\ No newline at end of file
+accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
+
+logOutAllSessions=Log out all sessions
diff --git a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl
index 2fa5400..85c7a16 100755
--- a/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl
+++ b/forms/common-themes/src/main/resources/theme/account/base/sessions.ftl
@@ -42,6 +42,6 @@
</table>
- <a id="logout-all-sessions" href="${url.sessionsLogoutUrl}">Logout all sessions</a>
+ <a id="logout-all-sessions" href="${url.sessionsLogoutUrl}">${rb.logOutAllSessions}</a>
-</@layout.mainLayout>
\ No newline at end of file
+</@layout.mainLayout>
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 9709294..db284b1 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -7,7 +7,6 @@ import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.enums.SslRequired;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.KeycloakUriBuilder;
-import org.keycloak.util.UriUtils;
import java.net.URI;
import java.security.PublicKey;
@@ -87,15 +86,18 @@ public class KeycloakDeployment {
URI uri = URI.create(authServerBaseUrl);
if (uri.getHost() == null) {
- if (config.isUseHostnameForLocalRequests()) {
+ String authServerURLForBackendReqs = config.getAuthServerUrlForBackendRequests();
+ if (authServerURLForBackendReqs != null) {
relativeUrls = RelativeUrlsUsed.BROWSER_ONLY;
- KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(authServerBaseUrl);
- serverBuilder.host(UriUtils.getHostName()).port(config.getLocalRequestsPort()).scheme(config.getLocalRequestsScheme());
+ KeycloakUriBuilder serverBuilder = KeycloakUriBuilder.fromUri(authServerURLForBackendReqs);
+ if (serverBuilder.getHost() == null || serverBuilder.getScheme() == null) {
+ throw new IllegalStateException("Relative URL not supported for auth-server-url-for-backend-requests option. URL used: "
+ + authServerURLForBackendReqs + ", Client: " + config.getResource());
+ }
resolveNonBrowserUrls(serverBuilder);
} else {
relativeUrls = RelativeUrlsUsed.ALL_REQUESTS;
- return;
}
} else {
// We have absolute URI in config
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 0a3ab33..5aa996b 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -6,6 +6,7 @@ import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.PemUtils;
+import org.keycloak.util.SystemPropertiesJsonParserFactory;
import java.io.IOException;
import java.io.InputStream;
@@ -79,7 +80,7 @@ public class KeycloakDeploymentBuilder {
}
public static KeycloakDeployment build(InputStream is) {
- ObjectMapper mapper = new ObjectMapper();
+ ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
AdapterConfig adapterConfig = null;
try {
diff --git a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
index e1ccd5d..e9013fd 100755
--- a/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
+++ b/integration/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClientBuilder.java
@@ -7,7 +7,6 @@ import org.keycloak.enums.RelativeUrlsUsed;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.KeycloakUriBuilder;
-import org.keycloak.util.UriUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -25,7 +24,7 @@ public class ServletOAuthClientBuilder {
public static AdapterConfig getAdapterConfig(InputStream is) {
try {
- return JsonSerialization.readValue(is, AdapterConfig.class);
+ return JsonSerialization.readValue(is, AdapterConfig.class, true);
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -57,13 +56,17 @@ public class ServletOAuthClientBuilder {
String authUrl = serverBuilder.clone().path(ServiceUrlConstants.TOKEN_SERVICE_LOGIN_PATH).build(adapterConfig.getRealm()).toString();
- KeycloakUriBuilder tokenUrlBuilder = serverBuilder.clone();
- KeycloakUriBuilder refreshUrlBuilder = serverBuilder.clone();
+ KeycloakUriBuilder tokenUrlBuilder;
+ KeycloakUriBuilder refreshUrlBuilder;
if (useRelative == RelativeUrlsUsed.BROWSER_ONLY) {
// Use absolute URI for refreshToken and codeToToken requests
- tokenUrlBuilder.scheme(adapterConfig.getLocalRequestsScheme()).host(UriUtils.getHostName()).port(adapterConfig.getLocalRequestsPort());
- refreshUrlBuilder.scheme(adapterConfig.getLocalRequestsScheme()).host(UriUtils.getHostName()).port(adapterConfig.getLocalRequestsPort());
+ KeycloakUriBuilder nonBrowsersServerBuilder = KeycloakUriBuilder.fromUri(adapterConfig.getAuthServerUrlForBackendRequests());
+ tokenUrlBuilder = nonBrowsersServerBuilder.clone();
+ refreshUrlBuilder = nonBrowsersServerBuilder.clone();
+ } else {
+ tokenUrlBuilder = serverBuilder.clone();
+ refreshUrlBuilder = serverBuilder.clone();
}
String tokenUrl = tokenUrlBuilder.path(ServiceUrlConstants.TOKEN_SERVICE_ACCESS_CODE_PATH).build(adapterConfig.getRealm()).toString();
String refreshUrl = refreshUrlBuilder.path(ServiceUrlConstants.TOKEN_SERVICE_REFRESH_PATH).build(adapterConfig.getRealm()).toString();
@@ -74,7 +77,7 @@ public class ServletOAuthClientBuilder {
private static RelativeUrlsUsed relativeUrls(KeycloakUriBuilder serverBuilder, AdapterConfig adapterConfig) {
if (serverBuilder.clone().getHost() == null) {
- return (adapterConfig.isUseHostnameForLocalRequests()) ? RelativeUrlsUsed.BROWSER_ONLY : RelativeUrlsUsed.ALL_REQUESTS;
+ return (adapterConfig.getAuthServerUrlForBackendRequests() != null) ? RelativeUrlsUsed.BROWSER_ONLY : RelativeUrlsUsed.ALL_REQUESTS;
} else {
return RelativeUrlsUsed.NEVER;
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java
index b7112d7..f39fe6a 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowUserSessionManagement.java
@@ -19,6 +19,7 @@ package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticatedSessionManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
+import io.undertow.server.session.SessionConfig;
import io.undertow.server.session.SessionListener;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler;
@@ -108,7 +109,7 @@ public class UndertowUserSessionManagement implements SessionListener {
log.debug("invalidating session for user: " + user);
String sessionId = entry.getKey();
String keycloakSessionId = entry.getValue();
- Session session = manager.getSession(sessionId);
+ Session session = getSessionById(manager, sessionId);
try {
session.invalidate(null);
} catch (Exception e) {
@@ -131,7 +132,7 @@ public class UndertowUserSessionManagement implements SessionListener {
}
sessions.httpSessionToKeycloakSession.remove(sessionId);
- Session session = manager.getSession(sessionId);
+ Session session = getSessionById(manager, sessionId);
try {
session.invalidate(null);
} catch (Exception e) {
@@ -142,6 +143,41 @@ public class UndertowUserSessionManagement implements SessionListener {
}
}
+ protected Session getSessionById(SessionManager manager, final String sessionId) {
+ // TODO: Workaround for WFLY-3345. Remove this once we move to wildfly 8.2
+ if (manager.getClass().getName().equals("org.wildfly.clustering.web.undertow.session.DistributableSessionManager")) {
+ return manager.getSession(null, new SessionConfig() {
+
+ @Override
+ public void setSessionId(HttpServerExchange exchange, String sessionId) {
+ }
+
+ @Override
+ public void clearSession(HttpServerExchange exchange, String sessionId) {
+ }
+
+ @Override
+ public String findSessionId(HttpServerExchange exchange) {
+ return sessionId;
+ }
+
+ @Override
+ public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) {
+ return null;
+ }
+
+ @Override
+ public String rewriteUrl(String originalUrl, String sessionId) {
+ return null;
+ }
+
+ });
+
+ } else {
+ return manager.getSession(sessionId);
+ }
+ }
+
@Override
public void sessionCreated(Session session, HttpServerExchange exchange) {
@@ -151,6 +187,7 @@ public class UndertowUserSessionManagement implements SessionListener {
public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
// Look up the single session id associated with this session (if any)
String username = getUsernameFromSession(session);
+ log.debugf("Session destroyed for user: %s, sessionId: %s", username, session.getId());
if (username == null) return;
String sessionId = session.getId();
UserSessions userSessions = userSessionMap.get(username);
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
index 033fe53..60bcb59 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java
@@ -395,7 +395,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
throw new IllegalStateException("Can't remove session: task in progress for session");
}
} else {
- tasks.put(key, new CacheTask(cache, CacheOperation.ADD, key, value));
+ tasks.put(key, new CacheTask(cache, CacheOperation.REPLACE, key, value));
}
}
@@ -426,6 +426,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
break;
case REPLACE:
cache.replace(key, value);
+ break;
}
}
}
diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
index 7329d3f..369b7a6 100644
--- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
+++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/mapreduce/ClientSessionMapper.java
@@ -80,7 +80,9 @@ public class ClientSessionMapper implements Mapper<String, SessionEntity, String
collector.emit(key, entity);
break;
case USER_SESSION_AND_TIMESTAMP:
- collector.emit(entity.getUserSession(), entity.getTimestamp());
+ if (entity.getUserSession() != null) {
+ collector.emit(entity.getUserSession(), entity.getTimestamp());
+ }
break;
}
}
diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
index 26ddb0a..b43f827 100755
--- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
+++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/MemUserSessionProvider.java
@@ -121,7 +121,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
for (ClientSessionEntity s : clientSessions.values()) {
String realmId = realm.getId();
String clientId = client.getId();
- if (s.getSession().getRealm().equals(realmId) && s.getClientId().equals(clientId)) {
+ if (s.getSession() != null && s.getSession().getRealm().equals(realmId) && s.getClientId().equals(clientId)) {
if (!userSessionEntities.contains(s.getSession())) {
userSessionEntities.add(s.getSession());
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
index b310403..b88648e 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OpenIDConnectService.java
@@ -28,6 +28,7 @@ import org.keycloak.models.OAuthClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.RefreshToken;
@@ -276,7 +277,7 @@ public class OpenIDConnectService {
}
event.detail(Details.USERNAME, username);
- UserModel user = session.users().getUserByUsername(username, realm);
+ UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, username);
if (user != null) event.user(user);
ClientModel client = authorizeClient(authorizationHeader, form, event);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index 761b69f..33485b0 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -99,7 +99,7 @@ public class TokenManager {
RefreshToken refreshToken = null;
try {
if (!RSAProvider.verify(jws, realm.getPublicKey())) {
- throw new RuntimeException("Invalid refresh token");
+ throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token");
}
refreshToken = jws.readJsonContent(RefreshToken.class);
} catch (IOException e) {
diff --git a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
index 8b55f00..105751f 100755
--- a/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ResourceAdminManager.java
@@ -23,6 +23,7 @@ import org.keycloak.representations.adapters.action.UserStats;
import org.keycloak.representations.adapters.action.UserStatsAction;
import org.keycloak.services.util.HttpClientBuilder;
import org.keycloak.services.util.ResolveRelative;
+import org.keycloak.util.StringPropertyReplacer;
import org.keycloak.util.Time;
import javax.ws.rs.core.MediaType;
@@ -108,8 +109,10 @@ public class ResourceAdminManager {
}
// this is to support relative admin urls when keycloak and applications are deployed on the same machine
- return ResolveRelative.resolveRelativeUri(requestUri, mgmtUrl);
+ String absoluteURI = ResolveRelative.resolveRelativeUri(requestUri, mgmtUrl);
+ // this is for resolving URI like "http://${jboss.home.name}:8080/..." in order to send request to same machine and avoid LB in cluster env
+ return StringPropertyReplacer.replaceProperties(absoluteURI);
}
public UserStats getUserStats(URI requestUri, RealmModel realm, ApplicationModel application, UserModel user) {
@@ -242,7 +245,8 @@ public class ResourceAdminManager {
try {
response = request.body(MediaType.TEXT_PLAIN_TYPE, token).post(UserStats.class);
} catch (Exception e) {
- throw new RuntimeException(e);
+ logger.warn("Logout for application '" + resource.getName() + "' failed", e);
+ return false;
}
try {
boolean success = response.getStatus() == 204;
testsuite/docker-cluster/as7/Dockerfile 13(+13 -0)
diff --git a/testsuite/docker-cluster/as7/Dockerfile b/testsuite/docker-cluster/as7/Dockerfile
new file mode 100644
index 0000000..f216f84
--- /dev/null
+++ b/testsuite/docker-cluster/as7/Dockerfile
@@ -0,0 +1,13 @@
+FROM mposolda/as7
+
+ADD keycloak-as7-trigger.sh /keycloak-as7-trigger.sh
+RUN chmod u+x /keycloak-as7-trigger.sh
+
+ENV JBOSS_HOME /opt/as7
+ENV JBOSS_MODULES_HOME $JBOSS_HOME/modules
+ENV JBOSS_TYPE as7
+ENV NODE_PREFIX as
+
+EXPOSE 8787
+
+CMD [ "/keycloak-as7-trigger.sh" ]
\ No newline at end of file
diff --git a/testsuite/docker-cluster/as7/keycloak-as7-trigger.sh b/testsuite/docker-cluster/as7/keycloak-as7-trigger.sh
new file mode 100644
index 0000000..a59408c
--- /dev/null
+++ b/testsuite/docker-cluster/as7/keycloak-as7-trigger.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-run-node.sh
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-base-prepare.sh
+chmod u+x /keycloak-docker-cluster/shared-files/deploy-examples.sh
+
+echo "Permissions changed. Triggering keycloak-run-node.sh"
+/keycloak-docker-cluster/shared-files/keycloak-run-node.sh
diff --git a/testsuite/docker-cluster/as7-image/Dockerfile b/testsuite/docker-cluster/as7-image/Dockerfile
new file mode 100644
index 0000000..6c17097
--- /dev/null
+++ b/testsuite/docker-cluster/as7-image/Dockerfile
@@ -0,0 +1,32 @@
+FROM jboss/wildfly
+
+USER root
+
+# Update yum and install required programs
+RUN yum install -y unzip && yum install -y wget && yum install -y mc && yum -y install nc
+RUN yum clean all
+
+# Download mysql driver
+RUN cd /tmp
+RUN wget http://search.maven.org/remotecontent?filepath=mysql/mysql-connector-java/5.1.32/mysql-connector-java-5.1.32.jar
+RUN mv *.jar mysql-connector-java-5.1.32.jar
+
+# Drop wildfly
+RUN rm -rf /opt/wildfly*
+
+ENV AS7_VERSION 7.1.1.Final
+
+# Download and unpack AS7 distribution
+RUN cd /opt
+RUN wget http://download.jboss.org/jbossas/7.1/jboss-as-$AS7_VERSION/jboss-as-$AS7_VERSION.zip
+RUN sleep 3
+RUN unzip -q jboss-as-$AS7_VERSION.zip
+
+# Make sure the distribution is available from a well-known place
+RUN mv jboss-as-$AS7_VERSION /opt/as7
+
+RUN rm -rf jboss-as-$AS7_VERSION.zip
+
+EXPOSE 8787
+
+CMD [ "/bin/bash" ]
testsuite/docker-cluster/assembly.xml 16(+14 -2)
diff --git a/testsuite/docker-cluster/assembly.xml b/testsuite/docker-cluster/assembly.xml
index d7c98cf..da0580d 100644
--- a/testsuite/docker-cluster/assembly.xml
+++ b/testsuite/docker-cluster/assembly.xml
@@ -23,8 +23,20 @@
</excludes>
</fileSet>
<fileSet>
- <directory>target/modules</directory>
- <outputDirectory>modules</outputDirectory>
+ <directory>target/wildfly-adapter</directory>
+ <outputDirectory>wildfly-adapter</outputDirectory>
+ </fileSet>
+ <fileSet>
+ <directory>target/as7-adapter</directory>
+ <outputDirectory>as7-adapter</outputDirectory>
+ </fileSet>
+ <fileSet>
+ <directory>target/eap63-adapter</directory>
+ <outputDirectory>eap63-adapter</outputDirectory>
+ </fileSet>
+ <fileSet>
+ <directory>shared-files</directory>
+ <outputDirectory>shared-files</outputDirectory>
</fileSet>
</fileSets>
</assembly>
testsuite/docker-cluster/eap63/Dockerfile 13(+13 -0)
diff --git a/testsuite/docker-cluster/eap63/Dockerfile b/testsuite/docker-cluster/eap63/Dockerfile
new file mode 100644
index 0000000..136bc38
--- /dev/null
+++ b/testsuite/docker-cluster/eap63/Dockerfile
@@ -0,0 +1,13 @@
+FROM mposolda/eap63
+
+ADD keycloak-eap63-trigger.sh /keycloak-eap63-trigger.sh
+RUN chmod u+x /keycloak-eap63-trigger.sh
+
+ENV JBOSS_HOME /opt/eap63
+ENV JBOSS_MODULES_HOME $JBOSS_HOME/modules/system/layers/base
+ENV JBOSS_TYPE eap63
+ENV NODE_PREFIX eap
+
+EXPOSE 8787
+
+CMD [ "/keycloak-eap63-trigger.sh" ]
\ No newline at end of file
diff --git a/testsuite/docker-cluster/eap63/keycloak-eap63-trigger.sh b/testsuite/docker-cluster/eap63/keycloak-eap63-trigger.sh
new file mode 100644
index 0000000..a59408c
--- /dev/null
+++ b/testsuite/docker-cluster/eap63/keycloak-eap63-trigger.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-run-node.sh
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-base-prepare.sh
+chmod u+x /keycloak-docker-cluster/shared-files/deploy-examples.sh
+
+echo "Permissions changed. Triggering keycloak-run-node.sh"
+/keycloak-docker-cluster/shared-files/keycloak-run-node.sh
diff --git a/testsuite/docker-cluster/eap63-image/Dockerfile b/testsuite/docker-cluster/eap63-image/Dockerfile
new file mode 100644
index 0000000..fa8a626
--- /dev/null
+++ b/testsuite/docker-cluster/eap63-image/Dockerfile
@@ -0,0 +1,30 @@
+FROM jboss/wildfly
+
+USER root
+
+# Update yum and install required programs
+RUN yum install -y unzip && yum install -y wget && yum install -y mc && yum -y install nc
+RUN yum clean all
+
+# Download mysql driver
+RUN cd /tmp
+RUN wget http://search.maven.org/remotecontent?filepath=mysql/mysql-connector-java/5.1.32/mysql-connector-java-5.1.32.jar
+RUN mv *.jar mysql-connector-java-5.1.32.jar
+
+# Drop wildfly
+RUN rm -rf /opt/wildfly*
+
+# Download and unpack EAP63 distribution TODO: Check if it's an issue for EAP 6.3
+RUN cd /
+RUN wget https://dl.dropboxusercontent.com/u/5525920/jboss-eap-6.3.0.zip
+RUN sleep 3
+RUN unzip -q jboss-eap-6.3.0.zip
+
+# Make sure the distribution is available from a well-known place
+RUN mv jboss-eap-6.3 /opt/eap63
+
+RUN rm -rf jboss-eap-6.3.0.zip
+
+EXPOSE 8787
+
+CMD [ "/bin/bash" ]
\ No newline at end of file
testsuite/docker-cluster/fig.yml 4(+2 -2)
diff --git a/testsuite/docker-cluster/fig.yml b/testsuite/docker-cluster/fig.yml
index 046d73b..e75c7c5 100644
--- a/testsuite/docker-cluster/fig.yml
+++ b/testsuite/docker-cluster/fig.yml
@@ -15,9 +15,9 @@ mysql:
- /apachelogs
ports:
- "33306:3306"
-node:
+wfnode:
build: wildfly
- command: /keycloak-run-node.sh
+ command: /keycloak-wildfly-trigger.sh
volumes:
- target/keycloak-docker-cluster:/keycloak-docker-cluster
volumes_from:
testsuite/docker-cluster/fig-as7.yml 31(+31 -0)
diff --git a/testsuite/docker-cluster/fig-as7.yml b/testsuite/docker-cluster/fig-as7.yml
new file mode 100644
index 0000000..9d11d69
--- /dev/null
+++ b/testsuite/docker-cluster/fig-as7.yml
@@ -0,0 +1,31 @@
+httpd:
+ build: httpd
+ ports:
+ - "8000:80"
+ - "10001:10001"
+ volumes_from:
+ - mysql
+mysql:
+ image: mysql:5.6.20
+ environment:
+ - MYSQL_ROOT_PASSWORD=mysecretpassword
+ - MYSQL_DATABASE=keycloak_db
+ volumes:
+ - /keycloak-docker-shared
+ - /apachelogs
+ ports:
+ - "33306:3306"
+asnode:
+ build: as7
+ command: /keycloak-as7-trigger.sh
+ volumes:
+ - target/keycloak-docker-cluster:/keycloak-docker-cluster
+ volumes_from:
+ - mysql
+ links:
+ - httpd:httpd
+ - mysql:mysql
+ ports:
+ - "8787"
+ - "8080"
+ - "9990"
\ No newline at end of file
testsuite/docker-cluster/fig-eap63.yml 31(+31 -0)
diff --git a/testsuite/docker-cluster/fig-eap63.yml b/testsuite/docker-cluster/fig-eap63.yml
new file mode 100644
index 0000000..b7ed7fe
--- /dev/null
+++ b/testsuite/docker-cluster/fig-eap63.yml
@@ -0,0 +1,31 @@
+httpd:
+ build: httpd
+ ports:
+ - "8000:80"
+ - "10001:10001"
+ volumes_from:
+ - mysql
+mysql:
+ image: mysql:5.6.20
+ environment:
+ - MYSQL_ROOT_PASSWORD=mysecretpassword
+ - MYSQL_DATABASE=keycloak_db
+ volumes:
+ - /keycloak-docker-shared
+ - /apachelogs
+ ports:
+ - "33306:3306"
+eapnode:
+ build: eap63
+ command: /keycloak-eap63-trigger.sh
+ volumes:
+ - target/keycloak-docker-cluster:/keycloak-docker-cluster
+ volumes_from:
+ - mysql
+ links:
+ - httpd:httpd
+ - mysql:mysql
+ ports:
+ - "8787"
+ - "8080"
+ - "9990"
\ No newline at end of file
testsuite/docker-cluster/pom.xml 16(+15 -1)
diff --git a/testsuite/docker-cluster/pom.xml b/testsuite/docker-cluster/pom.xml
index d552ddf..d3a4dbc 100644
--- a/testsuite/docker-cluster/pom.xml
+++ b/testsuite/docker-cluster/pom.xml
@@ -51,7 +51,21 @@
<artifactId>keycloak-wildfly-adapter-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
- <outputDirectory>${project.build.directory}</outputDirectory>
+ <outputDirectory>${project.build.directory}/wildfly-adapter</outputDirectory>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-as7-adapter-dist</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ <outputDirectory>${project.build.directory}/as7-adapter</outputDirectory>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-eap6-adapter-dist</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ <outputDirectory>${project.build.directory}/eap63-adapter</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
testsuite/docker-cluster/README.md 30(+26 -4)
diff --git a/testsuite/docker-cluster/README.md b/testsuite/docker-cluster/README.md
index 2aaa4bf..c51cd80 100644
--- a/testsuite/docker-cluster/README.md
+++ b/testsuite/docker-cluster/README.md
@@ -3,7 +3,7 @@ How to test Keycloak cluster with Docker
Docker+Fig allows to easily setup and test the whole environment with:
* Apache HTTPD 2.4 + modcluster 1.3 as Load Balancer
* MySQL 5.6.1 as database
-* Various number of Keycloak cluster nodes running on WildFly (with "demo" examples deployed)
+* Various number of Keycloak cluster nodes running on WildFly with "demo" examples deployed. (See below for EAP 6.3 and AS7)
You don't need to setup Apache with modcluster + MySQL on your laptop as Docker will do it for you and all will run in Docker containers.
@@ -42,7 +42,6 @@ be able to access Apache modCluster status page: [http://localhost:10001/mod_clu
with deployed "auth-server.war" and few other WARs (keycloak demo).
Also you can access Keycloak admin console via loadBalancer on [http://localhost:8000/auth/admin](http://localhost:8000/auth/admin) and similarly Account mgmt.
-TODO: Examples currently doesn't work and I am looking at it..
MySQL can be directly accessed from your machine (if you have MySQL client installed):
```shell
@@ -74,7 +73,7 @@ Scale / more cluster nodes
Run this in separate terminal to add more (in this case 2) cluster nodes:
```shell
-$ fig scale node=2
+$ fig scale wfnode=2
````
Now it should be visible on mod_cluster_manager page that they are 2 nodes.
@@ -89,7 +88,7 @@ to see output of MySql and Keycloak server consoles.
To see Apache and debug logs of keycloak server:
```shell
-$ fig run node /bin/bash
+$ fig run wfnode /bin/bash
````
Then you're in shell inside docker container, which has some mounted volumes with apache logs and keycloak nodes. Apache logs are at:
@@ -133,3 +132,26 @@ In this case you might need to stop and remove existing containers. Then start f
changed jars, then rebuild distribution and testsuite/docker-cluster
(or just copy changed JAR into $KEYCLOAK_HOME/testsuite/docker-cluster/target/keycloak-docker-cluster/deployments/auth-server.war/WEB-INF/lib if it's not adapter stuff.
But 'fig rm' is safer to call anyway)
+
+Test with Keycloak and examples on EAP 6.3
+------------------------------------------
+Steps are quite similar like for WildFly but we need to pass different file "fig-eap63.yml" instead of default "fig.yml" which is used for WildFly.
+Also name of the node is "eapnode" instead of "wfnode".
+
+So your commands will look like
+```shell
+$ fig -f fig-eap63.yml build
+$ fig -f fig-eap63.yml up
+$ fig -f fig-eap63.yml scale eapnode=2
+````
+and viceversa.
+
+Test with Keycloak and examples on AS 7.1.1
+-------------------------------------------
+Also arguments need to be passed with different fig file and node name: TODO: AS7 cluster setup doesn't work correctly yet
+
+ ```shell
+$ fig -f fig-as7.yml build
+$ fig -f fig-as7.yml up
+$ fig -f fig-as7.yml scale asnode=2
+````
\ No newline at end of file
diff --git a/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh b/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh
new file mode 100644
index 0000000..137e5c6
--- /dev/null
+++ b/testsuite/docker-cluster/shared-files/keycloak-base-prepare.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Copy MySQL driver
+cd /tmp
+mkdir -p mysql/main && mv /mysql-connector-java-5.1.32.jar mysql/main/
+cp /keycloak-docker-cluster/shared-files/mysql-module.xml mysql/main/module.xml
+mv mysql $JBOSS_MODULES_HOME/com/
+
+if [ $JBOSS_TYPE == "eap63" ]; then
+ EXT="as7";
+else
+ EXT=$JBOSS_TYPE;
+fi;
+
+sed -i -e "s/<extensions>/&\n <extension module=\"org.keycloak.keycloak-$EXT-subsystem\"\/>/" $JBOSS_HOME/standalone/configuration/standalone-ha.xml
+sed -i -e 's/<profile>/&\n <subsystem xmlns="urn:jboss:domain:keycloak:1.0"\/>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml && \
+sed -i -e 's/<security-domains>/&\n <security-domain name="keycloak">\n <authentication>\n <login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule" flag="required"\/>\n <\/authentication>\n <\/security-domain>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml && \
+sed -i -e 's/<drivers>/&\n <driver name="mysql" module="com.mysql">\n <xa-datasource-class>com.mysql.jdbc.Driver<\/xa-datasource-class>\n <driver-class>com.mysql.jdbc.Driver<\/driver-class>\n <\/driver>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml && \
+sed -i -e 's/<\/periodic-rotating-file-handler>/&\n <logger category=\"org.keycloak\">\n <level name=\"DEBUG\" \/> \n <\/logger>\n <logger category=\"org.jboss.resteasy.core.ResourceLocator\">\n <level name=\"ERROR\" \/> \n <\/logger>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml
+
+sed -i -e 's/<subsystem xmlns=\"urn:jboss:domain:infinispan:[0-9]\.[0-9]\">/&\n <cache-container name=\"keycloak\" jndi-name=\"infinispan\/Keycloak\" start=\"EAGER\"> \
+\n <transport lock-timeout=\"60000\"\/>\n <distributed-cache name=\"sessions\" mode=\"SYNC\" owners=\"2\" segments=\"60\"\/> \
+\n <invalidation-cache name=\"realms\" mode=\"SYNC\"\/>\n <\/cache-container>/' $JBOSS_HOME/standalone/configuration/standalone-ha.xml
+
+sed -i "s|<mod-cluster-config .*>|<mod-cluster-config advertise-socket=\"modcluster\" proxy-list=\"\$\{httpd.proxyList\}\" proxy-url=\"\/\" balancer=\"mycluster\" advertise=\"false\" connector=\"ajp\" sticky-session=\"true\">|" $JBOSS_HOME/standalone/configuration/standalone-ha.xml
+
+sed -i "s|#JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|" $JBOSS_HOME/bin/standalone.conf
+
+cp /keycloak-docker-cluster/shared-files/mysql-keycloak-ds.xml $JBOSS_HOME/standalone/deployments/
testsuite/docker-cluster/wildfly/Dockerfile 36(+12 -24)
diff --git a/testsuite/docker-cluster/wildfly/Dockerfile b/testsuite/docker-cluster/wildfly/Dockerfile
index 3fde5bc..6aff50b 100644
--- a/testsuite/docker-cluster/wildfly/Dockerfile
+++ b/testsuite/docker-cluster/wildfly/Dockerfile
@@ -1,37 +1,25 @@
FROM jboss/wildfly
USER root
+
+# Update yum and install required programs
RUN yum install -y unzip && yum install -y wget && yum install -y mc && yum -y install nc
RUN yum clean all
+# Download mysql driver
RUN cd /tmp
RUN wget http://search.maven.org/remotecontent?filepath=mysql/mysql-connector-java/5.1.32/mysql-connector-java-5.1.32.jar
-RUN mv *.jar mysql-connector-java-5.1.32.jar
-
-RUN mkdir -p mysql/main && mv mysql-connector-java-5.1.32.jar mysql/main/
-ADD mysql-module.xml mysql/main/module.xml
-RUN mv mysql /opt/wildfly/modules/system/layers/base/com/
-
-RUN sed -i -e 's/<extensions>/&\n <extension module="org.keycloak.keycloak-wildfly-subsystem"\/>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
-sed -i -e 's/<profile>/&\n <subsystem xmlns="urn:jboss:domain:keycloak:1.0"\/>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
-sed -i -e 's/<security-domains>/&\n <security-domain name="keycloak">\n <authentication>\n <login-module code="org.keycloak.adapters.jboss.KeycloakLoginModule" flag="required"\/>\n <\/authentication>\n <\/security-domain>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
-sed -i -e 's/<drivers>/&\n <driver name="mysql" module="com.mysql">\n <xa-datasource-class>com.mysql.jdbc.Driver<\/xa-datasource-class>\n <\/driver>/' /opt/wildfly/standalone/configuration/standalone-ha.xml && \
-sed -i -e 's/<\/periodic-rotating-file-handler>/&\n <logger category=\"org.keycloak\">\n <level name=\"DEBUG\" \/> \n <\/logger>/' /opt/wildfly/standalone/configuration/standalone-ha.xml
+RUN mv *.jar /mysql-connector-java-5.1.32.jar
-RUN sed -i -e 's/<subsystem xmlns=\"urn:jboss:domain:infinispan:2\.0\">/&\n <cache-container name=\"keycloak\" jndi-name=\"infinispan\/Keycloak\" start=\"EAGER\"> \
-\n <transport lock-timeout=\"60000\"\/>\n <distributed-cache name=\"sessions\" mode=\"SYNC\" owners=\"2\" segments=\"60\"\/> \
-\n <invalidation-cache name=\"realms\" mode=\"SYNC\"\/>\n <\/cache-container>/' /opt/wildfly/standalone/configuration/standalone-ha.xml
+ADD keycloak-wildfly-trigger.sh /keycloak-wildfly-trigger.sh
+RUN chmod u+x /keycloak-wildfly-trigger.sh
-RUN sed -i "s|<mod-cluster-config .*>|<mod-cluster-config advertise-socket=\"modcluster\" proxy-list=\"\$\{httpd.proxyList\}\" proxy-url=\"\/\" balancer=\"mycluster\" advertise=\"false\" connector=\"ajp\" sticky-session=\"true\">|" /opt/wildfly/standalone/configuration/standalone-ha.xml
-
-RUN sed -i "s|#JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|JAVA_OPTS=\"\$JAVA_OPTS -agentlib:jdwp=transport=dt_socket|" /opt/wildfly/bin/standalone.conf
-
-ADD mysql-keycloak-ds.xml /opt/wildfly/standalone/deployments/
-ADD keycloak-run-node.sh /keycloak-run-node.sh
-RUN chmod u+x /keycloak-run-node.sh
-ADD deploy-examples.sh /deploy-examples.sh
-RUN chmod u+x /deploy-examples.sh
+ENV JBOSS_HOME /opt/wildfly-8.1.0.Final
+ENV JBOSS_MODULES_HOME $JBOSS_HOME/modules/system/layers/base
+ENV JBOSS_TYPE wildfly
+ENV NODE_PREFIX wf
EXPOSE 8787
-CMD [ "/keycloak-run-node.sh" ]
+CMD [ "/keycloak-wildfly-trigger.sh" ]
+
diff --git a/testsuite/docker-cluster/wildfly/keycloak-wildfly-trigger.sh b/testsuite/docker-cluster/wildfly/keycloak-wildfly-trigger.sh
new file mode 100644
index 0000000..a59408c
--- /dev/null
+++ b/testsuite/docker-cluster/wildfly/keycloak-wildfly-trigger.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-run-node.sh
+chmod u+x /keycloak-docker-cluster/shared-files/keycloak-base-prepare.sh
+chmod u+x /keycloak-docker-cluster/shared-files/deploy-examples.sh
+
+echo "Permissions changed. Triggering keycloak-run-node.sh"
+/keycloak-docker-cluster/shared-files/keycloak-run-node.sh
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 4fe0131..5aa52c3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -453,20 +453,24 @@ public class AccountTest {
// Create second session
WebDriver driver2 = WebRule.createWebDriver();
- OAuthClient oauth2 = new OAuthClient(driver2);
- oauth2.state("mystate");
- oauth2.doLogin("view-sessions", "password");
+ try {
+ OAuthClient oauth2 = new OAuthClient(driver2);
+ oauth2.state("mystate");
+ oauth2.doLogin("view-sessions", "password");
- Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();
+ Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();
- sessionsPage.open();
- sessions = sessionsPage.getSessions();
- Assert.assertEquals(2, sessions.size());
+ sessionsPage.open();
+ sessions = sessionsPage.getSessions();
+ Assert.assertEquals(2, sessions.size());
- sessionsPage.logoutAll();
+ sessionsPage.logoutAll();
- events.expectLogout(registerEvent.getSessionId());
- events.expectLogout(login2Event.getSessionId());
+ events.expectLogout(registerEvent.getSessionId());
+ events.expectLogout(login2Event.getSessionId());
+ } finally {
+ driver2.close();
+ }
}
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
index 4001dfa..28bd4b6 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTest.java
@@ -1,429 +1,480 @@
-/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2012, Red Hat, Inc., and individual contributors
- * as indicated by the @author tags. See the copyright.txt file in the
- * distribution for a full listing of individual contributors.
- *
- * This is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as
- * published by the Free Software Foundation; either version 2.1 of
- * the License, or (at your option) any later version.
- *
- * This software 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this software; if not, write to the Free
- * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
- * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
- */
-package org.keycloak.testsuite.adapter;
-
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Rule;
-import org.junit.Test;
-import org.keycloak.Config;
-import org.keycloak.OAuth2Constants;
-import org.keycloak.Version;
-import org.keycloak.adapters.AdapterConstants;
-import org.keycloak.models.ApplicationModel;
-import org.keycloak.models.Constants;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.oidc.OpenIDConnectService;
-import org.keycloak.protocol.oidc.TokenManager;
-import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.adapters.action.SessionStats;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.services.managers.RealmManager;
-import org.keycloak.services.managers.ResourceAdminManager;
-import org.keycloak.services.resources.admin.AdminRoot;
-import org.keycloak.testsuite.OAuthClient;
-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.keycloak.util.BasicAuthHelper;
-import org.openqa.selenium.WebDriver;
-
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.Form;
-import javax.ws.rs.core.GenericType;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-import java.net.URL;
-import java.security.PublicKey;
-import java.util.Map;
-
-/**
- * Tests Undertow Adapter
- *
- * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
- */
-public class AdapterTest {
-
- public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
- public static PublicKey realmPublicKey;
- @ClassRule
- public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
- @Override
- protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
- RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
- RealmModel realm = manager.importRealm(representation);
-
- realmPublicKey = realm.getPublicKey();
-
- URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
- deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
- url = getClass().getResource("/adapter-test/secure-portal-keycloak.json");
- deployApplication("secure-portal", "/secure-portal", CallAuthenticatedServlet.class, url.getPath(), "user", false);
- url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
- deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
- url = getClass().getResource("/adapter-test/product-keycloak.json");
- deployApplication("product-portal", "/product-portal", ProductServlet.class, url.getPath(), "user");
-
- }
- };
-
- private static String createToken() {
- KeycloakSession session = keycloakRule.startSession();
- try {
- RealmManager manager = new RealmManager(session);
-
- RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
- ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
- TokenManager tm = new TokenManager();
- UserModel admin = session.users().getUserByUsername("admin", adminRealm);
- UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
- AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
- return tm.encodeToken(adminRealm, token);
- } finally {
- keycloakRule.stopSession(session, true);
- }
- }
-
-
- @Rule
- public WebRule webRule = new WebRule(this);
-
- @WebResource
- protected WebDriver driver;
-
- @WebResource
- protected OAuthClient oauth;
-
- @WebResource
- protected LoginPage loginPage;
-
- @Test
- public void testLoginSSOAndLogout() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- // test SSO
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
- pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
-
- // View stats
- String adminToken = createToken();
-
- Client client = ClientBuilder.newClient();
- UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
- WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo");
- Map<String, SessionStats> stats = adminTarget.path("session-stats").request()
- .header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
- .get(new GenericType<Map<String, SessionStats>>() {
- });
-
- SessionStats custStats = stats.get("customer-portal");
- Assert.assertNotNull(custStats);
- Assert.assertEquals(1, custStats.getActiveSessions());
- SessionStats prodStats = stats.get("product-portal");
- Assert.assertNotNull(prodStats);
- Assert.assertEquals(1, prodStats.getActiveSessions());
-
- client.close();
-
-
- // test logout
-
- String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
- .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/customer-portal").build("demo").toString();
- driver.navigate().to(logoutUri);
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- driver.navigate().to("http://localhost:8081/customer-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
-
-
- }
-
- @Test
- public void testServletRequestLogout() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- // test SSO
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
- pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
-
- // back
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
- // test logout
-
- driver.navigate().to("http://localhost:8081/customer-portal/logout");
-
-
-
- driver.navigate().to("http://localhost:8081/customer-portal");
- String currentUrl = driver.getCurrentUrl();
- Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
-
-
- }
-
- @Test
- public void testLoginSSOIdle() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- KeycloakSession session = keycloakRule.startSession();
- RealmModel realm = session.realms().getRealmByName("demo");
- int originalIdle = realm.getSsoSessionIdleTimeout();
- realm.setSsoSessionIdleTimeout(1);
- session.getTransaction().commit();
- session.close();
-
- Thread.sleep(2000);
-
-
- // test SSO
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
-
- session = keycloakRule.startSession();
- realm = session.realms().getRealmByName("demo");
- realm.setSsoSessionIdleTimeout(originalIdle);
- session.getTransaction().commit();
- session.close();
- }
-
- @Test
- public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- KeycloakSession session = keycloakRule.startSession();
- RealmModel realm = session.realms().getRealmByName("demo");
- int originalIdle = realm.getSsoSessionIdleTimeout();
- realm.setSsoSessionIdleTimeout(1);
- session.getTransaction().commit();
- session.close();
-
- Thread.sleep(2000);
-
- session = keycloakRule.startSession();
- realm = session.realms().getRealmByName("demo");
- session.sessions().removeExpiredUserSessions(realm);
- session.getTransaction().commit();
- session.close();
-
- // test SSO
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
-
-
-
- session = keycloakRule.startSession();
- realm = session.realms().getRealmByName("demo");
- // need to cleanup so other tests don't fail, so invalidate http sessions on remote clients.
- UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm);
- new ResourceAdminManager().logoutUser(null, realm, user.getId(), null);
- realm.setSsoSessionIdleTimeout(originalIdle);
- session.getTransaction().commit();
- session.close();
-
-
- }
-
- @Test
- public void testLoginSSOMax() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/customer-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- KeycloakSession session = keycloakRule.startSession();
- RealmModel realm = session.realms().getRealmByName("demo");
- int original = realm.getSsoSessionMaxLifespan();
- realm.setSsoSessionMaxLifespan(1);
- session.getTransaction().commit();
- session.close();
-
- Thread.sleep(2000);
-
-
- // test SSO
- driver.navigate().to("http://localhost:8081/product-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
-
- session = keycloakRule.startSession();
- realm = session.realms().getRealmByName("demo");
- realm.setSsoSessionMaxLifespan(original);
- session.getTransaction().commit();
- session.close();
- }
-
- /**
- * KEYCLOAK-518
- * @throws Exception
- */
- @Test
- public void testNullBearerToken() throws Exception {
- Client client = ClientBuilder.newClient();
- WebTarget target = client.target("http://localhost:8081/customer-db");
- Response response = target.request().get();
- Assert.assertEquals(401, response.getStatus());
- response.close();
- response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer null").get();
- Assert.assertEquals(401, response.getStatus());
- response.close();
- client.close();
-
- }
-
- /**
- * KEYCLOAK-518
- * @throws Exception
- */
- @Test
- public void testBadUser() throws Exception {
- Client client = ClientBuilder.newClient();
- UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
- URI uri = OpenIDConnectService.grantAccessTokenUrl(builder).build("demo");
- WebTarget target = client.target(uri);
- String header = BasicAuthHelper.createHeader("customer-portal", "password");
- Form form = new Form();
- form.param("username", "monkey@redhat.com")
- .param("password", "password");
- Response response = target.request()
- .header(HttpHeaders.AUTHORIZATION, header)
- .post(Entity.form(form));
- Assert.assertEquals(400, response.getStatus());
- response.close();
- client.close();
-
- }
-
- @Test
- public void testVersion() throws Exception {
- Client client = ClientBuilder.newClient();
- WebTarget target = client.target(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT).path("version");
- Version version = target.request().get(Version.class);
- Assert.assertNotNull(version);
- Assert.assertNotNull(version.getVersion());
- Assert.assertNotNull(version.getBuildTime());
- Assert.assertNotEquals(version.getVersion(), Version.UNKNOWN);
- Assert.assertNotEquals(version.getBuildTime(), Version.UNKNOWN);
-
- Version version2 = client.target("http://localhost:8081/secure-portal").path(AdapterConstants.K_VERSION).request().get(Version.class);
- Assert.assertNotNull(version2);
- Assert.assertNotNull(version2.getVersion());
- Assert.assertNotNull(version2.getBuildTime());
- Assert.assertEquals(version.getVersion(), version2.getVersion());
- Assert.assertEquals(version.getBuildTime(), version2.getBuildTime());
- client.close();
-
- }
-
-
-
- @Test
- public void testAuthenticated() throws Exception {
- // test login to customer-portal which does a bearer request to customer-db
- driver.navigate().to("http://localhost:8081/secure-portal");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- loginPage.login("bburke@redhat.com", "password");
- System.out.println("Current url: " + driver.getCurrentUrl());
- Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/secure-portal");
- String pageSource = driver.getPageSource();
- System.out.println(pageSource);
- Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
-
- // test logout
-
- String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
- .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/secure-portal").build("demo").toString();
- driver.navigate().to(logoutUri);
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- driver.navigate().to("http://localhost:8081/secure-portal");
- Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
- }
-
-
-
-}
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.adapter;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.FixMethodOrder;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.keycloak.Config;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.Version;
+import org.keycloak.adapters.AdapterConstants;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.oidc.OpenIDConnectService;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.action.SessionStats;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.resources.admin.AdminRoot;
+import org.keycloak.testsuite.OAuthClient;
+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.keycloak.util.BasicAuthHelper;
+import org.openqa.selenium.WebDriver;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.net.URL;
+import java.security.PublicKey;
+import java.util.Map;
+
+/**
+ * Tests Undertow Adapter
+ *
+ * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
+ */
+public class AdapterTest {
+
+ public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
+ public static PublicKey realmPublicKey;
+ @ClassRule
+ public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
+ @Override
+ protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
+ RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/adapter-test/demorealm.json"), RealmRepresentation.class);
+ RealmModel realm = manager.importRealm(representation);
+
+ realmPublicKey = realm.getPublicKey();
+
+ URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json");
+ deployApplication("customer-portal", "/customer-portal", CustomerServlet.class, url.getPath(), "user");
+ url = getClass().getResource("/adapter-test/secure-portal-keycloak.json");
+ deployApplication("secure-portal", "/secure-portal", CallAuthenticatedServlet.class, url.getPath(), "user", false);
+ url = getClass().getResource("/adapter-test/customer-db-keycloak.json");
+ deployApplication("customer-db", "/customer-db", CustomerDatabaseServlet.class, url.getPath(), "user");
+ url = getClass().getResource("/adapter-test/product-keycloak.json");
+ deployApplication("product-portal", "/product-portal", ProductServlet.class, url.getPath(), "user");
+
+ // Test that replacing system properties works for adapters
+ System.setProperty("my.host.name", "localhost");
+ url = getClass().getResource("/adapter-test/session-keycloak.json");
+ deployApplication("session-portal", "/session-portal", SessionServlet.class, url.getPath(), "user");
+ }
+ };
+
+ private static String createToken() {
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmManager manager = new RealmManager(session);
+
+ RealmModel adminRealm = manager.getRealm(Config.getAdminRealm());
+ ApplicationModel adminConsole = adminRealm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION);
+ TokenManager tm = new TokenManager();
+ UserModel admin = session.users().getUserByUsername("admin", adminRealm);
+ UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false);
+ AccessToken token = tm.createClientAccessToken(tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession);
+ return tm.encodeToken(adminRealm, token);
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+
+ @WebResource
+ protected WebDriver driver;
+
+ @WebResource
+ protected OAuthClient oauth;
+
+ @WebResource
+ protected LoginPage loginPage;
+
+ @Test
+ public void testLoginSSOAndLogout() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
+
+ // View stats
+ String adminToken = createToken();
+
+ Client client = ClientBuilder.newClient();
+ UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
+ WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo");
+ Map<String, SessionStats> stats = adminTarget.path("session-stats").request()
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
+ .get(new GenericType<Map<String, SessionStats>>() {
+ });
+
+ SessionStats custStats = stats.get("customer-portal");
+ Assert.assertNotNull(custStats);
+ Assert.assertEquals(1, custStats.getActiveSessions());
+ SessionStats prodStats = stats.get("product-portal");
+ Assert.assertNotNull(prodStats);
+ Assert.assertEquals(1, prodStats.getActiveSessions());
+
+ client.close();
+
+
+ // test logout
+
+ String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/customer-portal").build("demo").toString();
+ driver.navigate().to(logoutUri);
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+
+ }
+
+ @Test
+ public void testServletRequestLogout() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/product-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("iPhone") && pageSource.contains("iPad"));
+
+ // back
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+ // test logout
+
+ driver.navigate().to("http://localhost:8081/customer-portal/logout");
+
+
+
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ String currentUrl = driver.getCurrentUrl();
+ Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+
+ }
+
+ @Test
+ public void testLoginSSOIdle() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("demo");
+ int originalIdle = realm.getSsoSessionIdleTimeout();
+ realm.setSsoSessionIdleTimeout(1);
+ session.getTransaction().commit();
+ session.close();
+
+ Thread.sleep(2000);
+
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("demo");
+ realm.setSsoSessionIdleTimeout(originalIdle);
+ session.getTransaction().commit();
+ session.close();
+ }
+
+ @Test
+ public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("demo");
+ int originalIdle = realm.getSsoSessionIdleTimeout();
+ realm.setSsoSessionIdleTimeout(1);
+ session.getTransaction().commit();
+ session.close();
+
+ Thread.sleep(2000);
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("demo");
+ session.sessions().removeExpiredUserSessions(realm);
+ session.getTransaction().commit();
+ session.close();
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("demo");
+ // need to cleanup so other tests don't fail, so invalidate http sessions on remote clients.
+ UserModel user = session.users().getUserByUsername("bburke@redhat.com", realm);
+ new ResourceAdminManager().logoutUser(null, realm, user.getId(), null);
+ realm.setSsoSessionIdleTimeout(originalIdle);
+ session.getTransaction().commit();
+ session.close();
+ }
+
+ @Test
+ public void testLoginSSOMax() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/customer-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ KeycloakSession session = keycloakRule.startSession();
+ RealmModel realm = session.realms().getRealmByName("demo");
+ int original = realm.getSsoSessionMaxLifespan();
+ realm.setSsoSessionMaxLifespan(1);
+ session.getTransaction().commit();
+ session.close();
+
+ Thread.sleep(2000);
+
+
+ // test SSO
+ driver.navigate().to("http://localhost:8081/product-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ session = keycloakRule.startSession();
+ realm = session.realms().getRealmByName("demo");
+ realm.setSsoSessionMaxLifespan(original);
+ session.getTransaction().commit();
+ session.close();
+ }
+
+ /**
+ * KEYCLOAK-518
+ * @throws Exception
+ */
+ @Test
+ public void testNullBearerToken() throws Exception {
+ Client client = ClientBuilder.newClient();
+ WebTarget target = client.target("http://localhost:8081/customer-db");
+ Response response = target.request().get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+ response = target.request().header(HttpHeaders.AUTHORIZATION, "Bearer null").get();
+ Assert.assertEquals(401, response.getStatus());
+ response.close();
+ client.close();
+
+ }
+
+ /**
+ * KEYCLOAK-518
+ * @throws Exception
+ */
+ @Test
+ public void testBadUser() throws Exception {
+ Client client = ClientBuilder.newClient();
+ UriBuilder builder = UriBuilder.fromUri(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT);
+ URI uri = OpenIDConnectService.grantAccessTokenUrl(builder).build("demo");
+ WebTarget target = client.target(uri);
+ String header = BasicAuthHelper.createHeader("customer-portal", "password");
+ Form form = new Form();
+ form.param("username", "monkey@redhat.com")
+ .param("password", "password");
+ Response response = target.request()
+ .header(HttpHeaders.AUTHORIZATION, header)
+ .post(Entity.form(form));
+ Assert.assertEquals(400, response.getStatus());
+ response.close();
+ client.close();
+
+ }
+
+ @Test
+ public void testVersion() throws Exception {
+ Client client = ClientBuilder.newClient();
+ WebTarget target = client.target(org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT).path("version");
+ Version version = target.request().get(Version.class);
+ Assert.assertNotNull(version);
+ Assert.assertNotNull(version.getVersion());
+ Assert.assertNotNull(version.getBuildTime());
+ Assert.assertNotEquals(version.getVersion(), Version.UNKNOWN);
+ Assert.assertNotEquals(version.getBuildTime(), Version.UNKNOWN);
+
+ Version version2 = client.target("http://localhost:8081/secure-portal").path(AdapterConstants.K_VERSION).request().get(Version.class);
+ Assert.assertNotNull(version2);
+ Assert.assertNotNull(version2.getVersion());
+ Assert.assertNotNull(version2.getBuildTime());
+ Assert.assertEquals(version.getVersion(), version2.getVersion());
+ Assert.assertEquals(version.getBuildTime(), version2.getBuildTime());
+ client.close();
+
+ }
+
+
+
+ @Test
+ public void testAuthenticated() throws Exception {
+ // test login to customer-portal which does a bearer request to customer-db
+ driver.navigate().to("http://localhost:8081/secure-portal");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/secure-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
+
+ // test logout
+
+ String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/secure-portal").build("demo").toString();
+ driver.navigate().to(logoutUri);
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8081/secure-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ }
+
+ @Test
+ public void testSingleSessionInvalidated() throws Throwable {
+ AdapterTest browser1 = this;
+ AdapterTest browser2 = new AdapterTest();
+
+ loginAndCheckSession(browser1.driver, browser1.loginPage);
+
+ // Open browser2
+ browser2.webRule.before();
+ try {
+ browser2.loginAndCheckSession(browser2.driver, browser2.loginPage);
+
+ // Logout in browser1
+ String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8081/session-portal").build("demo").toString();
+ browser1.driver.navigate().to(logoutUri);
+ Assert.assertTrue(browser1.driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ // Assert that I am logged out in browser1
+ browser1.driver.navigate().to("http://localhost:8081/session-portal");
+ Assert.assertTrue(browser1.driver.getCurrentUrl().startsWith(LOGIN_URL));
+
+ // Assert that I am still logged in browser2 and same session is still preserved
+ browser2.driver.navigate().to("http://localhost:8081/session-portal");
+ Assert.assertEquals(browser2.driver.getCurrentUrl(), "http://localhost:8081/session-portal");
+ String pageSource = browser2.driver.getPageSource();
+ Assert.assertTrue(pageSource.contains("Counter=3"));
+
+ browser2.driver.navigate().to(logoutUri);
+ Assert.assertTrue(browser2.driver.getCurrentUrl().startsWith(LOGIN_URL));
+ } finally {
+ browser2.webRule.after();
+ }
+ }
+
+ private static void loginAndCheckSession(WebDriver driver, LoginPage loginPage) {
+ driver.navigate().to("http://localhost:8081/session-portal");
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ loginPage.login("bburke@redhat.com", "password");
+ System.out.println("Current url: " + driver.getCurrentUrl());
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/session-portal");
+ String pageSource = driver.getPageSource();
+ Assert.assertTrue(pageSource.contains("Counter=1"));
+
+ // Counter increased now
+ driver.navigate().to("http://localhost:8081/session-portal");
+ pageSource = driver.getPageSource();
+ Assert.assertTrue(pageSource.contains("Counter=2"));
+
+ }
+
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
index e77d0f8..eafe55b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/CustomerServlet.java
@@ -7,6 +7,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
index 228461c..0c3dfb8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/RelativeUriAdapterTest.java
@@ -35,7 +35,6 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
-import org.keycloak.representations.adapters.action.SessionStats;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.admin.AdminRoot;
@@ -132,16 +131,16 @@ public class RelativeUriAdapterTest {
Client client = ClientBuilder.newClient();
UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
WebTarget adminTarget = client.target(AdminRoot.realmsUrl(authBase)).path("demo");
- Map<String, SessionStats> stats = adminTarget.path("session-stats").request()
+ Map<String, Integer> stats = adminTarget.path("application-session-stats").request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + adminToken)
- .get(new GenericType<Map<String, SessionStats>>(){});
-
- SessionStats custStats = stats.get("customer-portal");
- Assert.assertNotNull(custStats);
- Assert.assertEquals(1, custStats.getActiveSessions());
- SessionStats prodStats = stats.get("product-portal");
- Assert.assertNotNull(prodStats);
- Assert.assertEquals(1, prodStats.getActiveSessions());
+ .get(new GenericType<Map<String, Integer>>(){});
+
+ Integer custSessionsCount = stats.get("customer-portal");
+ Assert.assertNotNull(custSessionsCount);
+ Assert.assertTrue(1 == custSessionsCount);
+ Integer prodStatsCount = stats.get("product-portal");
+ Assert.assertNotNull(prodStatsCount);
+ Assert.assertTrue(1 == prodStatsCount);
client.close();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/SessionServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/SessionServlet.java
new file mode 100644
index 0000000..c7c4d85
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/SessionServlet.java
@@ -0,0 +1,38 @@
+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 javax.servlet.http.HttpSession;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class SessionServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ String counter = increaseAndGetCounter(req);
+
+ resp.setContentType("text/html");
+ PrintWriter pw = resp.getWriter();
+ pw.printf("<html><head><title>%s</title></head><body>", "Session Test");
+ pw.printf("Counter=%s", counter);
+ pw.print("</body></html>");
+ pw.flush();
+
+
+ }
+
+ private String increaseAndGetCounter(HttpServletRequest req) {
+ HttpSession session = req.getSession();
+ Integer counter = (Integer)session.getAttribute("counter");
+ counter = (counter == null) ? 1 : counter + 1;
+ session.setAttribute("counter", counter);
+ return String.valueOf(counter);
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
index 30c10a7..2f78c6d 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/SSOTest.java
@@ -27,6 +27,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
+import org.keycloak.events.Event;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
@@ -40,6 +41,7 @@ import org.openqa.selenium.WebDriver;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -88,7 +90,7 @@ public class SSOTest {
profilePage.open();
- Assert.assertTrue(profilePage.isCurrent());
+ assertTrue(profilePage.isCurrent());
String sessionId2 = events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("test-app").assertEvent().getSessionId();
@@ -105,4 +107,51 @@ public class SSOTest {
events.clear();
}
+ @Test
+ public void multipleSessions() {
+ loginPage.open();
+ loginPage.login("test-user@localhost", "password");
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
+ Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ Event login1 = events.expectLogin().assertEvent();
+
+ WebDriver driver2 = WebRule.createWebDriver();
+ try {
+ OAuthClient oauth2 = new OAuthClient(driver2);
+ oauth2.state("mystate");
+ oauth2.doLogin("test-user@localhost", "password");
+
+ Event login2 = events.expectLogin().assertEvent();
+
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, RequestType.valueOf(driver2.getTitle()));
+ Assert.assertNotNull(oauth2.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ assertNotEquals(login1.getSessionId(), login2.getSessionId());
+
+ oauth.openLogout();
+ events.expectLogout(login1.getSessionId()).assertEvent();
+
+ oauth.openLoginForm();
+
+ assertTrue(loginPage.isCurrent());
+
+ oauth2.openLoginForm();
+
+ events.expectLogin().session(login2.getSessionId()).detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).assertEvent();
+ Assert.assertEquals(RequestType.AUTH_RESPONSE, RequestType.valueOf(driver2.getTitle()));
+ Assert.assertNotNull(oauth2.getCurrentQuery().get(OAuth2Constants.CODE));
+
+ oauth2.openLogout();
+ events.expectLogout(login2.getSessionId()).assertEvent();
+
+ oauth2.openLoginForm();
+
+ assertTrue(driver2.getTitle().equals("Log in to test"));
+ } finally {
+ driver2.close();
+ }
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
index 803133e..fcd51b3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java
@@ -33,9 +33,11 @@ import org.keycloak.events.Event;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OpenIDConnectService;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
+import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
@@ -56,6 +58,8 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
+import java.security.PrivateKey;
+import java.security.PublicKey;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertEquals;
@@ -175,6 +179,54 @@ public class RefreshTokenTest {
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
}
+ PrivateKey privateKey;
+ PublicKey publicKey;
+
+ @Test
+ public void refreshTokenRealmKeysChanged() throws Exception {
+ oauth.doLogin("test-user@localhost", "password");
+
+ Event loginEvent = events.expectLogin().assertEvent();
+
+ String sessionId = loginEvent.getSessionId();
+ String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+ String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+ AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+ String refreshTokenString = response.getRefreshToken();
+ RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
+
+ events.expectCodeToToken(codeId, sessionId).assertEvent();
+
+ try {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ privateKey = appRealm.getPrivateKey();
+ publicKey = appRealm.getPublicKey();
+ KeycloakModelUtils.generateRealmKeys(appRealm);
+ }
+ });
+
+ response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
+
+ assertEquals(400, response.getStatusCode());
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectRefresh(refreshToken.getId(), sessionId).user((String) null).session((String) null).clearDetails().error(Errors.INVALID_TOKEN).assertEvent();
+ } finally {
+ keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ appRealm.setPrivateKey(privateKey);
+ appRealm.setPublicKey(publicKey);
+ }
+ });
+
+ }
+ }
+
@Test
public void refreshTokenUserSessionExpired() {
oauth.doLogin("test-user@localhost", "password");
@@ -417,5 +469,4 @@ public class RefreshTokenTest {
}
-
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
index f59c8ef..f172dec 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/ResourceOwnerPasswordCredentialsGrantTest.java
@@ -8,6 +8,8 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.managers.RealmManager;
@@ -32,6 +34,14 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
ApplicationModel app = appRealm.addApplication("resource-owner");
app.setSecret("secret");
appRealm.setPasswordCredentialGrantAllowed(true);
+
+ UserModel user = session.users().addUser(appRealm, "direct-login");
+ user.setEmail("direct-login@localhost");
+ user.setEnabled(true);
+
+ userId = user.getId();
+
+ session.users().updateCredential(appRealm, user, UserCredentialModel.password("password"));
}
});
@@ -47,11 +57,22 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
@WebResource
protected OAuthClient oauth;
+ private static String userId;
+
@Test
- public void grantAccessToken() throws Exception {
+ public void grantAccessTokenUsername() throws Exception {
+ grantAccessToken("direct-login");
+ }
+
+ @Test
+ public void grantAccessTokenEmail() throws Exception {
+ grantAccessToken("direct-login@localhost");
+ }
+
+ private void grantAccessToken(String login) throws Exception {
oauth.clientId("resource-owner");
- OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "test-user@localhost", "password");
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", login, "password");
assertEquals(200, response.getStatusCode());
@@ -60,11 +81,13 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
events.expectLogin()
.client("resource-owner")
+ .user(userId)
.session(accessToken.getSessionState())
.detail(Details.AUTH_METHOD, "oauth_credentials")
.detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+ .detail(Details.USERNAME, login)
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.assertEvent();
@@ -79,7 +102,7 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
assertEquals(accessToken.getSessionState(), refreshedAccessToken.getSessionState());
assertEquals(accessToken.getSessionState(), refreshedRefreshToken.getSessionState());
- events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).client("resource-owner").assertEvent();
+ events.expectRefresh(refreshToken.getId(), refreshToken.getSessionState()).user(userId).client("resource-owner").assertEvent();
}
@Test
@@ -187,4 +210,27 @@ public class ResourceOwnerPasswordCredentialsGrantTest {
.assertEvent();
}
+ @Test
+ public void grantAccessTokenUserNotFound() throws Exception {
+ oauth.clientId("resource-owner");
+
+ OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("secret", "invalid", "invalid");
+
+ assertEquals(400, response.getStatusCode());
+
+ assertEquals("invalid_grant", response.getError());
+
+ events.expectLogin()
+ .client("resource-owner")
+ .user((String) null)
+ .session((String) null)
+ .detail(Details.AUTH_METHOD, "oauth_credentials")
+ .detail(Details.RESPONSE_TYPE, "token")
+ .detail(Details.USERNAME, "invalid")
+ .removeDetail(Details.CODE_ID)
+ .removeDetail(Details.REDIRECT_URI)
+ .error(Errors.INVALID_USER_CREDENTIALS)
+ .assertEvent();
+ }
+
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index 35454dc..dab37f7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -46,7 +46,7 @@ public class WebRule extends ExternalResource {
}
@Override
- protected void before() throws Throwable {
+ public void before() throws Throwable {
driver = createWebDriver();
oauth = new OAuthClient(driver);
initWebResources(test);
@@ -121,7 +121,7 @@ public class WebRule extends ExternalResource {
}
@Override
- protected void after() {
+ public void after() {
driver.manage().deleteAllCookies();
driver.close();
}
diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
index ed538f2..abc8e40 100755
--- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json
+++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
@@ -105,6 +105,16 @@
"http://localhost:8081/secure-portal/*"
],
"secret": "password"
+ },
+ {
+ "name": "session-portal",
+ "enabled": true,
+ "adminUrl": "http://localhost:8081/session-portal",
+ "baseUrl": "http://localhost:8081/session-portal",
+ "redirectUris": [
+ "http://localhost:8081/session-portal/*"
+ ],
+ "secret": "password"
}
],
"oauthClients": [
diff --git a/testsuite/integration/src/test/resources/adapter-test/session-keycloak.json b/testsuite/integration/src/test/resources/adapter-test/session-keycloak.json
new file mode 100644
index 0000000..6a7f60b
--- /dev/null
+++ b/testsuite/integration/src/test/resources/adapter-test/session-keycloak.json
@@ -0,0 +1,10 @@
+{
+ "realm" : "demo",
+ "resource" : "session-portal",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://${my.host.name}:8081/auth",
+ "ssl-required" : "external",
+ "credentials" : {
+ "secret": "password"
+ }
+}
\ No newline at end of file