This commit is contained in:
Bill Burke 2014-10-07 18:17:44 -04:00
commit 5f1e6f3a23
57 changed files with 1489 additions and 611 deletions

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View 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());
}
}

View 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}"
}

View file

@ -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>

View file

@ -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",

View 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>

View file

@ -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:

View file

@ -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>

View file

@ -7,6 +7,5 @@
"expose-token": true,
"credentials": {
"secret": "password"
},
"use-hostname-for-local-requests": false
}
}

View file

@ -6,6 +6,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
},
"use-hostname-for-local-requests": false
}
}

View file

@ -5,6 +5,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
},
"use-hostname-for-local-requests": false
}
}

View file

@ -5,6 +5,5 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
},
"use-hostname-for-local-requests": false
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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 {

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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());
}

View file

@ -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);

View file

@ -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) {

View file

@ -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;

View file

@ -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
````

View 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" ]

View 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" ]

View 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

View file

@ -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>

View 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" ]

View 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" ]

View 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

View 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"

View 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"

View file

@ -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:

View file

@ -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>

View file

@ -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

View file

@ -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/

View file

@ -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/

View file

@ -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" ]

View 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

View file

@ -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();
}
}
}

View file

@ -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"));
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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 {
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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": [

View file

@ -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"
}
}