From ec9ce6ef2f9cdb00c2260945dcddaf9faea0bced Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 23 Dec 2014 16:33:08 -0500 Subject: [PATCH] error page adapter support --- .../org/keycloak/adapters/AuthChallenge.java | 7 +++ .../BearerTokenRequestAuthenticator.java | 10 ++++ .../adapters/OAuthRequestAuthenticator.java | 29 +++++++++-- .../jbossweb/KeycloakAuthenticatorValve.java | 13 ++++- .../AbstractKeycloakJettyAuthenticator.java | 52 ++++++++++++++----- .../AbstractKeycloakAuthenticatorValve.java | 10 +++- .../tomcat/KeycloakAuthenticatorValve.java | 14 ++++- .../tomcat/KeycloakAuthenticatorValve.java | 13 ++++- integration/tomcat/tomcat8/pom.xml | 13 +++++ .../tomcat/KeycloakAuthenticatorValve.java | 34 +++++++++++- .../AbstractUndertowKeycloakAuthMech.java | 23 +++++++- .../undertow/KeycloakServletExtension.java | 24 ++++++--- .../undertow/ServletKeycloakAuthMech.java | 38 +++++++++++++- .../UndertowAuthenticationMechanism.java | 4 +- .../WildflyAuthenticationMechanism.java | 4 +- .../WildflyKeycloakServletExtension.java | 2 +- .../keycloak/proxy/ProxyServerBuilder.java | 2 +- .../adapter/AdapterTestStrategy.java | 3 ++ .../testsuite/rule/AbstractKeycloakRule.java | 18 ++++++- .../keycloak/testsuite/rule/ErrorServlet.java | 28 ++++++++++ .../customer-portal/WEB-INF/web.xml | 22 +++++++- .../customer-portal/WEB-INF/web.xml | 21 +++++++- .../customer-portal/WEB-INF/web.xml | 21 +++++++- .../customer-portal/WEB-INF/web.xml | 19 +++++++ .../customer-portal/WEB-INF/web.xml | 19 +++++++ .../customer-portal/WEB-INF/web.xml | 19 +++++++ 26 files changed, 421 insertions(+), 41 deletions(-) create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthChallenge.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthChallenge.java index 4682808543a..10f960e2c65 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthChallenge.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AuthChallenge.java @@ -11,4 +11,11 @@ public interface AuthChallenge { * @return challenge sent */ boolean challenge(HttpFacade exchange); + + /** + * Whether or not an error page should be displayed if possible + * + * @return + */ + boolean errorPage(); } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java index ccbe5961b26..81b1d4221f4 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java @@ -109,6 +109,11 @@ public class BearerTokenRequestAuthenticator { protected AuthChallenge clientCertChallenge() { return new AuthChallenge() { + @Override + public boolean errorPage() { + return false; + } + @Override public boolean challenge(HttpFacade exchange) { // do the same thing as client cert auth @@ -129,6 +134,11 @@ public class BearerTokenRequestAuthenticator { } final String challenge = header.toString(); return new AuthChallenge() { + @Override + public boolean errorPage() { + return false; + } + @Override public boolean challenge(HttpFacade facade) { facade.getResponse().setStatus(401); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index f5451c877b4..d780f5d94a5 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -146,13 +146,29 @@ public class OAuthRequestAuthenticator { protected AuthChallenge loginRedirect() { final String state = getStateCode(); final String redirect = getRedirectUri(state); - return new AuthChallenge() { - @Override - public boolean challenge(HttpFacade exchange) { - if (redirect == null) { + if (redirect == null) { + return new AuthChallenge() { + @Override + public boolean challenge(HttpFacade exchange) { exchange.getResponse().setStatus(403); return true; } + + @Override + public boolean errorPage() { + return true; + } + }; + } + return new AuthChallenge() { + + @Override + public boolean errorPage() { + return false; + } + + @Override + public boolean challenge(HttpFacade exchange) { tokenStore.saveRequest(); log.debug("Sending redirect to login page: " + redirect); exchange.getResponse().setStatus(302); @@ -218,6 +234,11 @@ public class OAuthRequestAuthenticator { protected AuthChallenge challenge(final int code) { return new AuthChallenge() { + @Override + public boolean errorPage() { + return true; + } + @Override public boolean challenge(HttpFacade exchange) { exchange.getResponse().setStatus(code); diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/jbossweb/KeycloakAuthenticatorValve.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/jbossweb/KeycloakAuthenticatorValve.java index 71e0f19b303..91cbb8720f7 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/jbossweb/KeycloakAuthenticatorValve.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/jbossweb/KeycloakAuthenticatorValve.java @@ -11,6 +11,7 @@ import org.keycloak.adapters.tomcat.GenericPrincipalFactory; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.security.Principal; import java.util.List; @@ -22,9 +23,19 @@ import java.util.List; */ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve { public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws java.io.IOException { - return authenticateInternal(request, response); + return authenticateInternal(request, response, config); } + @Override + protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { + if (loginConfig == null) return false; + LoginConfig config = (LoginConfig)loginConfig; + if (config.getErrorPage() == null) return false; + forwardToErrorPage(request, (Response)response, config); + return true; + } + + @Override public void start() throws LifecycleException { StandardContext standardContext = (StandardContext) context; diff --git a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractKeycloakJettyAuthenticator.java b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractKeycloakJettyAuthenticator.java index 2e78eed9f34..fc01385debc 100755 --- a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractKeycloakJettyAuthenticator.java +++ b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractKeycloakJettyAuthenticator.java @@ -1,14 +1,18 @@ package org.keycloak.adapters.jetty; +import org.apache.http.HttpVersion; import org.eclipse.jetty.security.DefaultUserIdentity; import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.authentication.DeferredAuthentication; +import org.eclipse.jetty.security.authentication.FormAuthenticator; import org.eclipse.jetty.security.authentication.LoginAuthenticator; import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.handler.ContextHandler; +import org.eclipse.jetty.util.URIUtil; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; @@ -37,6 +41,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Set; @@ -52,6 +57,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica protected NodesRegistrationManagement nodesRegistrationManagement; protected AdapterConfig adapterConfig; protected KeycloakConfigResolver configResolver; + protected String errorPage; public AbstractKeycloakJettyAuthenticator() { super(); @@ -66,7 +72,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica } public AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { - AdapterTokenStore store = (AdapterTokenStore)request.getAttribute(TOKEN_STORE_NOTE); + AdapterTokenStore store = (AdapterTokenStore) request.getAttribute(TOKEN_STORE_NOTE); if (store != null) { return store; } @@ -84,8 +90,8 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica public abstract AdapterTokenStore createSessionTokenStore(Request request, KeycloakDeployment resolvedDeployment); public void logoutCurrent(Request request) { - AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext)request.getAttribute(AdapterDeploymentContext.class.getName()); - KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getAttribute(AdapterDeploymentContext.class.getName()); + KeycloakSecurityContext ksc = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); if (ksc != null) { JettyHttpFacade facade = new JettyHttpFacade(request, null); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); @@ -115,11 +121,25 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica public void setConfiguration(AuthConfiguration configuration) { //super.setConfiguration(configuration); initializeKeycloak(); + String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE); + setErrorPage(error); + } + + private void setErrorPage(String path) { + if (path == null || path.trim().length() == 0) { + } else { + if (!path.startsWith("/")) { + path = "/" + path; + } + errorPage = path; + + if (errorPage.indexOf('?') > 0) + errorPage = errorPage.substring(0, errorPage.indexOf('?')); + } } @Override - public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException - { + public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException { return true; } @@ -167,12 +187,13 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica InputStream configInputStream = getConfigInputStream(theServletContext); if (configInputStream != null) { deploymentContext = new AdapterDeploymentContext(KeycloakDeploymentBuilder.build(configInputStream)); - } + } } if (deploymentContext == null) { deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); } - if (theServletContext != null) theServletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); + if (theServletContext != null) + theServletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); } private InputStream getConfigInputStream(ServletContext servletContext) { @@ -198,7 +219,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica log.trace("*** authenticate"); } Request request = resolveRequest(req); - JettyHttpFacade facade = new JettyHttpFacade(request, (HttpServletResponse)res); + JettyHttpFacade facade = new JettyHttpFacade(request, (HttpServletResponse) res); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); if (deployment == null || !deployment.isConfigured()) { log.debug("*** deployment isn't configured return false"); @@ -231,17 +252,25 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica } AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { + if (challenge.errorPage() && errorPage != null) { + Response response = (Response)res; + try { + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } challenge.challenge(facade); } return Authentication.SEND_CONTINUE; } - protected abstract Request resolveRequest(ServletRequest req); protected JettyRequestAuthenticator createRequestAuthenticator(Request request, JettyHttpFacade facade, - KeycloakDeployment deployment, AdapterTokenStore tokenStore) { + KeycloakDeployment deployment, AdapterTokenStore tokenStore) { return new JettyRequestAuthenticator(facade, deployment, tokenStore, -1, request); } @@ -263,8 +292,7 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica protected abstract Authentication createAuthentication(UserIdentity userIdentity); - public static abstract class KeycloakAuthentication extends UserAuthentication - { + public static abstract class KeycloakAuthentication extends UserAuthentication { public KeycloakAuthentication(String method, UserIdentity userIdentity) { super(method, userIdentity); } diff --git a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java index acf33a4b785..ffc59414348 100755 --- a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java +++ b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/AbstractKeycloakAuthenticatorValve.java @@ -10,6 +10,7 @@ import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.authenticator.SavedRequest; import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; +import org.apache.catalina.deploy.LoginConfig; import org.apache.tomcat.util.buf.ByteChunk; import org.keycloak.KeycloakSecurityContext; import org.keycloak.constants.AdapterConstants; @@ -175,8 +176,9 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat } protected abstract GenericPrincipalFactory createPrincipalFactory(); + protected abstract boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException; - protected boolean authenticateInternal(Request request, HttpServletResponse response) { + protected boolean authenticateInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { CatalinaHttpFacade facade = new CatalinaHttpFacade(request, response); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); if (deployment == null || !deployment.isConfigured()) { @@ -196,6 +198,12 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat } AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { + if (loginConfig == null) { + loginConfig = request.getContext().getLoginConfig(); + } + if (challenge.errorPage()) { + if (forwardToErrorPageInternal(request, response, loginConfig))return false; + } challenge.challenge(facade); } return false; diff --git a/integration/tomcat/tomcat6/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java b/integration/tomcat/tomcat6/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java index fa6ac80b202..5dc7701d91c 100755 --- a/integration/tomcat/tomcat6/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java +++ b/integration/tomcat/tomcat6/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java @@ -8,6 +8,8 @@ import org.apache.catalina.deploy.LoginConfig; import org.apache.catalina.realm.GenericPrincipal; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.security.Principal; import java.util.List; @@ -20,9 +22,19 @@ import java.util.List; public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve { @Override public boolean authenticate(Request request, Response response, LoginConfig config) throws java.io.IOException { - return authenticateInternal(request, response); + return authenticateInternal(request, response, config); } + @Override + protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { + if (loginConfig == null) return false; + LoginConfig config = (LoginConfig)loginConfig; + if (config.getErrorPage() == null) return false; + forwardToErrorPage(request, (Response)response, config); + return true; + } + + @Override public void start() throws LifecycleException { StandardContext standardContext = (StandardContext) context; diff --git a/integration/tomcat/tomcat7/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java b/integration/tomcat/tomcat7/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java index 700378e23cb..45402e61c4e 100755 --- a/integration/tomcat/tomcat7/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java +++ b/integration/tomcat/tomcat7/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java @@ -1,6 +1,7 @@ package org.keycloak.adapters.tomcat; import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.deploy.LoginConfig; import org.apache.catalina.realm.GenericPrincipal; @@ -19,9 +20,19 @@ import java.util.List; */ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve { public boolean authenticate(Request request, HttpServletResponse response, LoginConfig config) throws IOException { - return authenticateInternal(request, response); + return authenticateInternal(request, response, config); } + @Override + protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { + if (loginConfig == null) return false; + LoginConfig config = (LoginConfig)loginConfig; + if (config.getErrorPage() == null) return false; + forwardToErrorPage(request, (Response)response, config); + return true; + } + + protected void initInternal() { StandardContext standardContext = (StandardContext) context; standardContext.addLifecycleListener(this); diff --git a/integration/tomcat/tomcat8/pom.xml b/integration/tomcat/tomcat8/pom.xml index 8e9eb1b90b9..f503a662170 100755 --- a/integration/tomcat/tomcat8/pom.xml +++ b/integration/tomcat/tomcat8/pom.xml @@ -32,6 +32,19 @@ keycloak-adapter-core ${project.version} + + org.apache.tomcat + tomcat-servlet-api + ${tomcat.version} + provided + + + org.apache.tomcat + tomcat-catalina + ${tomcat.version} + provided + + org.keycloak keycloak-tomcat-core-adapter diff --git a/integration/tomcat/tomcat8/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java b/integration/tomcat/tomcat8/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java index 9812c3d2e11..f390fe77fcf 100755 --- a/integration/tomcat/tomcat8/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java +++ b/integration/tomcat/tomcat8/src/main/java/org/keycloak/adapters/tomcat/KeycloakAuthenticatorValve.java @@ -1,12 +1,19 @@ package org.keycloak.adapters.tomcat; +import org.apache.catalina.authenticator.FormAuthenticator; import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; import org.apache.catalina.core.StandardContext; import org.apache.catalina.realm.GenericPrincipal; +import org.apache.tomcat.util.ExceptionUtils; +import org.apache.tomcat.util.descriptor.web.LoginConfig; +import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.security.Principal; import java.util.List; @@ -18,7 +25,32 @@ import java.util.List; */ public class KeycloakAuthenticatorValve extends AbstractKeycloakAuthenticatorValve { public boolean authenticate(Request request, HttpServletResponse response) throws IOException { - return authenticateInternal(request, response); + return authenticateInternal(request, response, request.getContext().getLoginConfig()); + } + + @Override + protected boolean forwardToErrorPageInternal(Request request, HttpServletResponse response, Object loginConfig) throws IOException { + if (loginConfig == null) return false; + LoginConfig config = (LoginConfig)loginConfig; + if (config.getErrorPage() == null) return false; + // had to do this to get around compiler/IDE issues :( + try { + Method method = null; + /* + for (Method m : getClass().getDeclaredMethods()) { + if (m.getName().equals("forwardToErrorPage")) { + method = m; + break; + } + } + */ + method = FormAuthenticator.class.getDeclaredMethod("forwardToErrorPage", Request.class, HttpServletResponse.class, LoginConfig.class); + method.setAccessible(true); + method.invoke(this, request, response, config); + } catch (Exception e) { + throw new RuntimeException(e); + } + return true; } protected void initInternal() { diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java index 9a35896122c..892343b82a3 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowKeycloakAuthMech.java @@ -23,7 +23,9 @@ import io.undertow.security.api.SecurityNotification; import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.util.AttachmentKey; +import io.undertow.util.Headers; import io.undertow.util.Sessions; +import io.undertow.util.StatusCodes; import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakSecurityContext; import org.keycloak.adapters.AdapterDeploymentContext; @@ -46,16 +48,22 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication public static final AttachmentKey KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(AuthChallenge.class); protected AdapterDeploymentContext deploymentContext; protected UndertowUserSessionManagement sessionManagement; + protected String errorPage; - public AbstractUndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement) { + public AbstractUndertowKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) { this.deploymentContext = deploymentContext; this.sessionManagement = sessionManagement; + this.errorPage = errorPage; } @Override public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { AuthChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY); if (challenge != null) { + if (challenge.errorPage() && errorPage != null) { + Integer code = servePage(exchange, errorPage); + return new ChallengeResult(true, code); + } UndertowHttpFacade facade = new UndertowHttpFacade(exchange); if (challenge.challenge(facade)) { return new ChallengeResult(true, exchange.getResponseCode()); @@ -64,6 +72,19 @@ public abstract class AbstractUndertowKeycloakAuthMech implements Authentication return new ChallengeResult(false); } + protected Integer servePage(final HttpServerExchange exchange, final String location) { + sendRedirect(exchange, location); + return StatusCodes.TEMPORARY_REDIRECT; + } + + static void sendRedirect(final HttpServerExchange exchange, final String location) { + // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. + String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; + exchange.getResponseHeaders().put(Headers.LOCATION, loc); + } + + + protected void registerNotifications(final SecurityContext securityContext) { final NotificationReceiver logoutReceiver = new NotificationReceiver() { diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java index 4014a92320a..025862157f2 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/KeycloakServletExtension.java @@ -32,11 +32,12 @@ import io.undertow.servlet.api.LoginConfig; import io.undertow.servlet.api.ServletSessionConfig; import io.undertow.servlet.util.ImmediateInstanceHandle; import org.jboss.logging.Logger; -import org.keycloak.constants.AdapterConstants; import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.KeycloakConfigResolver; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.NodesRegistrationManagement; +import org.keycloak.constants.AdapterConstants; import javax.servlet.ServletContext; import java.io.ByteArrayInputStream; @@ -44,7 +45,6 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.util.Map; -import org.keycloak.adapters.KeycloakConfigResolver; /** * @author Bill Burke @@ -57,9 +57,9 @@ public class KeycloakServletExtension implements ServletExtension { // todo when this DeploymentInfo method of the same name is fixed. public boolean isAuthenticationMechanismPresent(DeploymentInfo deploymentInfo, final String mechanismName) { LoginConfig loginConfig = deploymentInfo.getLoginConfig(); - if(loginConfig != null) { - for(AuthMethodConfig method : loginConfig.getAuthMethods()) { - if(method.getName().equalsIgnoreCase(mechanismName)) { + if (loginConfig != null) { + for (AuthMethodConfig method : loginConfig.getAuthMethods()) { + if (method.getName().equalsIgnoreCase(mechanismName)) { return true; } } @@ -191,7 +191,17 @@ public class KeycloakServletExtension implements ServletExtension { protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement) { - log.debug("creating ServletKeycloakAuthMech"); - return new ServletKeycloakAuthMech(deploymentContext, userSessionManagement, nodesRegistrationManagement, deploymentInfo.getConfidentialPortManager()); + log.debug("creating ServletKeycloakAuthMech"); + String errorPage = getErrorPage(deploymentInfo); + return new ServletKeycloakAuthMech(deploymentContext, userSessionManagement, nodesRegistrationManagement, deploymentInfo.getConfidentialPortManager(), errorPage); + } + + protected String getErrorPage(DeploymentInfo deploymentInfo) { + LoginConfig loginConfig = deploymentInfo.getLoginConfig(); + String errorPage = null; + if (loginConfig != null) { + errorPage = loginConfig.getErrorPage(); + } + return errorPage; } } 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 0ae5b6eb78c..144210f8a18 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,18 +16,28 @@ */ package org.keycloak.adapters.undertow; +import io.undertow.security.api.AuthenticationMechanism; import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpServerExchange; import io.undertow.servlet.api.ConfidentialPortManager; +import io.undertow.servlet.handlers.ServletRequestContext; +import io.undertow.util.Headers; import org.jboss.logging.Logger; import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.NodesRegistrationManagement; import org.keycloak.adapters.RequestAuthenticator; import org.keycloak.enums.TokenStore; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; + /** * @author Bill Burke * @author Stan Silvert ssilvert@redhat.com (C) 2014 Red Hat Inc. @@ -39,12 +49,36 @@ public class ServletKeycloakAuthMech extends AbstractUndertowKeycloakAuthMech { protected NodesRegistrationManagement nodesRegistrationManagement; protected ConfidentialPortManager portManager; - public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager) { - super(deploymentContext, userSessionManagement); + public ServletKeycloakAuthMech(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, + NodesRegistrationManagement nodesRegistrationManagement, ConfidentialPortManager portManager, + String errorPage) { + super(deploymentContext, userSessionManagement, errorPage); this.nodesRegistrationManagement = nodesRegistrationManagement; this.portManager = portManager; } + @Override + protected Integer servePage(HttpServerExchange exchange, String location) { + final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); + ServletRequest req = servletRequestContext.getServletRequest(); + ServletResponse resp = servletRequestContext.getServletResponse(); + RequestDispatcher disp = req.getRequestDispatcher(location); + //make sure the login page is never cached + exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache"); + exchange.getResponseHeaders().add(Headers.EXPIRES, "0"); + + + try { + disp.forward(req, resp); + } catch (ServletException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return null; + } + @Override public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { UndertowHttpFacade facade = new UndertowHttpFacade(exchange); 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 index 057fc50f8df..b4082558845 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowAuthenticationMechanism.java @@ -17,8 +17,8 @@ public class UndertowAuthenticationMechanism extends AbstractUndertowKeycloakAut protected int confidentialPort; public UndertowAuthenticationMechanism(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, - NodesRegistrationManagement nodesRegistrationManagement, int confidentialPort) { - super(deploymentContext, sessionManagement); + NodesRegistrationManagement nodesRegistrationManagement, int confidentialPort, String errorPage) { + super(deploymentContext, sessionManagement, errorPage); this.nodesRegistrationManagement = nodesRegistrationManagement; this.confidentialPort = confidentialPort; } diff --git a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java index 6880d669ebf..85f8bd318df 100755 --- a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java +++ b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyAuthenticationMechanism.java @@ -21,8 +21,8 @@ public class WildflyAuthenticationMechanism extends ServletKeycloakAuthMech { public WildflyAuthenticationMechanism(AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement, - ConfidentialPortManager portManager) { - super(deploymentContext, userSessionManagement, nodesRegistrationManagement, portManager); + ConfidentialPortManager portManager, String errorPage) { + super(deploymentContext, userSessionManagement, nodesRegistrationManagement, portManager, errorPage); } @Override diff --git a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java index 0cdf2f90e72..2e432857cd5 100755 --- a/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java +++ b/integration/wildfly-adapter/src/main/java/org/keycloak/adapters/wildfly/WildflyKeycloakServletExtension.java @@ -19,7 +19,7 @@ public class WildflyKeycloakServletExtension extends KeycloakServletExtension { protected ServletKeycloakAuthMech createAuthenticationMechanism(DeploymentInfo deploymentInfo, AdapterDeploymentContext deploymentContext, UndertowUserSessionManagement userSessionManagement, NodesRegistrationManagement nodesRegistrationManagement) { log.debug("creating WildflyAuthenticationMechanism"); - return new WildflyAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, deploymentInfo.getConfidentialPortManager()); + return new WildflyAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, deploymentInfo.getConfidentialPortManager(), getErrorPage(deploymentInfo)); } } 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 index 88358564896..59977e61d56 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java @@ -229,7 +229,7 @@ public class ProxyServerBuilder { handler = new ConstraintMatcherHandler(matches, handler, toWrap, errorPage); final List mechanisms = new LinkedList(); mechanisms.add(new CachedAuthenticatedSessionMechanism()); - mechanisms.add(new UndertowAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, -1)); + mechanisms.add(new UndertowAuthenticationMechanism(deploymentContext, userSessionManagement, nodesRegistrationManagement, -1, null)); handler = new AuthenticationMechanismsHandler(handler, mechanisms); IdentityManager identityManager = new IdentityManager() { @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java index 49b1a6dc50b..6ff9df2f47a 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java @@ -224,6 +224,9 @@ public class AdapterTestStrategy extends ExternalResource { Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); driver.navigate().to(APP_SERVER_BASE_URL + "/customer-portal"); Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL)); + loginPage.cancel(); + System.out.println(driver.getPageSource()); + Assert.assertTrue(driver.getPageSource().contains("Error Page")); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java index d618567899d..0603751a043 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java @@ -5,6 +5,7 @@ import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.FilterInfo; import io.undertow.servlet.api.LoginConfig; import io.undertow.servlet.api.SecurityConstraint; +import io.undertow.servlet.api.SecurityInfo; import io.undertow.servlet.api.ServletInfo; import io.undertow.servlet.api.WebResourceCollection; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; @@ -126,6 +127,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource { server.getServer().deploy(deploymentInfo); } + private DeploymentInfo createDeploymentInfo(String name, String contextPath, Class servletClass) { DeploymentInfo deploymentInfo = new DeploymentInfo(); deploymentInfo.setClassLoader(getClass().getClassLoader()); @@ -168,11 +170,25 @@ public abstract class AbstractKeycloakRule extends ExternalResource { constraint.addRoleAllowed(role); di.addSecurityConstraint(constraint); } - LoginConfig loginConfig = new LoginConfig("KEYCLOAK", "demo"); + LoginConfig loginConfig = new LoginConfig("KEYCLOAK", "demo", null, "/error.html"); di.setLoginConfig(loginConfig); + addErrorPage(di); + server.getServer().deploy(di); } + public void addErrorPage(DeploymentInfo di) { + ServletInfo servlet = new ServletInfo("Error Page", ErrorServlet.class); + servlet.addMapping("/error.html"); + SecurityConstraint constraint = new SecurityConstraint(); + WebResourceCollection collection = new WebResourceCollection(); + collection.addUrlPattern("/error.html"); + constraint.addWebResourceCollection(collection); + constraint.setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT); + di.addSecurityConstraint(constraint); + di.addServlet(servlet); + } + public void deployJaxrsApplication(String name, String contextPath, Class applicationClass, Map initParams) { ResteasyDeployment deployment = new ResteasyDeployment(); deployment.setApplicationClass(applicationClass.getName()); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java new file mode 100755 index 00000000000..1f8534dc1cd --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/ErrorServlet.java @@ -0,0 +1,28 @@ +package org.keycloak.testsuite.rule; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author Marek Posolda + */ +public class ErrorServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + + + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.printf("%s", "Error Page"); + pw.print("

There was an error

"); + pw.flush(); + + + } +} diff --git a/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..f96add7af75 100755 --- a/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/jetty/jetty81/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,12 +34,23 @@ user + + + Errors + /error.html + + - BASIC + FORM demo + + /error.html + /error.html + +♦ admin diff --git a/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..cce06164986 100755 --- a/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/jetty/jetty91/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,10 +34,20 @@ user + + + Errors + /error.html + + - BASIC + FORM demo + + /error.html + /error.html + diff --git a/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..cce06164986 100755 --- a/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/jetty/jetty92/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,10 +34,20 @@ user + + + Errors + /error.html + + - BASIC + FORM demo + + /error.html + /error.html + diff --git a/testsuite/tomcat6/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/tomcat6/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..9c211cb5bfb 100755 --- a/testsuite/tomcat6/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/tomcat6/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,10 +34,20 @@ user + + + Errors + /error.html + + BASIC demo + + /error.html + /error.html + diff --git a/testsuite/tomcat7/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/tomcat7/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..9c211cb5bfb 100755 --- a/testsuite/tomcat7/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/tomcat7/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,10 +34,20 @@ user + + + Errors + /error.html + + BASIC demo + + /error.html + /error.html + diff --git a/testsuite/tomcat8/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml b/testsuite/tomcat8/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml index 19acca0cbeb..9c211cb5bfb 100755 --- a/testsuite/tomcat8/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml +++ b/testsuite/tomcat8/src/test/resources/adapter-test/customer-portal/WEB-INF/web.xml @@ -10,12 +10,21 @@ Servlet org.keycloak.testsuite.adapter.CustomerServlet + + Error Servlet + org.keycloak.testsuite.rule.ErrorServlet + Servlet /* + + Error Servlet + /error.html + + Users @@ -25,10 +34,20 @@ user + + + Errors + /error.html + + BASIC demo + + /error.html + /error.html +