keycloak-uncached
Changes
integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 7(+6 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java 4(+2 -2)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java 94(+94 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java 9(+1 -8)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java 2(+1 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java 2(+1 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java 42(+42 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java 2(+1 -1)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java 56(+56 -0)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java 81(+7 -74)
integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java 2(+1 -1)
pom.xml 1(+1 -0)
proxy/proxy-server/pom.xml 87(+87 -0)
testsuite/pom.xml 1(+1 -0)
testsuite/proxy/pom.xml 509(+509 -0)
Details
diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 8d8eed9..e03eb51 100755
--- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -87,6 +87,11 @@ public class KeycloakDeploymentBuilder {
}
public static KeycloakDeployment build(InputStream is) {
+ AdapterConfig adapterConfig = loadAdapterConfig(is);
+ return new KeycloakDeploymentBuilder().internalBuild(adapterConfig);
+ }
+
+ public static AdapterConfig loadAdapterConfig(InputStream is) {
ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
AdapterConfig adapterConfig;
@@ -95,7 +100,7 @@ public class KeycloakDeploymentBuilder {
} catch (IOException e) {
throw new RuntimeException(e);
}
- return new KeycloakDeploymentBuilder().internalBuild(adapterConfig);
+ return adapterConfig;
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java
new file mode 100755
index 0000000..b9761e1
--- /dev/null
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.keycloak.adapters.undertow;
+
+import io.undertow.security.api.SecurityContext;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.Session;
+import io.undertow.util.Sessions;
+import org.keycloak.KeycloakPrincipal;
+import org.keycloak.adapters.AdapterTokenStore;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OAuthRequestAuthenticator;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.RequestAuthenticator;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
+ * @version $Revision: 1 $
+ */
+public abstract class AbstractUndertowRequestAuthenticator extends RequestAuthenticator {
+ protected SecurityContext securityContext;
+ protected HttpServerExchange exchange;
+
+
+ public AbstractUndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
+ SecurityContext securityContext, HttpServerExchange exchange,
+ AdapterTokenStore tokenStore) {
+ super(facade, deployment, tokenStore, sslRedirectPort);
+ this.securityContext = securityContext;
+ this.exchange = exchange;
+ }
+
+ protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
+ exchange.putAttachment(UndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY, account.getKeycloakSecurityContext());
+ }
+
+ @Override
+ protected OAuthRequestAuthenticator createOAuthAuthenticator() {
+ return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) {
+ @Override
+ protected void saveRequest() {
+ // todo
+ }
+ };
+ }
+
+ @Override
+ protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
+ KeycloakUndertowAccount account = createAccount(principal);
+ securityContext.authenticationComplete(account, "KEYCLOAK", false);
+ propagateKeycloakContext(account);
+ tokenStore.saveAccountInfo(account);
+ }
+
+ @Override
+ protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
+ KeycloakUndertowAccount account = createAccount(principal);
+ securityContext.authenticationComplete(account, "KEYCLOAK", false);
+ propagateKeycloakContext(account);
+ }
+
+ @Override
+ protected String getHttpSessionId(boolean create) {
+ if (create) {
+ Session session = Sessions.getOrCreateSession(exchange);
+ return session.getId();
+ } else {
+ Session session = Sessions.getSession(exchange);
+ return session != null ? session.getId() : null;
+ }
+ }
+
+ /**
+ * Subclasses need to be able to create their own version of the KeycloakUndertowAccount
+ * @return The account
+ */
+ protected abstract KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
+}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
index 7f18e90..0ae5b6e 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletKeycloakAuthMech.java
@@ -16,14 +16,10 @@
*/
package org.keycloak.adapters.undertow;
-import io.undertow.security.api.NotificationReceiver;
import io.undertow.security.api.SecurityContext;
-import io.undertow.security.api.SecurityNotification;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ConfidentialPortManager;
-import io.undertow.servlet.handlers.ServletRequestContext;
import org.jboss.logging.Logger;
-import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
@@ -32,15 +28,12 @@ import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.RequestAuthenticator;
import org.keycloak.enums.TokenStore;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
-
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
* @version $Revision: 1 $
*/
-public class ServletKeycloakAuthMech extends UndertowKeycloakAuthMech {
+public class ServletKeycloakAuthMech extends AbstractUndertowKeycloakAuthMech {
private static final Logger log = Logger.getLogger(ServletKeycloakAuthMech.class);
protected NodesRegistrationManagement nodesRegistrationManagement;
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java
index ed13f56..d28ff29 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletRequestAuthenticator.java
@@ -34,7 +34,7 @@ import javax.servlet.http.HttpSession;
* @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
* @version $Revision: 1 $
*/
-public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
+public class ServletRequestAuthenticator extends AbstractUndertowRequestAuthenticator {
public ServletRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
old mode 100644
new mode 100755
index 70f0bee..2168d9d
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
@@ -63,7 +63,7 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
if (account.checkActive()) {
log.debug("Cached account found");
securityContext.authenticationComplete(account, "KEYCLOAK", false);
- ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
+ ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
return true;
} else {
log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java
new file mode 100755
index 0000000..057fc50
--- /dev/null
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java
@@ -0,0 +1,42 @@
+package org.keycloak.adapters.undertow;
+
+import io.undertow.security.api.SecurityContext;
+import io.undertow.server.HttpServerExchange;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.AdapterTokenStore;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.NodesRegistrationManagement;
+import org.keycloak.adapters.RequestAuthenticator;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UndertowAuthenticationMechanism extends AbstractUndertowKeycloakAuthMech {
+ protected NodesRegistrationManagement nodesRegistrationManagement;
+ protected int confidentialPort;
+
+ public UndertowAuthenticationMechanism(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement,
+ NodesRegistrationManagement nodesRegistrationManagement, int confidentialPort) {
+ super(deploymentContext, sessionManagement);
+ this.nodesRegistrationManagement = nodesRegistrationManagement;
+ this.confidentialPort = confidentialPort;
+ }
+
+ @Override
+ public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
+ UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
+ KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
+ if (!deployment.isConfigured()) {
+ return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
+ }
+
+ nodesRegistrationManagement.tryRegister(deployment);
+
+ AdapterTokenStore tokenStore = getTokenStore(exchange, facade, deployment, securityContext);
+ RequestAuthenticator authenticator = new UndertowRequestAuthenticator(facade, deployment, confidentialPort, securityContext, exchange, tokenStore);
+
+ return keycloakAuthenticate(exchange, securityContext, authenticator);
+ }
+
+}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
index 65b6ab2..ccd695f 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowCookieTokenStore.java
@@ -53,7 +53,7 @@ public class UndertowCookieTokenStore implements AdapterTokenStore {
if (account.checkActive()) {
log.debug("Cached account found");
securityContext.authenticationComplete(account, "KEYCLOAK", false);
- ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
+ ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
return true;
} else {
log.debug("Account was not active, removing cookie and returning false");
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
index 54552cc..21738f5 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java
@@ -96,6 +96,7 @@ public class UndertowHttpFacade implements HttpFacade {
@Override
public InputStream getInputStream() {
+ if (!exchange.isBlocking()) exchange.startBlocking();
return exchange.getInputStream();
}
@@ -142,6 +143,7 @@ public class UndertowHttpFacade implements HttpFacade {
@Override
public OutputStream getOutputStream() {
+ if (!exchange.isBlocking()) exchange.startBlocking();
return exchange.getOutputStream();
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java
new file mode 100755
index 0000000..0a70c67
--- /dev/null
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowPreAuthActionsHandler.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.keycloak.adapters.undertow;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.session.SessionManager;
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.PreAuthActionsHandler;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class UndertowPreAuthActionsHandler implements HttpHandler {
+
+ private static final Logger log = Logger.getLogger(UndertowPreAuthActionsHandler.class);
+ protected HttpHandler next;
+ protected SessionManager sessionManager;
+ protected UndertowUserSessionManagement userSessionManagement;
+ protected AdapterDeploymentContext deploymentContext;
+
+ public UndertowPreAuthActionsHandler(AdapterDeploymentContext deploymentContext,
+ UndertowUserSessionManagement userSessionManagement,
+ SessionManager sessionManager,
+ HttpHandler next) {
+ this.next = next;
+ this.deploymentContext = deploymentContext;
+ this.sessionManager = sessionManager;
+ this.userSessionManagement = userSessionManagement;
+ }
+
+ @Override
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
+ UndertowHttpFacade facade = new UndertowHttpFacade(exchange);
+ SessionManagementBridge bridge = new SessionManagementBridge(userSessionManagement, sessionManager);
+ PreAuthActionsHandler handler = new PreAuthActionsHandler(bridge, deploymentContext, facade);
+ if (handler.handleRequest()) return;
+ next.handleRequest(exchange);
+ }
+}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java
index 6b9c351..91fe9e8 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowRequestAuthenticator.java
@@ -1,94 +1,27 @@
-/*
- * Copyright 2014 Red Hat Inc. and/or its affiliates and other contributors
- * as indicated by the @author tags. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
package org.keycloak.adapters.undertow;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
-import io.undertow.server.session.Session;
-import io.undertow.util.Sessions;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakDeployment;
-import org.keycloak.adapters.OAuthRequestAuthenticator;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
-import org.keycloak.adapters.RequestAuthenticator;
+import org.keycloak.adapters.undertow.AbstractUndertowRequestAuthenticator;
+import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
- * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc.
* @version $Revision: 1 $
*/
-public abstract class UndertowRequestAuthenticator extends RequestAuthenticator {
- protected SecurityContext securityContext;
- protected HttpServerExchange exchange;
-
-
+public class UndertowRequestAuthenticator extends AbstractUndertowRequestAuthenticator {
public UndertowRequestAuthenticator(HttpFacade facade, KeycloakDeployment deployment, int sslRedirectPort,
- SecurityContext securityContext, HttpServerExchange exchange,
- AdapterTokenStore tokenStore) {
- super(facade, deployment, tokenStore, sslRedirectPort);
- this.securityContext = securityContext;
- this.exchange = exchange;
- }
-
- protected void propagateKeycloakContext(KeycloakUndertowAccount account) {
- exchange.putAttachment(UndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY, account.getKeycloakSecurityContext());
- }
-
- @Override
- protected OAuthRequestAuthenticator createOAuthAuthenticator() {
- return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) {
- @Override
- protected void saveRequest() {
- // todo
- }
- };
+ SecurityContext securityContext, HttpServerExchange exchange, AdapterTokenStore tokenStore) {
+ super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore);
}
@Override
- protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
- KeycloakUndertowAccount account = createAccount(principal);
- securityContext.authenticationComplete(account, "KEYCLOAK", false);
- propagateKeycloakContext(account);
- tokenStore.saveAccountInfo(account);
+ protected KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
+ return new KeycloakUndertowAccount(principal);
}
-
- @Override
- protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
- KeycloakUndertowAccount account = createAccount(principal);
- securityContext.authenticationComplete(account, "KEYCLOAK", false);
- propagateKeycloakContext(account);
- }
-
- @Override
- protected String getHttpSessionId(boolean create) {
- if (create) {
- Session session = Sessions.getOrCreateSession(exchange);
- return session.getId();
- } else {
- Session session = Sessions.getSession(exchange);
- return session != null ? session.getId() : null;
- }
- }
-
- /**
- * Subclasses need to be able to create their own version of the KeycloakUndertowAccount
- * @return The account
- */
- protected abstract KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
}
diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
index cc9e3d9..e5f013c 100755
--- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
+++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
@@ -60,7 +60,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
if (account.checkActive()) {
log.debug("Cached account found");
securityContext.authenticationComplete(account, "KEYCLOAK", false);
- ((UndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
+ ((AbstractUndertowRequestAuthenticator)authenticator).propagateKeycloakContext(account);
return true;
} else {
log.debug("Account was not active, returning false");
pom.xml 1(+1 -0)
diff --git a/pom.xml b/pom.xml
index ffeaa5e..ba351f1 100755
--- a/pom.xml
+++ b/pom.xml
@@ -108,6 +108,7 @@
<module>events</module>
<module>model</module>
<module>integration</module>
+ <module>proxy/proxy-server</module>
<module>picketlink</module>
<module>federation</module>
<module>services</module>
proxy/proxy-server/pom.xml 87(+87 -0)
diff --git a/proxy/proxy-server/pom.xml b/proxy/proxy-server/pom.xml
new file mode 100755
index 0000000..4b4d12a
--- /dev/null
+++ b/proxy/proxy-server/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.1.0.Beta2-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-proxy-server</artifactId>
+ <name>Keycloak Proxy Server</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <version>${jboss.logging.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-undertow-adapter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-adapter-core</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>${keycloak.apache.httpcomponents.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>net.iharder</groupId>
+ <artifactId>base64</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk16</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-xc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java
new file mode 100755
index 0000000..bdbddc6
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java
@@ -0,0 +1,42 @@
+package org.keycloak.proxy;
+
+import io.undertow.security.handlers.AuthenticationConstraintHandler;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.AttachmentKey;
+import org.keycloak.KeycloakSecurityContext;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ConstraintMatcherHandler implements HttpHandler {
+ public static final AttachmentKey<SingleConstraintMatch> CONSTRAINT_KEY = AttachmentKey.create(SingleConstraintMatch.class);
+ protected SecurityPathMatches matcher;
+ protected HttpHandler securedHandler;
+ protected HttpHandler unsecuredHandler;
+
+ public ConstraintMatcherHandler(SecurityPathMatches matcher, HttpHandler securedHandler, HttpHandler unsecuredHandler) {
+ this.matcher = matcher;
+ this.securedHandler = securedHandler;
+ this.unsecuredHandler = unsecuredHandler;
+ }
+
+ @Override
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
+ SingleConstraintMatch match = matcher.getSecurityInfo(exchange.getRelativePath(), exchange.getRequestMethod().toString()).getMergedConstraint();
+ if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.PERMIT)) {
+ unsecuredHandler.handleRequest(exchange);
+ return;
+ }
+
+ if (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.DENY) {
+ exchange.setResponseCode(403);
+ exchange.endExchange();
+ }
+ exchange.getSecurityContext().setAuthenticationRequired();
+ exchange.putAttachment(CONSTRAINT_KEY, match);
+ securedHandler.handleRequest(exchange);
+
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java
new file mode 100755
index 0000000..f236579
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java
@@ -0,0 +1,278 @@
+package org.keycloak.proxy;
+
+import io.undertow.Undertow;
+import io.undertow.security.api.AuthenticationMechanism;
+import io.undertow.security.api.AuthenticationMode;
+import io.undertow.security.handlers.AuthenticationCallHandler;
+import io.undertow.security.handlers.AuthenticationConstraintHandler;
+import io.undertow.security.handlers.AuthenticationMechanismsHandler;
+import io.undertow.security.handlers.SecurityInitialHandler;
+import io.undertow.security.idm.Account;
+import io.undertow.security.idm.Credential;
+import io.undertow.security.idm.IdentityManager;
+import io.undertow.security.impl.CachedAuthenticatedSessionMechanism;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.PathHandler;
+import io.undertow.server.handlers.ResponseCodeHandler;
+import io.undertow.server.handlers.proxy.ProxyHandler;
+import io.undertow.server.handlers.proxy.SimpleProxyClientProvider;
+import io.undertow.server.session.InMemorySessionManager;
+import io.undertow.server.session.SessionAttachmentHandler;
+import io.undertow.server.session.SessionCookieConfig;
+import io.undertow.server.session.SessionManager;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.NodesRegistrationManagement;
+import org.keycloak.adapters.undertow.UndertowAuthenticatedActionsHandler;
+import org.keycloak.adapters.undertow.UndertowAuthenticationMechanism;
+import org.keycloak.adapters.undertow.UndertowPreAuthActionsHandler;
+import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
+import org.keycloak.enums.SslRequired;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.xnio.Option;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class ProxyServerBuilder {
+ public static final HttpHandler NOT_FOUND = new HttpHandler() {
+ @Override
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
+ exchange.setResponseCode(404);
+ exchange.endExchange();
+ }
+ };
+
+ protected Undertow.Builder builder = Undertow.builder();
+
+ protected PathHandler root = new PathHandler(NOT_FOUND);
+ protected HttpHandler proxyHandler;
+
+ public ProxyServerBuilder target(String uri) {
+ SimpleProxyClientProvider provider = null;
+ try {
+ provider = new SimpleProxyClientProvider(new URI(uri));
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ final HttpHandler handler = new ProxyHandler(provider, 30000, ResponseCodeHandler.HANDLE_404);
+ proxyHandler = new HttpHandler() {
+ @Override
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
+ exchange.setRelativePath(exchange.getResolvedPath()); // need this otherwise proxy forwards to chopped off path
+ handler.handleRequest(exchange);
+ }
+ };
+ return this;
+ }
+
+ public ApplicationBuilder application(AdapterConfig config) {
+ return new ApplicationBuilder(config);
+ }
+
+ public class ApplicationBuilder {
+ protected NodesRegistrationManagement nodesRegistrationManagement = new NodesRegistrationManagement();
+ protected UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
+ protected AdapterDeploymentContext deploymentContext;
+ protected KeycloakDeployment deployment;
+ SessionManager sessionManager = new InMemorySessionManager(
+ "SESSION_MANAGER");
+ protected String base;
+ protected SecurityPathMatches.Builder constraintBuilder = new SecurityPathMatches.Builder();
+ protected SecurityPathMatches matches;
+
+ public ApplicationBuilder base(String base) {
+ this.base = base;
+ return this;
+ }
+
+ public ApplicationBuilder(AdapterConfig config) {
+ this.deployment = KeycloakDeploymentBuilder.build(config);
+ this.deploymentContext = new AdapterDeploymentContext(deployment);
+ }
+
+ public ProxyServerBuilder add() {
+ matches = constraintBuilder.build();
+ HttpHandler handler = sessionHandling(addSecurity(proxyHandler));
+ root.addPrefixPath(base, handler);
+ return ProxyServerBuilder.this;
+ }
+
+ public ConstraintBuilder constraint(String pattern) {
+ return new ConstraintBuilder(pattern);
+ }
+
+ public class ConstraintBuilder {
+ protected String pattern;
+ protected Set<String> rolesAllowed = new HashSet<String>();
+ protected Set<String> methods = new HashSet<String>();
+ protected Set<String> excludedMethods = new HashSet<String>();
+ protected SecurityInfo.EmptyRoleSemantic semantic = SecurityInfo.EmptyRoleSemantic.AUTHENTICATE;
+
+ public ConstraintBuilder(String pattern) {
+ this.pattern = pattern;
+
+ }
+
+ public ConstraintBuilder deny() {
+ semantic = SecurityInfo.EmptyRoleSemantic.DENY;
+ return this;
+ }
+ public ConstraintBuilder permit() {
+ semantic = SecurityInfo.EmptyRoleSemantic.PERMIT;
+ return this;
+ }
+ public ConstraintBuilder authenticate() {
+ semantic = SecurityInfo.EmptyRoleSemantic.AUTHENTICATE;
+ return this;
+ }
+
+ public ConstraintBuilder method(String method) {
+ methods.add(method);
+ return this;
+ }
+
+ public ConstraintBuilder excludeMethod(String method) {
+ excludedMethods.add(method);
+ return this;
+ }
+
+
+ public ConstraintBuilder roles(String... roles) {
+ for (String role : roles) role(role);
+ return this;
+ }
+
+ public ConstraintBuilder role(String role) {
+ rolesAllowed.add(role);
+ return this;
+ }
+
+ public ApplicationBuilder add() {
+ constraintBuilder.addSecurityConstraint(rolesAllowed, semantic, pattern, methods, excludedMethods);
+ return ApplicationBuilder.this;
+ }
+
+
+ }
+
+ private HttpHandler addSecurity(final HttpHandler toWrap) {
+ HttpHandler handler = toWrap;
+ handler = new UndertowAuthenticatedActionsHandler(deploymentContext, toWrap);
+ handler = new AuthenticationCallHandler(handler);
+ handler = new ConstraintMatcherHandler(matches, handler, toWrap);
+ final List<AuthenticationMechanism> mechanisms = new LinkedList<AuthenticationMechanism>();
+ mechanisms.add(new CachedAuthenticatedSessionMechanism());
+ mechanisms.add(new UndertowAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, -1));
+ handler = new AuthenticationMechanismsHandler(handler, mechanisms);
+ IdentityManager identityManager = new IdentityManager() {
+ @Override
+ public Account verify(Account account) {
+ return account;
+ }
+
+ @Override
+ public Account verify(String id, Credential credential) {
+ throw new IllegalStateException("Should never be called in Keycloak flow");
+ }
+
+ @Override
+ public Account verify(Credential credential) {
+ throw new IllegalStateException("Should never be called in Keycloak flow");
+ }
+ };
+ handler = new UndertowPreAuthActionsHandler(deploymentContext, userSessionManagement, sessionManager, handler);
+ return new SecurityInitialHandler(AuthenticationMode.PRO_ACTIVE, identityManager, handler);
+ }
+
+ private HttpHandler sessionHandling(HttpHandler toWrap) {
+ SessionCookieConfig sessionConfig = new SessionCookieConfig();
+ sessionConfig.setCookieName("keycloak." + deployment.getResourceName() + ".session");
+ sessionConfig.setPath(base);
+ if (deployment.getSslRequired() == SslRequired.ALL) sessionConfig.setSecure(true);
+ toWrap = new SessionAttachmentHandler(
+ toWrap, sessionManager, sessionConfig);
+ return toWrap;
+ }
+
+ }
+
+
+ public Undertow build() {
+ builder.setHandler(root);
+ return builder.build();
+ }
+
+ public ProxyServerBuilder addHttpListener(int port, String host) {
+ builder.addHttpListener(port, host);
+ return this;
+ }
+
+ public ProxyServerBuilder addHttpsListener(int port, String host, KeyManager[] keyManagers, TrustManager[] trustManagers) {
+ builder.addHttpsListener(port, host, keyManagers, trustManagers);
+ return this;
+ }
+
+ public ProxyServerBuilder addHttpsListener(int port, String host, SSLContext sslContext) {
+ builder.addHttpsListener(port, host, sslContext);
+ return this;
+ }
+
+ public ProxyServerBuilder setBufferSize(int bufferSize) {
+ builder.setBufferSize(bufferSize);
+ return this;
+ }
+
+ public ProxyServerBuilder setBuffersPerRegion(int buffersPerRegion) {
+ builder.setBuffersPerRegion(buffersPerRegion);
+ return this;
+ }
+
+ public ProxyServerBuilder setIoThreads(int ioThreads) {
+ builder.setIoThreads(ioThreads);
+ return this;
+ }
+
+ public ProxyServerBuilder setWorkerThreads(int workerThreads) {
+ builder.setWorkerThreads(workerThreads);
+ return this;
+ }
+
+ public ProxyServerBuilder setDirectBuffers(boolean directBuffers) {
+ builder.setDirectBuffers(directBuffers);
+ return this;
+ }
+
+ public ProxyServerBuilder setHandler(HttpHandler handler) {
+ builder.setHandler(handler);
+ return this;
+ }
+
+ public <T> ProxyServerBuilder setServerOption(Option<T> option, T value) {
+ builder.setServerOption(option, value);
+ return this;
+ }
+
+ public <T> ProxyServerBuilder setSocketOption(Option<T> option, T value) {
+ builder.setSocketOption(option, value);
+ return this;
+ }
+
+ public <T> ProxyServerBuilder setWorkerOption(Option<T> option, T value) {
+ builder.setWorkerOption(option, value);
+ return this;
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/RoleAuthHandler.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/RoleAuthHandler.java
new file mode 100755
index 0000000..3beb870
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/RoleAuthHandler.java
@@ -0,0 +1,43 @@
+package org.keycloak.proxy;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
+
+import java.util.Collection;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class RoleAuthHandler implements HttpHandler {
+
+ protected Collection<String> roles;
+ protected HttpHandler next;
+
+ public RoleAuthHandler(Collection<String> roles, HttpHandler next) {
+ this.roles = roles;
+ this.next = next;
+ }
+
+ @Override
+ public void handleRequest(HttpServerExchange exchange) throws Exception {
+ KeycloakUndertowAccount account = (KeycloakUndertowAccount)exchange.getSecurityContext().getAuthenticatedAccount();
+ SingleConstraintMatch match = exchange.getAttachment(ConstraintMatcherHandler.CONSTRAINT_KEY);
+ if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.AUTHENTICATE)) {
+ next.handleRequest(exchange);
+ return;
+ }
+ if (match != null) {
+ for (String role : match.getRequiredRoles()) {
+ if (account.getRoles().contains(role)) {
+ next.handleRequest(exchange);
+ return;
+ }
+ }
+ }
+ exchange.setResponseCode(403);
+ exchange.endExchange();
+
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java
new file mode 100755
index 0000000..5d29137
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java
@@ -0,0 +1,93 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2014 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.proxy;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Stuart Douglas
+ */
+public class SecurityInfo<T extends SecurityInfo> implements Cloneable {
+
+ /**
+ * Equivalent to {@see ServletSecurity.EmptyRoleSemantic} but with an additional mode to require authentication but no role
+ * check.
+ */
+ public enum EmptyRoleSemantic {
+
+ /**
+ * Permit access to the resource without requiring authentication or role membership.
+ */
+ PERMIT,
+
+ /**
+ * Deny access to the resource regardless of the authentication state.
+ */
+ DENY,
+
+ /**
+ * Mandate authentication but authorize access as no roles to check against.
+ */
+ AUTHENTICATE;
+
+ }
+
+ private volatile EmptyRoleSemantic emptyRoleSemantic = EmptyRoleSemantic.DENY;
+ private final Set<String> rolesAllowed = new HashSet<String>();
+
+ public EmptyRoleSemantic getEmptyRoleSemantic() {
+ return emptyRoleSemantic;
+ }
+
+ public T setEmptyRoleSemantic(final EmptyRoleSemantic emptyRoleSemantic) {
+ this.emptyRoleSemantic = emptyRoleSemantic;
+ return (T)this;
+ }
+
+ public T addRoleAllowed(final String role) {
+ this.rolesAllowed.add(role);
+ return (T) this;
+ }
+
+ public T addRolesAllowed(final String ... roles) {
+ this.rolesAllowed.addAll(Arrays.asList(roles));
+ return (T) this;
+ }
+ public T addRolesAllowed(final Collection<String> roles) {
+ this.rolesAllowed.addAll(roles);
+ return (T) this;
+ }
+ public Set<String> getRolesAllowed() {
+ return new HashSet<String>(rolesAllowed);
+ }
+
+ @Override
+ public T clone() {
+ final SecurityInfo info = createInstance();
+ info.emptyRoleSemantic = emptyRoleSemantic;
+ info.rolesAllowed.addAll(rolesAllowed);
+ return (T) info;
+ }
+
+ protected T createInstance() {
+ return (T) new SecurityInfo();
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatch.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatch.java
new file mode 100755
index 0000000..857d8fd
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatch.java
@@ -0,0 +1,36 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2014 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.proxy;
+
+
+/**
+ * @author Stuart Douglas
+ */
+public class SecurityPathMatch {
+
+ private final SingleConstraintMatch mergedConstraint;
+
+ SecurityPathMatch(final SingleConstraintMatch mergedConstraint) {
+ this.mergedConstraint = mergedConstraint;
+ }
+
+
+ SingleConstraintMatch getMergedConstraint() {
+ return mergedConstraint;
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatches.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatches.java
new file mode 100755
index 0000000..c5c39a0
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityPathMatches.java
@@ -0,0 +1,250 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2014 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.proxy;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Stuart Douglas
+ */
+public class SecurityPathMatches {
+
+ private final boolean denyUncoveredHttpMethods;
+ private final PathSecurityInformation defaultPathSecurityInformation;
+ private final Map<String, PathSecurityInformation> exactPathRoleInformation;
+ private final Map<String, PathSecurityInformation> prefixPathRoleInformation;
+ private final Map<String, PathSecurityInformation> extensionRoleInformation;
+
+ private SecurityPathMatches(final boolean denyUncoveredHttpMethods, final PathSecurityInformation defaultPathSecurityInformation, final Map<String, PathSecurityInformation> exactPathRoleInformation, final Map<String, PathSecurityInformation> prefixPathRoleInformation, final Map<String, PathSecurityInformation> extensionRoleInformation) {
+ this.denyUncoveredHttpMethods = denyUncoveredHttpMethods;
+ this.defaultPathSecurityInformation = defaultPathSecurityInformation;
+ this.exactPathRoleInformation = exactPathRoleInformation;
+ this.prefixPathRoleInformation = prefixPathRoleInformation;
+ this.extensionRoleInformation = extensionRoleInformation;
+ }
+
+ /**
+ *
+ * @return <code>true</code> If no security path information has been defined
+ */
+ public boolean isEmpty() {
+ return defaultPathSecurityInformation.excludedMethodRoles.isEmpty() &&
+ defaultPathSecurityInformation.perMethodRequiredRoles.isEmpty() &&
+ defaultPathSecurityInformation.defaultRequiredRoles.isEmpty() &&
+ exactPathRoleInformation.isEmpty() &&
+ prefixPathRoleInformation.isEmpty() &&
+ extensionRoleInformation.isEmpty();
+ }
+
+ public SecurityPathMatch getSecurityInfo(final String path, final String method) {
+ RuntimeMatch currentMatch = new RuntimeMatch();
+ handleMatch(method, defaultPathSecurityInformation, currentMatch);
+ PathSecurityInformation match = exactPathRoleInformation.get(path);
+ if (match != null) {
+ handleMatch(method, match, currentMatch);
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+
+ match = prefixPathRoleInformation.get(path);
+ if (match != null) {
+ handleMatch(method, match, currentMatch);
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+
+ int qsPos = -1;
+ boolean extension = false;
+ for (int i = path.length() - 1; i >= 0; --i) {
+ final char c = path.charAt(i);
+ if (c == '?') {
+ //there was a query string, check the exact matches again
+ final String part = path.substring(0, i);
+ match = exactPathRoleInformation.get(part);
+ if (match != null) {
+ handleMatch(method, match, currentMatch);
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+ qsPos = i;
+ extension = false;
+ } else if (c == '/') {
+ extension = true;
+ final String part = path.substring(0, i);
+ match = prefixPathRoleInformation.get(part);
+ if (match != null) {
+ handleMatch(method, match, currentMatch);
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+ } else if (c == '.') {
+ if (!extension) {
+ extension = true;
+ final String ext;
+ if (qsPos == -1) {
+ ext = path.substring(i + 1, path.length());
+ } else {
+ ext = path.substring(i + 1, qsPos);
+ }
+ match = extensionRoleInformation.get(ext);
+ if (match != null) {
+ handleMatch(method, match, currentMatch);
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+ }
+ }
+ }
+ return new SecurityPathMatch(mergeConstraints(currentMatch));
+ }
+
+ /**
+ * merge all constraints, as per 13.8.1 Combining Constraints
+ */
+ private SingleConstraintMatch mergeConstraints(final RuntimeMatch currentMatch) {
+ if(currentMatch.uncovered && denyUncoveredHttpMethods) {
+ return new SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic.DENY, Collections.<String>emptySet());
+ }
+ final Set<String> allowedRoles = new HashSet<String>();
+ for(SingleConstraintMatch match : currentMatch.constraints) {
+ if(match.getRequiredRoles().isEmpty()) {
+ return new SingleConstraintMatch(match.getEmptyRoleSemantic(), Collections.<String>emptySet());
+ } else {
+ allowedRoles.addAll(match.getRequiredRoles());
+ }
+ }
+ return new SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic.PERMIT, allowedRoles);
+ }
+
+ private void handleMatch(final String method, final PathSecurityInformation exact, RuntimeMatch currentMatch) {
+ List<SecurityInformation> roles = exact.defaultRequiredRoles;
+ for (SecurityInformation role : roles) {
+ currentMatch.constraints.add(new SingleConstraintMatch(role.emptyRoleSemantic, role.roles));
+ if(role.emptyRoleSemantic == SecurityInfo.EmptyRoleSemantic.DENY || !role.roles.isEmpty()) {
+ currentMatch.uncovered = false;
+ }
+ }
+ List<SecurityInformation> methodInfo = exact.perMethodRequiredRoles.get(method);
+ if (methodInfo != null) {
+ currentMatch.uncovered = false;
+ for (SecurityInformation role : methodInfo) {
+ currentMatch.constraints.add(new SingleConstraintMatch(role.emptyRoleSemantic, role.roles));
+ }
+ }
+ for (ExcludedMethodRoles excluded : exact.excludedMethodRoles) {
+ if (!excluded.methods.contains(method)) {
+ currentMatch.uncovered = false;
+ currentMatch.constraints.add(new SingleConstraintMatch(excluded.securityInformation.emptyRoleSemantic, excluded.securityInformation.roles));
+ }
+ }
+ }
+
+ public static class Builder {
+ private final PathSecurityInformation defaultPathSecurityInformation = new PathSecurityInformation();
+ private final Map<String, PathSecurityInformation> exactPathRoleInformation = new HashMap<String, PathSecurityInformation>();
+ private final Map<String, PathSecurityInformation> prefixPathRoleInformation = new HashMap<String, PathSecurityInformation>();
+ private final Map<String, PathSecurityInformation> extensionRoleInformation = new HashMap<String, PathSecurityInformation>();
+
+ public void addSecurityConstraint(Set<String> roles, SecurityInfo.EmptyRoleSemantic emptyRoleSemantic, String pattern, Set<String> httpMethods, Set<String> excludedMethods) {
+ final SecurityInformation securityInformation = new SecurityInformation(roles, emptyRoleSemantic);
+ if (pattern.endsWith("/*") || pattern.endsWith("/")) {
+ String part = pattern.substring(0, pattern.lastIndexOf('/'));
+ PathSecurityInformation info = prefixPathRoleInformation.get(part);
+ if (info == null) {
+ prefixPathRoleInformation.put(part, info = new PathSecurityInformation());
+ }
+ setupPathSecurityInformation(info, securityInformation, httpMethods, excludedMethods);
+ } else if (pattern.startsWith("*.")) {
+ String part = pattern.substring(2, pattern.length());
+ PathSecurityInformation info = extensionRoleInformation.get(part);
+ if (info == null) {
+ extensionRoleInformation.put(part, info = new PathSecurityInformation());
+ }
+ setupPathSecurityInformation(info, securityInformation, httpMethods, excludedMethods);
+ } else {
+ PathSecurityInformation info = exactPathRoleInformation.get(pattern);
+ if (info == null) {
+ exactPathRoleInformation.put(pattern, info = new PathSecurityInformation());
+ }
+ setupPathSecurityInformation(info, securityInformation, httpMethods, excludedMethods);
+ }
+
+ }
+
+ private Set<String> expandRolesAllowed(final Set<String> rolesAllowed) {
+ final Set<String> roles = new HashSet<String>(rolesAllowed);
+ return roles;
+ }
+
+ private void setupPathSecurityInformation(final PathSecurityInformation info, final SecurityInformation securityConstraint,
+ Set<String> httpMethods, Set<String> excludedMethods) {
+ if (httpMethods.isEmpty() &&
+ excludedMethods.isEmpty()) {
+ info.defaultRequiredRoles.add(securityConstraint);
+ } else if (!httpMethods.isEmpty()) {
+ for (String method : httpMethods) {
+ List<SecurityInformation> securityInformations = info.perMethodRequiredRoles.get(method);
+ if (securityInformations == null) {
+ info.perMethodRequiredRoles.put(method, securityInformations = new ArrayList<SecurityInformation>());
+ }
+ securityInformations.add(securityConstraint);
+ }
+ } else if (!excludedMethods.isEmpty()) {
+ info.excludedMethodRoles.add(new ExcludedMethodRoles(excludedMethods, securityConstraint));
+ }
+ }
+
+ public SecurityPathMatches build() {
+ return new SecurityPathMatches(false, defaultPathSecurityInformation, exactPathRoleInformation, prefixPathRoleInformation, extensionRoleInformation);
+ }
+ }
+
+
+ private static class PathSecurityInformation {
+ final List<SecurityInformation> defaultRequiredRoles = new ArrayList<SecurityInformation>();
+ final Map<String, List<SecurityInformation>> perMethodRequiredRoles = new HashMap<String, List<SecurityInformation>>();
+ final List<ExcludedMethodRoles> excludedMethodRoles = new ArrayList<ExcludedMethodRoles>();
+ }
+
+ private static final class ExcludedMethodRoles {
+ final Set<String> methods;
+ final SecurityInformation securityInformation;
+
+ public ExcludedMethodRoles(final Set<String> methods, final SecurityInformation securityInformation) {
+ this.methods = methods;
+ this.securityInformation = securityInformation;
+ }
+ }
+
+ private static final class SecurityInformation {
+ final Set<String> roles;
+ final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic;
+
+ private SecurityInformation(final Set<String> roles, final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic) {
+ this.emptyRoleSemantic = emptyRoleSemantic;
+ this.roles = new HashSet<String>(roles);
+ }
+ }
+
+ private static final class RuntimeMatch {
+ final List<SingleConstraintMatch> constraints = new ArrayList<SingleConstraintMatch>();
+ boolean uncovered = true;
+ }
+}
diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SingleConstraintMatch.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SingleConstraintMatch.java
new file mode 100755
index 0000000..cd0b242
--- /dev/null
+++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SingleConstraintMatch.java
@@ -0,0 +1,48 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2014 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.proxy;
+
+import java.util.Set;
+
+/**
+ * Representation of a single security constrain matched for a single request.
+ *
+ * When performing any authentication/authorization check every constraint MUST be satisfied for the request to be allowed to
+ * proceed.
+ *
+ * @author <a href="mailto:darran.lofthouse@jboss.com">Darran Lofthouse</a>
+ */
+public class SingleConstraintMatch {
+
+ private final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic;
+ private final Set<String> requiredRoles;
+
+ public SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic emptyRoleSemantic, Set<String> requiredRoles) {
+ this.emptyRoleSemantic = emptyRoleSemantic;
+ this.requiredRoles = requiredRoles;
+ }
+
+ public SecurityInfo.EmptyRoleSemantic getEmptyRoleSemantic() {
+ return emptyRoleSemantic;
+ }
+
+ public Set<String> getRequiredRoles() {
+ return requiredRoles;
+ }
+
+}
testsuite/pom.xml 1(+1 -0)
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index db0fad4..2c582f6 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -26,6 +26,7 @@
</build>
<modules>
<module>integration</module>
+ <module>proxy</module>
<module>tomcat6</module>
<module>tomcat7</module>
<module>tomcat8</module>
testsuite/proxy/pom.xml 509(+509 -0)
diff --git a/testsuite/proxy/pom.xml b/testsuite/proxy/pom.xml
new file mode 100755
index 0000000..892b121
--- /dev/null
+++ b/testsuite/proxy/pom.xml
@@ -0,0 +1,509 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-testsuite-pom</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.1.0.Beta2-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-testsuite-security-proxy</artifactId>
+ <name>Keycloak Security Proxy TestSuite</name>
+ <properties>
+ <!--<tomcat.version>8.0.14</tomcat.version>-->
+ <tomcat.version>7.0.54</tomcat.version>
+ </properties>
+ <description />
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-dependencies-server-all</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-admin-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-proxy-server</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>jaxrs-api</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jaxrs</artifactId>
+ <version>${resteasy.version.latest}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-client</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-crypto</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-multipart-provider</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-jackson-provider</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.resteasy</groupId>
+ <artifactId>resteasy-undertow</artifactId>
+ <version>${resteasy.version.latest}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.zxing</groupId>
+ <artifactId>javase</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.bouncycastle</groupId>
+ <artifactId>bcprov-jdk16</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>${keycloak.apache.httpcomponents.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-ldap-federation</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-undertow-adapter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-tomcat7-adapter</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-servlet</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.undertow</groupId>
+ <artifactId>undertow-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-core-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-xc</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate.javax.persistence</groupId>
+ <artifactId>hibernate-jpa-2.0-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hibernate</groupId>
+ <artifactId>hibernate-entitymanager</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.icegreen</groupId>
+ <artifactId>greenmail</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.infinispan</groupId>
+ <artifactId>infinispan-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-java</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>xml-apis</groupId>
+ <artifactId>xml-apis</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-chrome-driver</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.wildfly</groupId>
+ <artifactId>wildfly-undertow</artifactId>
+ <version>${wildfly.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-testsuite-integration</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-testsuite-integration</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>tomcat-catalina</artifactId>
+ <version>${tomcat.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat</groupId>
+ <artifactId>tomcat-util</artifactId>
+ <version>${tomcat.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat.embed</groupId>
+ <artifactId>tomcat-embed-core</artifactId>
+ <version>${tomcat.version}</version>
+ </dependency>
+
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.2</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>${maven.compiler.source}</source>
+ <target>${maven.compiler.target}</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <workingDirectory>${project.basedir}</workingDirectory>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>keycloak-server</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.KeycloakServer</mainClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>mail-server</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.MailServer</mainClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>totp</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <configuration>
+ <mainClass>org.keycloak.testutils.TotpGenerator</mainClass>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>jpa</id>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <keycloak.realm.provider>jpa</keycloak.realm.provider>
+ <keycloak.user.provider>jpa</keycloak.user.provider>
+ <keycloak.eventStore.provider>jpa</keycloak.eventStore.provider>
+ <keycloak.userSessions.provider>jpa</keycloak.userSessions.provider>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>mongo</id>
+
+ <properties>
+ <keycloak.connectionsMongo.host>localhost</keycloak.connectionsMongo.host>
+ <keycloak.connectionsMongo.port>27018</keycloak.connectionsMongo.port>
+ <keycloak.connectionsMongo.db>keycloak</keycloak.connectionsMongo.db>
+ <keycloak.connectionsMongo.clearOnStartup>true</keycloak.connectionsMongo.clearOnStartup>
+ <keycloak.connectionsMongo.bindIp>127.0.0.1</keycloak.connectionsMongo.bindIp>
+ </properties>
+
+ <build>
+ <plugins>
+
+ <!-- Postpone tests to "integration-test" phase, so that we can bootstrap embedded mongo on 27018 before running tests -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>test</id>
+ <phase>integration-test</phase>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <configuration>
+ <systemPropertyVariables>
+ <keycloak.realm.provider>mongo</keycloak.realm.provider>
+ <keycloak.user.provider>mongo</keycloak.user.provider>
+ <keycloak.audit.provider>mongo</keycloak.audit.provider>
+ <keycloak.userSessions.provider>mongo</keycloak.userSessions.provider>
+ <keycloak.connectionsMongo.host>${keycloak.connectionsMongo.host}</keycloak.connectionsMongo.host>
+ <keycloak.connectionsMongo.port>${keycloak.connectionsMongo.port}</keycloak.connectionsMongo.port>
+ <keycloak.connectionsMongo.db>${keycloak.connectionsMongo.db}</keycloak.connectionsMongo.db>
+ <keycloak.connectionsMongo.clearOnStartup>${keycloak.connectionsMongo.clearOnStartup}</keycloak.connectionsMongo.clearOnStartup>
+ <keycloak.connectionsMongo.bindIp>${keycloak.connectionsMongo.bindIp}</keycloak.connectionsMongo.bindIp>
+ </systemPropertyVariables>
+ </configuration>
+ </execution>
+ <execution>
+ <id>default-test</id>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ <!-- Embedded mongo -->
+ <plugin>
+ <groupId>com.github.joelittlejohn.embedmongo</groupId>
+ <artifactId>embedmongo-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>start-mongodb</id>
+ <phase>pre-integration-test</phase>
+ <goals>
+ <goal>start</goal>
+ </goals>
+ <configuration>
+ <port>${keycloak.connectionsMongo.port}</port>
+ <logging>file</logging>
+ <logFile>${project.build.directory}/mongodb.log</logFile>
+ <bindIp>${keycloak.connectionsMongo.bindIp}</bindIp>
+ </configuration>
+ </execution>
+ <execution>
+ <id>stop-mongodb</id>
+ <phase>post-integration-test</phase>
+ <goals>
+ <goal>stop</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ </profile>
+
+ <profile>
+ <id>infinispan</id>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <keycloak.realm.cache.provider>infinispan</keycloak.realm.cache.provider>
+ <keycloak.user.cache.provider>infinispan</keycloak.user.cache.provider>
+ <keycloak.userSessions.provider>infinispan</keycloak.userSessions.provider>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <!-- MySQL -->
+ <profile>
+ <activation>
+ <property>
+ <name>keycloak.connectionsJpa.driver</name>
+ <value>com.mysql.jdbc.Driver</value>
+ </property>
+ </activation>
+ <id>mysql</id>
+ <dependencies>
+ <dependency>
+ <groupId>mysql</groupId>
+ <artifactId>mysql-connector-java</artifactId>
+ <version>${mysql.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+
+ <!-- PostgreSQL -->
+ <profile>
+ <activation>
+ <property>
+ <name>keycloak.connectionsJpa.driver</name>
+ <value>org.postgresql.Driver</value>
+ </property>
+ </activation>
+ <id>postgresql</id>
+ <dependencies>
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <version>${postgresql.version}</version>
+ </dependency>
+ </dependencies>
+ </profile>
+
+ <profile>
+ <id>clean-jpa</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.liquibase</groupId>
+ <artifactId>liquibase-maven-plugin</artifactId>
+ <configuration>
+ <changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
+
+ <url>${keycloak.connectionsJpa.url}</url>
+ <driver>${keycloak.connectionsJpa.driver}</driver>
+ <username>${keycloak.connectionsJpa.user}</username>
+ <password>${keycloak.connectionsJpa.password}</password>
+
+ <promptOnNonLocalDatabase>false</promptOnNonLocalDatabase>
+ </configuration>
+ <executions>
+ <execution>
+ <id>clean-jpa</id>
+ <phase>clean</phase>
+ <goals>
+ <goal>dropAll</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/testsuite/proxy/src/test/java/org/keycloak/testsuite/ProxyTest.java b/testsuite/proxy/src/test/java/org/keycloak/testsuite/ProxyTest.java
new file mode 100755
index 0000000..ecd0d59
--- /dev/null
+++ b/testsuite/proxy/src/test/java/org/keycloak/testsuite/ProxyTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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;
+
+import io.undertow.Undertow;
+import io.undertow.io.IoCallback;
+import io.undertow.security.api.SecurityContext;
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.server.handlers.ResponseCodeHandler;
+import io.undertow.server.handlers.proxy.ProxyHandler;
+import io.undertow.server.handlers.proxy.SimpleProxyClientProvider;
+import io.undertow.util.Headers;
+import io.undertow.util.HttpString;
+import org.apache.catalina.startup.Tomcat;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.adapters.KeycloakDeploymentBuilder;
+import org.keycloak.adapters.undertow.AbstractUndertowRequestAuthenticator;
+import org.keycloak.adapters.undertow.UndertowHttpFacade;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OpenIDConnectService;
+import org.keycloak.proxy.ProxyServerBuilder;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.keycloak.testsuite.rule.WebResource;
+import org.keycloak.testsuite.rule.WebRule;
+import org.keycloak.testutils.KeycloakServer;
+import org.openqa.selenium.WebDriver;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.UriBuilder;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URL;
+import java.security.Principal;
+import java.util.regex.Matcher;
+
+/**
+ * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class ProxyTest {
+ static String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8080/customer-portal").build("demo").toString();
+
+ @ClassRule
+ public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() {
+ @Override
+ protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
+ RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/tomcat-test/demorealm.json"), RealmRepresentation.class);
+ RealmModel realm = manager.importRealm(representation);
+ }
+ };
+
+ public static class SendUsernameServlet extends HttpServlet {
+ @Override
+ protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ resp.setContentType("text/plain");
+ OutputStream stream = resp.getOutputStream();
+ stream.write(req.getRequestURL().toString().getBytes());
+ stream.write("\n".getBytes());
+ Integer count = (Integer)req.getSession().getAttribute("counter");
+ if (count == null) count = new Integer(0);
+ req.getSession().setAttribute("counter", new Integer(count.intValue() + 1));
+ stream.write(count.toString().getBytes());
+
+
+
+ }
+ @Override
+ protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
+ doGet(req, resp);
+ }
+ }
+
+ static Tomcat tomcat = null;
+
+ public static void initTomcat() throws Exception {
+ URL dir = ProxyTest.class.getResource("/tomcat-test/webapp/WEB-INF/web.xml");
+ File webappDir = new File(dir.getFile()).getParentFile().getParentFile();
+ tomcat = new Tomcat();
+ String baseDir = getBaseDirectory();
+ tomcat.setBaseDir(baseDir);
+ tomcat.setPort(8082);
+
+ tomcat.addWebapp("/customer-portal", webappDir.toString());
+ System.out.println("configuring app with basedir: " + webappDir.toString());
+
+ tomcat.start();
+ //tomcat.getServer().await();
+ }
+
+ public static void shutdownTomcat() throws Exception {
+ tomcat.stop();
+ tomcat.destroy();
+ }
+
+ static Undertow proxyServer = null;
+
+ //@BeforeClass
+ public static void initProxy() throws Exception {
+ initTomcat();
+ ProxyServerBuilder builder = new ProxyServerBuilder().addHttpListener(8080, "localhost");
+ InputStream is = ProxyTest.class.getResourceAsStream("/keycloak.json");
+ AdapterConfig config = KeycloakDeploymentBuilder.loadAdapterConfig(is);
+
+ builder.target("http://localhost:8082")
+ .application(config)
+ .base("/customer-portal")
+ .constraint("/*").add().add();
+ proxyServer = builder.build();
+ proxyServer.start();
+
+ }
+
+ @AfterClass
+ public static void shutdownProxy() throws Exception {
+ shutdownTomcat();
+ if (proxyServer != null) proxyServer.stop();
+ }
+
+
+ @Rule
+ public WebRule webRule = new WebRule(this);
+ @WebResource
+ protected WebDriver driver;
+ @WebResource
+ protected LoginPage loginPage;
+
+ public static final String LOGIN_URL = OpenIDConnectService.loginPageUrl(UriBuilder.fromUri("http://localhost:8081/auth")).build("demo").toString();
+
+ @Test
+ public void testLoginSSOAndLogout() throws Exception {
+ initProxy();
+ driver.navigate().to("http://localhost:8080/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:8080/customer-portal");
+ String pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("customer-portal"));
+ Assert.assertTrue(pageSource.contains("0"));
+ driver.navigate().to("http://localhost:8080/customer-portal");
+ Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/customer-portal");
+ pageSource = driver.getPageSource();
+ System.out.println(pageSource);
+ Assert.assertTrue(pageSource.contains("customer-portal"));
+ Assert.assertTrue(pageSource.contains("1")); // test http session
+
+ // test logout
+
+ String logoutUri = OpenIDConnectService.logoutUrl(UriBuilder.fromUri("http://localhost:8081/auth"))
+ .queryParam(OAuth2Constants.REDIRECT_URI, "http://localhost:8080/customer-portal").build("demo").toString();
+ driver.navigate().to(logoutUri);
+ Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
+ driver.navigate().to("http://localhost:8080/customer-portal");
+ String currentUrl = driver.getCurrentUrl();
+ Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
+
+
+ }
+
+ @Test
+ @Ignore
+ public void runit() throws Exception {
+ Thread.sleep(10000000);
+ }
+
+
+ private static String getBaseDirectory() {
+ String dirPath = null;
+ String relativeDirPath = "testsuite" + File.separator + "proxy" + File.separator + "target";
+
+ if (System.getProperties().containsKey("maven.home")) {
+ dirPath = System.getProperty("user.dir").replaceFirst("testsuite.proxy.*", Matcher.quoteReplacement(relativeDirPath));
+ } else {
+ for (String c : System.getProperty("java.class.path").split(File.pathSeparator)) {
+ if (c.contains(File.separator + "testsuite" + File.separator + "proxy")) {
+ dirPath = c.replaceFirst("testsuite.proxy.*", Matcher.quoteReplacement(relativeDirPath));
+ break;
+ }
+ }
+ }
+
+ String absolutePath = new File(dirPath).getAbsolutePath();
+ return absolutePath;
+ }
+
+
+
+
+}
diff --git a/testsuite/proxy/src/test/resources/keycloak.json b/testsuite/proxy/src/test/resources/keycloak.json
new file mode 100755
index 0000000..e49233f
--- /dev/null
+++ b/testsuite/proxy/src/test/resources/keycloak.json
@@ -0,0 +1,11 @@
+{
+ "realm": "demo",
+ "resource": "customer-portal",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8081/auth",
+ "ssl-required" : "external",
+ "principal-attribute": "name",
+ "credentials": {
+ "secret": "password"
+ }
+}
diff --git a/testsuite/proxy/src/test/resources/tomcat-test/demorealm.json b/testsuite/proxy/src/test/resources/tomcat-test/demorealm.json
new file mode 100755
index 0000000..a4a6ec9
--- /dev/null
+++ b/testsuite/proxy/src/test/resources/tomcat-test/demorealm.json
@@ -0,0 +1,58 @@
+{
+ "id": "demo",
+ "realm": "demo",
+ "enabled": true,
+ "accessTokenLifespan": 3000,
+ "accessCodeLifespan": 10,
+ "accessCodeLifespanUserAction": 6000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "social": false,
+ "passwordCredentialGrantAllowed": true,
+ "updateProfileOnInitialSocialLogin": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [ "password" ],
+ "users" : [
+ {
+ "username" : "bburke@redhat.com",
+ "enabled": true,
+ "email" : "bburke@redhat.com",
+ "firstName": "Bill",
+ "lastName": "Burke",
+ "credentials" : [
+ { "type" : "password",
+ "value" : "password" }
+ ],
+ "realmRoles": [ "user", "admin" ],
+ "applicationRoles": {
+ "account": [ "manage-account" ]
+ }
+ }
+ ],
+ "roles" : {
+ "realm" : [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ }
+ ]
+ },
+ "applications": [
+ {
+ "name": "customer-portal",
+ "enabled": true,
+ "fullScopeAllowed": true,
+ "adminUrl": "http://localhost:8080/customer-portal",
+ "baseUrl": "http://localhost:8080/customer-portal",
+ "redirectUris": [
+ "http://localhost:8080/customer-portal/*"
+ ],
+ "secret": "password"
+ }
+ ]
+}
diff --git a/testsuite/proxy/src/test/resources/tomcat-test/webapp/WEB-INF/web.xml b/testsuite/proxy/src/test/resources/tomcat-test/webapp/WEB-INF/web.xml
new file mode 100755
index 0000000..7e0b269
--- /dev/null
+++ b/testsuite/proxy/src/test/resources/tomcat-test/webapp/WEB-INF/web.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+
+ <module-name>adapter-test</module-name>
+
+ <servlet>
+ <servlet-name>SendUsername</servlet-name>
+ <servlet-class>org.keycloak.testsuite.ProxyTest$SendUsernameServlet</servlet-class>
+ </servlet>
+
+ <servlet-mapping>
+ <servlet-name>SendUsername</servlet-name>
+ <url-pattern>/*</url-pattern>
+ </servlet-mapping>
+
+</web-app>