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 8d8eed955c..e03eb51d3a 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/UndertowKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java similarity index 95% rename from integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java rename to integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java index 041b496858..1e49be9f18 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowKeycloakAuthMech.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java @@ -42,12 +42,12 @@ import org.keycloak.enums.TokenStore; * * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. */ -public abstract class UndertowKeycloakAuthMech implements AuthenticationMechanism { +public abstract class AbstractUndertowKeycloakAuthMech implements AuthenticationMechanism { public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); protected AdapterDeploymentContext deploymentContext; protected UndertowUserSessionManagement sessionManagement; - public UndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) { + public AbstractUndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) { this.deploymentContext = deploymentContext; this.sessionManagement = sessionManagement; } 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 0000000000..b9761e1397 --- /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 Bill Burke + * @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 principal) { + KeycloakUndertowAccount account = createAccount(principal); + securityContext.authenticationComplete(account, "KEYCLOAK", false); + propagateKeycloakContext(account); + tokenStore.saveAccountInfo(account); + } + + @Override + protected void completeBearerAuthentication(KeycloakPrincipal 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 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 7f18e907e0..0ae5b6eb78 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 Bill Burke * @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 ed13f5616f..d28ff29406 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 70f0beebeb..2168d9d1b6 --- 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 0000000000..057fc50f8d --- /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 Bill Burke + * @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 65b6ab278b..ccd695fac5 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 54552cc802..21738f53da 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 0000000000..0a70c67a6b --- /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 Bill Burke + * @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 6b9c3518f7..91fe9e8cd7 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 Bill Burke - * @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()); + SecurityContext securityContext, HttpServerExchange exchange, AdapterTokenStore tokenStore) { + super(facade, deployment, sslRedirectPort, securityContext, exchange, tokenStore); } @Override - protected OAuthRequestAuthenticator createOAuthAuthenticator() { - return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort) { - @Override - protected void saveRequest() { - // todo - } - }; + protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal) { + return new KeycloakUndertowAccount(principal); } - - @Override - protected void completeOAuthAuthentication(KeycloakPrincipal principal) { - KeycloakUndertowAccount account = createAccount(principal); - securityContext.authenticationComplete(account, "KEYCLOAK", false); - propagateKeycloakContext(account); - tokenStore.saveAccountInfo(account); - } - - @Override - protected void completeBearerAuthentication(KeycloakPrincipal 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 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 cc9e3d9bea..e5f013cfa7 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"); diff --git a/pom.xml b/pom.xml index ffeaa5eebe..ba351f100e 100755 --- a/pom.xml +++ b/pom.xml @@ -108,6 +108,7 @@ events model integration + proxy/proxy-server picketlink federation services diff --git a/proxy/proxy-server/pom.xml b/proxy/proxy-server/pom.xml new file mode 100755 index 0000000000..4b4d12adb9 --- /dev/null +++ b/proxy/proxy-server/pom.xml @@ -0,0 +1,87 @@ + + + + keycloak-parent + org.keycloak + 1.1.0.Beta2-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-proxy-server + Keycloak Proxy Server + + + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + provided + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-undertow-adapter + ${project.version} + + + org.keycloak + keycloak-adapter-core + ${project.version} + + + org.apache.httpcomponents + httpclient + ${keycloak.apache.httpcomponents.version} + + + net.iharder + base64 + + + org.bouncycastle + bcprov-jdk16 + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-xc + + + io.undertow + undertow-core + provided + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + 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 0000000000..bdbddc6b97 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class ConstraintMatcherHandler implements HttpHandler { + public static final AttachmentKey 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 0000000000..f236579a05 --- /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 Bill Burke + * @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 rolesAllowed = new HashSet(); + protected Set methods = new HashSet(); + protected Set excludedMethods = new HashSet(); + 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 mechanisms = new LinkedList(); + 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 ProxyServerBuilder setServerOption(Option option, T value) { + builder.setServerOption(option, value); + return this; + } + + public ProxyServerBuilder setSocketOption(Option option, T value) { + builder.setSocketOption(option, value); + return this; + } + + public ProxyServerBuilder setWorkerOption(Option 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 0000000000..3beb870de4 --- /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 Bill Burke + * @version $Revision: 1 $ + */ +public class RoleAuthHandler implements HttpHandler { + + protected Collection roles; + protected HttpHandler next; + + public RoleAuthHandler(Collection 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 0000000000..5d291373ab --- /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 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 rolesAllowed = new HashSet(); + + 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 roles) { + this.rolesAllowed.addAll(roles); + return (T) this; + } + public Set getRolesAllowed() { + return new HashSet(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 0000000000..857d8fd3d7 --- /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 0000000000..c5c39a013a --- /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 exactPathRoleInformation; + private final Map prefixPathRoleInformation; + private final Map extensionRoleInformation; + + private SecurityPathMatches(final boolean denyUncoveredHttpMethods, final PathSecurityInformation defaultPathSecurityInformation, final Map exactPathRoleInformation, final Map prefixPathRoleInformation, final Map extensionRoleInformation) { + this.denyUncoveredHttpMethods = denyUncoveredHttpMethods; + this.defaultPathSecurityInformation = defaultPathSecurityInformation; + this.exactPathRoleInformation = exactPathRoleInformation; + this.prefixPathRoleInformation = prefixPathRoleInformation; + this.extensionRoleInformation = extensionRoleInformation; + } + + /** + * + * @return true 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.emptySet()); + } + final Set allowedRoles = new HashSet(); + for(SingleConstraintMatch match : currentMatch.constraints) { + if(match.getRequiredRoles().isEmpty()) { + return new SingleConstraintMatch(match.getEmptyRoleSemantic(), Collections.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 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 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 exactPathRoleInformation = new HashMap(); + private final Map prefixPathRoleInformation = new HashMap(); + private final Map extensionRoleInformation = new HashMap(); + + public void addSecurityConstraint(Set roles, SecurityInfo.EmptyRoleSemantic emptyRoleSemantic, String pattern, Set httpMethods, Set 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 expandRolesAllowed(final Set rolesAllowed) { + final Set roles = new HashSet(rolesAllowed); + return roles; + } + + private void setupPathSecurityInformation(final PathSecurityInformation info, final SecurityInformation securityConstraint, + Set httpMethods, Set excludedMethods) { + if (httpMethods.isEmpty() && + excludedMethods.isEmpty()) { + info.defaultRequiredRoles.add(securityConstraint); + } else if (!httpMethods.isEmpty()) { + for (String method : httpMethods) { + List securityInformations = info.perMethodRequiredRoles.get(method); + if (securityInformations == null) { + info.perMethodRequiredRoles.put(method, securityInformations = new ArrayList()); + } + 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 defaultRequiredRoles = new ArrayList(); + final Map> perMethodRequiredRoles = new HashMap>(); + final List excludedMethodRoles = new ArrayList(); + } + + private static final class ExcludedMethodRoles { + final Set methods; + final SecurityInformation securityInformation; + + public ExcludedMethodRoles(final Set methods, final SecurityInformation securityInformation) { + this.methods = methods; + this.securityInformation = securityInformation; + } + } + + private static final class SecurityInformation { + final Set roles; + final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic; + + private SecurityInformation(final Set roles, final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic) { + this.emptyRoleSemantic = emptyRoleSemantic; + this.roles = new HashSet(roles); + } + } + + private static final class RuntimeMatch { + final List constraints = new ArrayList(); + 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 0000000000..cd0b242a91 --- /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 Darran Lofthouse + */ +public class SingleConstraintMatch { + + private final SecurityInfo.EmptyRoleSemantic emptyRoleSemantic; + private final Set requiredRoles; + + public SingleConstraintMatch(SecurityInfo.EmptyRoleSemantic emptyRoleSemantic, Set requiredRoles) { + this.emptyRoleSemantic = emptyRoleSemantic; + this.requiredRoles = requiredRoles; + } + + public SecurityInfo.EmptyRoleSemantic getEmptyRoleSemantic() { + return emptyRoleSemantic; + } + + public Set getRequiredRoles() { + return requiredRoles; + } + +} diff --git a/testsuite/pom.xml b/testsuite/pom.xml index db0fad4111..2c582f6b9d 100755 --- a/testsuite/pom.xml +++ b/testsuite/pom.xml @@ -26,6 +26,7 @@ integration + proxy tomcat6 tomcat7 tomcat8 diff --git a/testsuite/proxy/pom.xml b/testsuite/proxy/pom.xml new file mode 100755 index 0000000000..892b121731 --- /dev/null +++ b/testsuite/proxy/pom.xml @@ -0,0 +1,509 @@ + + + + keycloak-testsuite-pom + org.keycloak + 1.1.0.Beta2-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-testsuite-security-proxy + Keycloak Security Proxy TestSuite + + + 7.0.54 + + + + + + org.keycloak + keycloak-dependencies-server-all + ${project.version} + pom + + + org.keycloak + keycloak-admin-client + ${project.version} + + + org.keycloak + keycloak-proxy-server + ${project.version} + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + ${slf4j.version} + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + + + org.jboss.resteasy + jaxrs-api + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-jaxrs + ${resteasy.version.latest} + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + + + org.jboss.resteasy + resteasy-client + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-crypto + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-multipart-provider + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-jackson-provider + ${resteasy.version.latest} + + + org.jboss.resteasy + resteasy-undertow + ${resteasy.version.latest} + + + com.google.zxing + javase + + + org.bouncycastle + bcprov-jdk16 + + + org.apache.httpcomponents + httpclient + ${keycloak.apache.httpcomponents.version} + + + org.keycloak + keycloak-ldap-federation + ${project.version} + + + org.keycloak + keycloak-undertow-adapter + ${project.version} + + + org.keycloak + keycloak-tomcat7-adapter + ${project.version} + + + org.jboss.logging + jboss-logging + + + io.undertow + undertow-servlet + + + io.undertow + undertow-core + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-xc + + + junit + junit + + + org.hamcrest + hamcrest-all + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + + + com.h2database + h2 + + + org.hibernate + hibernate-entitymanager + + + com.icegreen + greenmail + + + org.slf4j + slf4j-api + + + + + org.infinispan + infinispan-core + + + org.seleniumhq.selenium + selenium-java + + + xml-apis + xml-apis + + + org.seleniumhq.selenium + selenium-chrome-driver + + + org.wildfly + wildfly-undertow + ${wildfly.version} + test + + + org.keycloak + keycloak-testsuite-integration + ${project.version} + test + + + org.keycloak + keycloak-testsuite-integration + ${project.version} + test-jar + test + + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + + + org.apache.tomcat + tomcat-util + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + test-jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.codehaus.mojo + exec-maven-plugin + + ${project.basedir} + + + + + + + + keycloak-server + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.KeycloakServer + + + + + + + mail-server + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.MailServer + + + + + + + totp + + + + org.codehaus.mojo + exec-maven-plugin + + org.keycloak.testutils.TotpGenerator + + + + + + + + jpa + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + jpa + jpa + jpa + jpa + + + + + + + + + mongo + + + localhost + 27018 + keycloak + true + 127.0.0.1 + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test + integration-test + + test + + + + mongo + mongo + mongo + mongo + ${keycloak.connectionsMongo.host} + ${keycloak.connectionsMongo.port} + ${keycloak.connectionsMongo.db} + ${keycloak.connectionsMongo.clearOnStartup} + ${keycloak.connectionsMongo.bindIp} + + + + + default-test + + true + + + + + + + + com.github.joelittlejohn.embedmongo + embedmongo-maven-plugin + + + start-mongodb + pre-integration-test + + start + + + ${keycloak.connectionsMongo.port} + file + ${project.build.directory}/mongodb.log + ${keycloak.connectionsMongo.bindIp} + + + + stop-mongodb + post-integration-test + + stop + + + + + + + + + + + infinispan + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + infinispan + infinispan + infinispan + + + + + + + + + + + + keycloak.connectionsJpa.driver + com.mysql.jdbc.Driver + + + mysql + + + mysql + mysql-connector-java + ${mysql.version} + + + + + + + + + keycloak.connectionsJpa.driver + org.postgresql.Driver + + + postgresql + + + org.postgresql + postgresql + ${postgresql.version} + + + + + + clean-jpa + + + + org.liquibase + liquibase-maven-plugin + + META-INF/jpa-changelog-master.xml + + ${keycloak.connectionsJpa.url} + ${keycloak.connectionsJpa.driver} + ${keycloak.connectionsJpa.user} + ${keycloak.connectionsJpa.password} + + false + + + + clean-jpa + clean + + dropAll + + + + + + + + + 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 0000000000..ecd0d594ab --- /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 Stian Thorgersen + */ +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 0000000000..e49233f08d --- /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 0000000000..a4a6ec9903 --- /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 0000000000..7e0b269ed6 --- /dev/null +++ b/testsuite/proxy/src/test/resources/tomcat-test/webapp/WEB-INF/web.xml @@ -0,0 +1,19 @@ + + + + adapter-test + + + SendUsername + org.keycloak.testsuite.ProxyTest$SendUsernameServlet + + + + SendUsername + /* + + +