merge
This commit is contained in:
commit
5f1e6f3a23
57 changed files with 1489 additions and 611 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
33
core/src/test/java/org/keycloak/JsonParserTest.java
Normal file
33
core/src/test/java/org/keycloak/JsonParserTest.java
Normal file
|
@ -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());
|
||||
}
|
||||
}
|
9
core/src/test/resources/keycloak.json
Normal file
9
core/src/test/resources/keycloak.json
Normal file
|
@ -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}"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
217
docbook/reference/en/en-US/modules/clustering.xml
Executable file
217
docbook/reference/en/en-US/modules/clustering.xml
Executable file
|
@ -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>
|
|
@ -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:
|
||||
|
|
|
@ -6,12 +6,38 @@
|
|||
and feel of end-user facing pages so they can be integrated with your brand and applications.
|
||||
</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>
|
||||
|
@ -25,26 +51,15 @@
|
|||
|
||||
<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>
|
||||
|
|
|
@ -7,6 +7,5 @@
|
|||
"expose-token": true,
|
||||
"credentials": {
|
||||
"secret": "password"
|
||||
},
|
||||
"use-hostname-for-local-requests": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,5 @@
|
|||
"ssl-required" : "external",
|
||||
"credentials" : {
|
||||
"secret": "password"
|
||||
},
|
||||
"use-hostname-for-local-requests": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,5 @@
|
|||
"ssl-required" : "external",
|
||||
"credentials" : {
|
||||
"secret": "password"
|
||||
},
|
||||
"use-hostname-for-local-requests": false
|
||||
}
|
||||
}
|
|
@ -5,6 +5,5 @@
|
|||
"ssl-required" : "external",
|
||||
"credentials" : {
|
||||
"secret": "password"
|
||||
},
|
||||
"use-hostname-for-local-requests": false
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later
|
||||
|
||||
logOutAllSessions=Log out all sessions
|
||||
|
|
|
@ -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>
|
||||
</@layout.mainLayout>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
````
|
32
testsuite/docker-cluster/as7-image/Dockerfile
Normal file
32
testsuite/docker-cluster/as7-image/Dockerfile
Normal file
|
@ -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" ]
|
13
testsuite/docker-cluster/as7/Dockerfile
Normal file
13
testsuite/docker-cluster/as7/Dockerfile
Normal file
|
@ -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" ]
|
8
testsuite/docker-cluster/as7/keycloak-as7-trigger.sh
Normal file
8
testsuite/docker-cluster/as7/keycloak-as7-trigger.sh
Normal file
|
@ -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
|
|
@ -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>
|
||||
|
|
30
testsuite/docker-cluster/eap63-image/Dockerfile
Normal file
30
testsuite/docker-cluster/eap63-image/Dockerfile
Normal file
|
@ -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" ]
|
13
testsuite/docker-cluster/eap63/Dockerfile
Normal file
13
testsuite/docker-cluster/eap63/Dockerfile
Normal file
|
@ -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" ]
|
8
testsuite/docker-cluster/eap63/keycloak-eap63-trigger.sh
Normal file
8
testsuite/docker-cluster/eap63/keycloak-eap63-trigger.sh
Normal file
|
@ -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
|
31
testsuite/docker-cluster/fig-as7.yml
Normal file
31
testsuite/docker-cluster/fig-as7.yml
Normal file
|
@ -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"
|
31
testsuite/docker-cluster/fig-eap63.yml
Normal file
31
testsuite/docker-cluster/fig-eap63.yml
Normal file
|
@ -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"
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Deploy and configure all examples
|
||||
## Deploy and configure all examples
|
||||
|
||||
# Deploy examples
|
||||
cd /keycloak-docker-cluster/examples
|
||||
for I in $(find . | grep .war$); do cp $I /opt/wildfly/standalone/deployments/; done;
|
||||
for I in $(find . | grep .war$); do cp $I $JBOSS_HOME/standalone/deployments/; done;
|
||||
|
||||
# Explode wars
|
||||
cd /opt/wildfly/standalone/deployments/
|
||||
cd $JBOSS_HOME/standalone/deployments/
|
||||
for I in $(ls -d *.war | grep -v auth-server.war); do
|
||||
echo "Configuring $I";
|
||||
mkdir $I.tmp;
|
||||
|
@ -25,10 +25,13 @@ sed -i -e 's/false/true/' admin-access.war/WEB-INF/web.xml
|
|||
|
||||
# Configure other examples
|
||||
for I in *.war/WEB-INF/keycloak.json; do
|
||||
sed -i -e 's/\"use-hostname-for-local-requests\": false/\"use-hostname-for-local-requests\": true/' $I;
|
||||
sed -i -e 's/\"\/auth\",/&\n \"auth-server-url-for-backend-requests\": \"http:\/\/\$\{jboss.host.name\}:8080\/auth\",/' $I;
|
||||
done;
|
||||
|
||||
# Enable distributable for customer-portal
|
||||
sed -i -e 's/<\/module-name>/&\n <distributable \/>/' customer-portal.war/WEB-INF/web.xml
|
||||
|
||||
# Configure testrealm.json - Enable adminUrl to access adapters on local machine
|
||||
sed -i -e 's/\"adminUrl\": \"/&http:\/\/\$\{jboss.host.name\}:8080/' /keycloak-docker-cluster/examples/testrealm.json
|
||||
|
||||
|
|
@ -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/
|
|
@ -1,47 +1,51 @@
|
|||
#!/bin/bash
|
||||
|
||||
export MYHOST=node$(echo $MYSQL_NAME | awk -F"/dockercluster[^0-9]*|\/mysql" '{print $2 }');
|
||||
export MYHOST="$NODE_PREFIX"node$(echo $MYSQL_NAME | awk -F"/dockercluster[^0-9]*|\/mysql" '{print $2 }');
|
||||
echo "MYHOST is $MYHOST. MYSQL_NAME is $MYSQL_NAME";
|
||||
|
||||
function prepareHost
|
||||
{
|
||||
if [ -d /keycloak-docker-shared/keycloak-wildfly-$MYHOST ]; then
|
||||
if [ -d /keycloak-docker-shared/keycloak-$JBOSS_TYPE-$MYHOST ]; then
|
||||
echo "Node $MYHOST already prepared. Skiping";
|
||||
return;
|
||||
fi
|
||||
|
||||
echo "Creating keycloak-wildfly-$MYHOST";
|
||||
echo "Creating keycloak-$JBOSS_TYPE-$MYHOST";
|
||||
|
||||
cd /opt/wildfly
|
||||
cp -r /keycloak-docker-cluster/modules ./
|
||||
/keycloak-docker-cluster/shared-files/keycloak-base-prepare.sh
|
||||
|
||||
echo "Base prepare finished";
|
||||
|
||||
cd $JBOSS_HOME
|
||||
cp -r /keycloak-docker-cluster/$JBOSS_TYPE-adapter/modules ./
|
||||
|
||||
# Deploy keycloak
|
||||
cp -r /keycloak-docker-cluster/deployments/* /opt/wildfly/standalone/deployments/
|
||||
cp -r /keycloak-docker-cluster/deployments/* $JBOSS_HOME/standalone/deployments/
|
||||
|
||||
# Enable Infinispan provider
|
||||
sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" /opt/wildfly/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
sed -i "s|keycloak.userSessions.provider:mem|keycloak.userSessions.provider:infinispan|" $JBOSS_HOME/standalone/deployments/auth-server.war/WEB-INF/classes/META-INF/keycloak-server.json
|
||||
|
||||
# Deploy and configure examples
|
||||
/deploy-examples.sh
|
||||
/keycloak-docker-cluster/shared-files/deploy-examples.sh
|
||||
|
||||
# Deploy to volume
|
||||
rm -rf /keycloak-docker-shared/keycloak-wildfly-$MYHOST
|
||||
cp -r /opt/wildfly-8.1.0.Final /keycloak-docker-shared/keycloak-wildfly-$MYHOST
|
||||
chmod -R 777 /keycloak-docker-shared/keycloak-wildfly-$MYHOST
|
||||
echo "keycloak-wildfly-$MYHOST prepared and copyied to volume";
|
||||
rm -rf /keycloak-docker-shared/keycloak-$JBOSS_TYPE-$MYHOST
|
||||
cp -r $JBOSS_HOME /keycloak-docker-shared/keycloak-$JBOSS_TYPE-$MYHOST
|
||||
chmod -R 777 /keycloak-docker-shared/keycloak-$JBOSS_TYPE-$MYHOST
|
||||
echo "keycloak-$JBOSS_TYPE-$MYHOST prepared and copyied to volume";
|
||||
}
|
||||
|
||||
function waitForPreviousNodeStart
|
||||
{
|
||||
myHostNumber=$(echo $MYHOST | awk -F"node" '{ print $2 }');
|
||||
if [ $myHostNumber -eq 1 ]; then
|
||||
echo "Our host is node1. No need to wait for previous server";
|
||||
echo "Our host is $MYHOST. No need to wait for previous server";
|
||||
else
|
||||
previous=node$(($myHostNumber-1));
|
||||
previous="$NODE_PREFIX"node$(($myHostNumber-1));
|
||||
echo "Waiting for host $previous to start";
|
||||
|
||||
for I in $(seq 1 10); do
|
||||
cat /keycloak-docker-shared/keycloak-wildfly-$previous/standalone/log/server.log | grep "\(INFO\|ERROR\).*WildFly.*started";
|
||||
cat /keycloak-docker-shared/keycloak-$JBOSS_TYPE-$previous/standalone/log/server.log | grep "\(INFO\|ERROR\).*\(WildFly\|JBoss AS\|JBoss EAP\).*started";
|
||||
if [ 0 -eq $? ]; then
|
||||
echo "Host $previous started. Going to start $MYHOST";
|
||||
return;
|
||||
|
@ -77,7 +81,7 @@ waitForMySQLStart;
|
|||
|
||||
echo "Running keycloak node $MYHOST. Additional arguments: $@";
|
||||
cd /keycloak-docker-shared
|
||||
export JBOSS_HOME=/keycloak-docker-shared/keycloak-wildfly-$MYHOST;
|
||||
export JBOSS_HOME=/keycloak-docker-shared/keycloak-$JBOSS_TYPE-$MYHOST;
|
||||
|
||||
cd $JBOSS_HOME/bin/
|
||||
|
|
@ -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 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/
|
||||
ADD keycloak-wildfly-trigger.sh /keycloak-wildfly-trigger.sh
|
||||
RUN chmod u+x /keycloak-wildfly-trigger.sh
|
||||
|
||||
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 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
|
||||
|
||||
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" ]
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>>(){});
|
||||
.get(new GenericType<Map<String, Integer>>(){});
|
||||
|
||||
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());
|
||||
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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue