From 9ced56d8d72548710b8d66b53d3ce8a1ec979c21 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 9 Oct 2015 18:07:50 -0400 Subject: [PATCH] saml and oidc filters --- .../BearerTokenRequestAuthenticator.java | 10 + .../adapters/OAuthRequestAuthenticator.java | 15 + .../org/keycloak/adapters/AuthChallenge.java | 10 +- .../adapters/InMemorySessionIdMapper.java | 8 + .../keycloak/adapters/SessionIdMapper.java | 2 + .../AbstractKeycloakJettyAuthenticator.java | 4 +- integration/pom.xml | 1 + .../adapters/servlet/FilterSessionStore.java | 304 +++++++++++++++++- .../adapters/servlet/ServletHttpFacade.java | 7 + integration/servlet-filter/pom.xml | 83 +++++ .../servlet/FilterRequestAuthenticator.java | 86 +++++ .../adapters/servlet/KeycloakOIDCFilter.java | 158 +++++++++ .../servlet/OIDCFilterSessionStore.java | 167 ++++++++++ .../servlet/OIDCServletHttpFacade.java | 23 ++ .../AbstractKeycloakAuthenticatorValve.java | 12 +- pom.xml | 7 +- .../keycloak/adapters/saml/InitiateLogin.java | 5 + .../saml/jetty/AbstractSamlAuthenticator.java | 2 +- saml/client-adapter/servlet-filter/pom.xml | 2 +- .../saml/servlet/FilterSamlSessionStore.java | 90 +----- .../adapters/saml/servlet/SamlFilter.java | 32 +- .../saml/AbstractSamlAuthenticatorValve.java | 4 +- testsuite/integration/pom.xml | 6 +- .../adapter/AdapterTestStrategy.java | 1 + .../testsuite/adapter/FilterAdapterTest.java | 214 ++++++++++++ .../testsuite/rule/AbstractKeycloakRule.java | 20 ++ 26 files changed, 1159 insertions(+), 114 deletions(-) create mode 100755 integration/servlet-filter/pom.xml create mode 100755 integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java create mode 100755 integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java create mode 100755 integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java create mode 100755 integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java 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 54395ebef4..c377b1fc55 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 @@ -114,6 +114,11 @@ public class BearerTokenRequestAuthenticator { return false; } + @Override + public int getResponseCode() { + return 0; + } + @Override public boolean challenge(HttpFacade exchange) { // do the same thing as client cert auth @@ -139,6 +144,11 @@ public class BearerTokenRequestAuthenticator { return true; } + @Override + public int getResponseCode() { + return 401; + } + @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 d077d7dc76..c6ffce5bd8 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 @@ -181,6 +181,11 @@ public class OAuthRequestAuthenticator { public boolean errorPage() { return true; } + + @Override + public int getResponseCode() { + return 403; + } }; } return new AuthChallenge() { @@ -190,6 +195,11 @@ public class OAuthRequestAuthenticator { return false; } + @Override + public int getResponseCode() { + return 0; + } + @Override public boolean challenge(HttpFacade exchange) { tokenStore.saveRequest(); @@ -262,6 +272,11 @@ public class OAuthRequestAuthenticator { return true; } + @Override + public int getResponseCode() { + return code; + } + @Override public boolean challenge(HttpFacade exchange) { exchange.getResponse().setStatus(code); diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/AuthChallenge.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/AuthChallenge.java index 94385f0939..47c07c2c12 100755 --- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/AuthChallenge.java +++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/AuthChallenge.java @@ -13,9 +13,17 @@ public interface AuthChallenge { boolean challenge(HttpFacade exchange); /** - * Whether or not an error page should be displayed if possible + * Whether or not an error page should be displayed if possible along with the challenge * * @return */ boolean errorPage(); + + /** + * If errorPage is true, this is the response code the challenge will send. This is used by platforms + * that call HttpServletResponse.sendError() to forward to error page. + * + * @return + */ + int getResponseCode(); } diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java index 1be7159dcc..c4a8081e90 100755 --- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java +++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/InMemorySessionIdMapper.java @@ -22,6 +22,14 @@ public class InMemorySessionIdMapper implements SessionIdMapper { return sessionToSso.containsKey(id) || sessionToPrincipal.containsKey(id); } + @Override + public void clear() { + ssoToSession.clear(); + sessionToSso.clear(); + principalToSession.clear(); + sessionToPrincipal.clear(); + } + @Override public Set getUserSessions(String principal) { Set lookup = principalToSession.get(principal); diff --git a/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java b/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java index d52e0299ac..0f8715590f 100755 --- a/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java +++ b/integration/adapter-spi/src/main/java/org/keycloak/adapters/SessionIdMapper.java @@ -9,6 +9,8 @@ import java.util.Set; public interface SessionIdMapper { boolean hasSession(String id); + void clear(); + Set getUserSessions(String principal); String getSessionFromSSO(String sso); diff --git a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java index c11f30a092..6b0233c381 100755 --- a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java +++ b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java @@ -262,16 +262,16 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica } AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { + challenge.challenge(facade); if (challenge.errorPage() && errorPage != null) { Response response = (Response)res; try { - response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage))); + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(request.getContextPath(), errorPage))); } catch (IOException e) { throw new RuntimeException(e); } } - challenge.challenge(facade); } return Authentication.SEND_CONTINUE; } diff --git a/integration/pom.xml b/integration/pom.xml index c1d3346e7d..6257c286db 100755 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -26,6 +26,7 @@ undertow-adapter-spi undertow wildfly + servlet-filter js installed admin-client diff --git a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java index 51c6909a40..b6ddbc7250 100755 --- a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java +++ b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/FilterSessionStore.java @@ -2,14 +2,32 @@ package org.keycloak.adapters.servlet; import org.keycloak.adapters.AdapterSessionStore; import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.util.Encode; import org.keycloak.util.MultivaluedHashMap; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpSession; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -24,6 +42,7 @@ public class FilterSessionStore implements AdapterSessionStore { protected final HttpFacade facade; protected final int maxBuffer; protected byte[] restoredBuffer = null; + protected boolean needRequestRestore; public FilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer) { this.request = request; @@ -38,6 +57,249 @@ public class FilterSessionStore implements AdapterSessionStore { session.removeAttribute(SAVED_BODY); } + public void servletRequestLogout() { + + } + + public static String getCharsetFromContentType(String contentType) { + + if (contentType == null) + return (null); + int start = contentType.indexOf("charset="); + if (start < 0) + return (null); + String encoding = contentType.substring(start + 8); + int end = encoding.indexOf(';'); + if (end >= 0) + encoding = encoding.substring(0, end); + encoding = encoding.trim(); + if ((encoding.length() > 2) && (encoding.startsWith("\"")) + && (encoding.endsWith("\""))) + encoding = encoding.substring(1, encoding.length() - 1); + return (encoding.trim()); + + } + + + public HttpServletRequestWrapper buildWrapper(HttpSession session, final KeycloakAccount account) { + if (needRequestRestore) { + final String method = (String)session.getAttribute(SAVED_METHOD); + final byte[] body = (byte[])session.getAttribute(SAVED_BODY); + final MultivaluedHashMap headers = (MultivaluedHashMap)session.getAttribute(SAVED_HEADERS); + clearSavedRequest(session); + HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { + protected MultivaluedHashMap parameters; + + MultivaluedHashMap getParams() { + if (parameters != null) return parameters; + String contentType = getContentType(); + contentType = contentType.toLowerCase(); + if (contentType.startsWith("application/x-www-form-urlencoded")) { + ByteArrayInputStream is = new ByteArrayInputStream(body); + try { + parameters = parseForm(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return parameters; + + } + @Override + public boolean isUserInRole(String role) { + return account.getRoles().contains(role); + } + + @Override + public Principal getUserPrincipal() { + return account.getPrincipal(); + } + + @Override + public String getMethod() { + if (needRequestRestore) { + return method; + } else { + return super.getMethod(); + + } + } + + @Override + public String getHeader(String name) { + if (needRequestRestore && headers != null) { + return headers.getFirst(name.toLowerCase()); + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaders(String name) { + if (needRequestRestore && headers != null) { + List values = headers.getList(name.toLowerCase()); + if (values == null) return Collections.emptyEnumeration(); + else return Collections.enumeration(values); + } + return super.getHeaders(name); + } + + @Override + public Enumeration getHeaderNames() { + if (needRequestRestore && headers != null) { + return Collections.enumeration(headers.keySet()); + } + return super.getHeaderNames(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + + if (needRequestRestore && body != null) { + final ByteArrayInputStream is = new ByteArrayInputStream(body); + return new ServletInputStream() { + @Override + public int read() throws IOException { + return is.read(); + } + }; + } + return super.getInputStream(); + } + + @Override + public void logout() throws ServletException { + servletRequestLogout(); + } + + @Override + public long getDateHeader(String name) { + if (!needRequestRestore) return super.getDateHeader(name); + throw new RuntimeException("This method is not supported in a restored authenticated request"); + } + + @Override + public int getIntHeader(String name) { + if (!needRequestRestore) return super.getIntHeader(name); + String value = getHeader(name); + if (value == null) return -1; + return Integer.valueOf(value); + + } + + @Override + public String[] getParameterValues(String name) { + if (!needRequestRestore) return super.getParameterValues(name); + MultivaluedHashMap formParams = getParams(); + if (formParams == null) { + return super.getParameterValues(name); + } + String[] values = request.getParameterValues(name); + List list = new LinkedList<>(); + if (values != null) { + for (String val : values) list.add(val); + } + List vals = formParams.get(name); + if (vals != null) list.addAll(vals); + return list.toArray(new String[list.size()]); + } + + @Override + public Enumeration getParameterNames() { + if (!needRequestRestore) return super.getParameterNames(); + MultivaluedHashMap formParams = getParams(); + if (formParams == null) { + return super.getParameterNames(); + } + Set names = new HashSet<>(); + Enumeration qnames = super.getParameterNames(); + while (qnames.hasMoreElements()) names.add(qnames.nextElement()); + names.addAll(formParams.keySet()); + return Collections.enumeration(names); + + } + + @Override + public Map getParameterMap() { + if (!needRequestRestore) return super.getParameterMap(); + MultivaluedHashMap formParams = getParams(); + if (formParams == null) { + return super.getParameterMap(); + } + Map map = new HashMap<>(); + Enumeration names = getParameterNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + String[] values = getParameterValues(name); + if (values != null) { + map.put(name, values); + } + } + return map; + } + + @Override + public String getParameter(String name) { + if (!needRequestRestore) return super.getParameter(name); + String param = super.getParameter(name); + if (param != null) return param; + MultivaluedHashMap formParams = getParams(); + if (formParams == null) { + return null; + } + return formParams.getFirst(name); + + } + + @Override + public BufferedReader getReader() throws IOException { + if (!needRequestRestore) return super.getReader(); + return new BufferedReader(new InputStreamReader(getInputStream())); + } + + @Override + public int getContentLength() { + if (!needRequestRestore) return super.getContentLength(); + String header = getHeader("content-length"); + if (header == null) return -1; + return Integer.valueOf(header); + } + + @Override + public String getContentType() { + if (!needRequestRestore) return super.getContentType(); + return getHeader("content-type"); + } + + @Override + public String getCharacterEncoding() { + if (!needRequestRestore) return super.getCharacterEncoding(); + return getCharsetFromContentType(getContentType()); + } + + }; + return wrapper; + } else { + return new HttpServletRequestWrapper(request) { + @Override + public boolean isUserInRole(String role) { + return account.getRoles().contains(role); + } + + @Override + public Principal getUserPrincipal() { + return account.getPrincipal(); + } + + @Override + public void logout() throws ServletException { + servletRequestLogout(); + } + + + }; + } + } + public String getRedirectUri() { HttpSession session = request.getSession(true); return (String)session.getAttribute(REDIRECT_URI); @@ -50,6 +312,44 @@ public class FilterSessionStore implements AdapterSessionStore { return session.getAttribute(REDIRECT_URI) != null; } + public static MultivaluedHashMap parseForm(InputStream entityStream) + throws IOException + { + char[] buffer = new char[100]; + StringBuffer buf = new StringBuffer(); + BufferedReader reader = new BufferedReader(new InputStreamReader(entityStream)); + + int wasRead = 0; + do + { + wasRead = reader.read(buffer, 0, 100); + if (wasRead > 0) buf.append(buffer, 0, wasRead); + } while (wasRead > -1); + + String form = buf.toString(); + + MultivaluedHashMap formData = new MultivaluedHashMap(); + if ("".equals(form)) return formData; + + String[] params = form.split("&"); + + for (String param : params) + { + if (param.indexOf('=') >= 0) + { + String[] nv = param.split("="); + String val = nv.length > 1 ? nv[1] : ""; + formData.add(Encode.decode(nv[0]), Encode.decode(val)); + } + else + { + formData.add(Encode.decode(param), ""); + } + } + return formData; + } + + @Override public void saveRequest() { @@ -62,7 +362,7 @@ public class FilterSessionStore implements AdapterSessionStore { String name = names.nextElement(); Enumeration values = request.getHeaders(name); while (values.hasMoreElements()) { - headers.add(name, values.nextElement()); + headers.add(name.toLowerCase(), values.nextElement()); } } session.setAttribute(SAVED_HEADERS, headers); @@ -93,6 +393,8 @@ public class FilterSessionStore implements AdapterSessionStore { if (body.length > 0) { session.setAttribute(SAVED_BODY, body); } + + } } diff --git a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java index bc4eaa3e4a..f12c9ca8a7 100755 --- a/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java +++ b/integration/servlet-adapter-spi/src/main/java/org/keycloak/adapters/servlet/ServletHttpFacade.java @@ -67,6 +67,13 @@ public class ServletHttpFacade implements HttpFacade { return queryParameters.getFirst(param); } + public MultivaluedHashMap getQueryParameters() { + if (queryParameters == null) { + queryParameters = UriUtils.decodeQueryString(request.getQueryString()); + } + return queryParameters; + } + @Override public Cookie getCookie(String cookieName) { if (request.getCookies() == null) return null; diff --git a/integration/servlet-filter/pom.xml b/integration/servlet-filter/pom.xml new file mode 100755 index 0000000000..8448d59edc --- /dev/null +++ b/integration/servlet-filter/pom.xml @@ -0,0 +1,83 @@ + + + + keycloak-parent + org.keycloak + 1.6.0.Final-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-servlet-filter-adapter + Keycloak Servlet Filter Adapter Integration + + + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + provided + + + org.keycloak + keycloak-core + + + org.keycloak + keycloak-adapter-spi + + + org.keycloak + keycloak-servlet-adapter-spi + + + org.keycloak + keycloak-adapter-core + + + org.apache.httpcomponents + httpclient + + + org.bouncycastle + bcprov-jdk15on + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-mapper-asl + + + org.codehaus.jackson + jackson-xc + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + junit + junit + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + + diff --git a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java new file mode 100755 index 0000000000..70cb7e9229 --- /dev/null +++ b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/FilterRequestAuthenticator.java @@ -0,0 +1,86 @@ +package org.keycloak.adapters.servlet; + +import org.keycloak.KeycloakPrincipal; +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.AdapterUtils; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OAuthRequestAuthenticator; +import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.security.Principal; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Davide Ungari + * @version $Revision: 1 $ + */ +public class FilterRequestAuthenticator extends RequestAuthenticator { + private static final Logger log = Logger.getLogger(""+FilterRequestAuthenticator.class); + protected HttpServletRequest request; + + public FilterRequestAuthenticator(KeycloakDeployment deployment, + AdapterTokenStore tokenStore, + OIDCHttpFacade facade, + HttpServletRequest request, + int sslRedirectPort) { + super(facade, deployment, tokenStore, sslRedirectPort); + this.request = request; + } + + @Override + protected OAuthRequestAuthenticator createOAuthAuthenticator() { + return new OAuthRequestAuthenticator(this, facade, deployment, sslRedirectPort, tokenStore); + } + + @Override + protected void completeOAuthAuthentication(final KeycloakPrincipal skp) { + final RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext(); + final Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + OidcKeycloakAccount account = new OidcKeycloakAccount() { + + @Override + public Principal getPrincipal() { + return skp; + } + + @Override + public Set getRoles() { + return roles; + } + + @Override + public KeycloakSecurityContext getKeycloakSecurityContext() { + return securityContext; + } + + }; + + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + this.tokenStore.saveAccountInfo(account); + } + + @Override + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { + RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); + Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); + if (log.isLoggable(Level.FINE)) { + log.fine("Completing bearer authentication. Bearer roles: " + roles); + } + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + } + + @Override + protected String getHttpSessionId(boolean create) { + HttpSession session = request.getSession(create); + return session != null ? session.getId() : null; + } + +} diff --git a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java new file mode 100755 index 0000000000..327040c618 --- /dev/null +++ b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/KeycloakOIDCFilter.java @@ -0,0 +1,158 @@ +package org.keycloak.adapters.servlet; + +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.AuthChallenge; +import org.keycloak.adapters.AuthOutcome; +import org.keycloak.adapters.AuthenticatedActionsHandler; +import org.keycloak.adapters.InMemorySessionIdMapper; +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.KeycloakDeploymentBuilder; +import org.keycloak.adapters.PreAuthActionsHandler; +import org.keycloak.adapters.SessionIdMapper; +import org.keycloak.adapters.UserSessionManagement; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +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.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class KeycloakOIDCFilter implements Filter { + protected AdapterDeploymentContext deploymentContext; + protected SessionIdMapper idMapper = new InMemorySessionIdMapper(); + private final static Logger log = Logger.getLogger(""+KeycloakOIDCFilter.class); + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + String configResolverClass = filterConfig.getInitParameter("keycloak.config.resolver"); + if (configResolverClass != null) { + try { + KeycloakConfigResolver configResolver = (KeycloakConfigResolver) getClass().getClassLoader().loadClass(configResolverClass).newInstance(); + deploymentContext = new AdapterDeploymentContext(configResolver); + log.log(Level.INFO, "Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); + } catch (Exception ex) { + log.log(Level.FINE, "The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); + deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); + } + } else { + String fp = filterConfig.getInitParameter("keycloak.config.file"); + InputStream is = null; + if (fp != null) { + try { + is = new FileInputStream(fp); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } else { + String path = "/WEB-INF/keycloak.json"; + String pathParam = filterConfig.getInitParameter("keycloak.config.path"); + if (pathParam != null) path = pathParam; + is = filterConfig.getServletContext().getResourceAsStream(path); + } + KeycloakDeployment kd; + if (is == null) { + log.fine("No adapter configuration. Keycloak is unconfigured and will deny all requests."); + kd = new KeycloakDeployment(); + } else { + kd = KeycloakDeploymentBuilder.build(is); + } + deploymentContext = new AdapterDeploymentContext(kd); + log.fine("Keycloak is using a per-deployment configuration."); + } + filterConfig.getServletContext().setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); + } + + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + HttpServletResponse response = (HttpServletResponse) res; + OIDCServletHttpFacade facade = new OIDCServletHttpFacade(request, response); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); + if (deployment == null || !deployment.isConfigured()) { + response.sendError(403); + log.fine("deployment not configured"); + return; + } + + PreAuthActionsHandler preActions = new PreAuthActionsHandler(new UserSessionManagement() { + @Override + public void logoutAll() { + if (idMapper != null) { + idMapper.clear(); + } + } + + @Override + public void logoutHttpSessions(List ids) { + for (String id : ids) { + idMapper.removeSession(id); + } + + } + }, deploymentContext, facade); + + if (preActions.handleRequest()) { + return; + } + + + OIDCFilterSessionStore tokenStore = new OIDCFilterSessionStore(request, facade, 100000, deployment, idMapper); + tokenStore.checkCurrentToken(); + + + FilterRequestAuthenticator authenticator = new FilterRequestAuthenticator(deployment, tokenStore, facade, request, 8443); + AuthOutcome outcome = authenticator.authenticate(); + if (outcome == AuthOutcome.AUTHENTICATED) { + log.fine("AUTHENTICATED"); + if (facade.isEnded()) { + return; + } + AuthenticatedActionsHandler actions = new AuthenticatedActionsHandler(deployment, facade); + if (actions.handledRequest()) { + return; + } else { + HttpServletRequestWrapper wrapper = tokenStore.buildWrapper(); + chain.doFilter(wrapper, res); + return; + } + } + AuthChallenge challenge = authenticator.getChallenge(); + if (challenge != null) { + log.fine("challenge"); + challenge.challenge(facade); + if (challenge.errorPage()) { + response.sendError(challenge.getResponseCode()); + return; + } + log.fine("sending challenge"); + return; + } + response.sendError(403); + + } + + @Override + public void destroy() { + + } +} diff --git a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java new file mode 100755 index 0000000000..bab5cbe3e4 --- /dev/null +++ b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java @@ -0,0 +1,167 @@ +package org.keycloak.adapters.servlet; + +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.AdapterTokenStore; +import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.OidcKeycloakAccount; +import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.RequestAuthenticator; +import org.keycloak.adapters.SessionIdMapper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpSession; +import java.io.Serializable; +import java.security.Principal; +import java.util.Set; +import java.util.logging.Logger; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OIDCFilterSessionStore extends FilterSessionStore implements AdapterTokenStore { + protected final KeycloakDeployment deployment; + private static final Logger log = Logger.getLogger("" + OIDCFilterSessionStore.class); + protected final SessionIdMapper idMapper; + + public OIDCFilterSessionStore(HttpServletRequest request, HttpFacade facade, int maxBuffer, KeycloakDeployment deployment, SessionIdMapper idMapper) { + super(request, facade, maxBuffer); + this.deployment = deployment; + this.idMapper = idMapper; + } + + public HttpServletRequestWrapper buildWrapper() { + HttpSession session = request.getSession(); + KeycloakAccount account = (KeycloakAccount)session.getAttribute((KeycloakAccount.class.getName())); + return buildWrapper(session, account); + } + + @Override + public void checkCurrentToken() { + HttpSession httpSession = request.getSession(false); + if (httpSession == null) return; + SerializableKeycloakAccount account = (SerializableKeycloakAccount)httpSession.getAttribute(KeycloakAccount.class.getName()); + if (account == null) { + return; + } + + RefreshableKeycloakSecurityContext session = account.getKeycloakSecurityContext(); + if (session == null) return; + + // just in case session got serialized + if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this); + + if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return; + + // FYI: A refresh requires same scope, so same roles will be set. Otherwise, refresh will fail and token will + // not be updated + boolean success = session.refreshExpiredToken(false); + if (success && session.isActive()) return; + + // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session + //log.fine("Cleanup and expire session " + httpSession.getId() + " after failed refresh"); + cleanSession(httpSession); + httpSession.invalidate(); + } + + protected void cleanSession(HttpSession session) { + session.removeAttribute(KeycloakAccount.class.getName()); + clearSavedRequest(session); + } + + @Override + public boolean isCached(RequestAuthenticator authenticator) { + HttpSession httpSession = request.getSession(false); + if (httpSession == null) return false; + SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName()); + if (account == null) { + return false; + } + + log.fine("remote logged in already. Establish state from session"); + + RefreshableKeycloakSecurityContext securityContext = account.getKeycloakSecurityContext(); + + if (!deployment.getRealm().equals(securityContext.getRealm())) { + log.fine("Account from cookie is from a different realm than for the request."); + cleanSession(httpSession); + return false; + } + + if (idMapper != null && !idMapper.hasSession(httpSession.getId())) { + cleanSession(httpSession); + return false; + } + + + securityContext.setCurrentRequestInfo(deployment, this); + request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); + needRequestRestore = restoreRequest(); + return true; + } + + public static class SerializableKeycloakAccount implements OidcKeycloakAccount, Serializable { + protected Set roles; + protected Principal principal; + protected RefreshableKeycloakSecurityContext securityContext; + + public SerializableKeycloakAccount(Set roles, Principal principal, RefreshableKeycloakSecurityContext securityContext) { + this.roles = roles; + this.principal = principal; + this.securityContext = securityContext; + } + + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public Set getRoles() { + return roles; + } + + @Override + public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() { + return securityContext; + } + } + + @Override + public void saveAccountInfo(OidcKeycloakAccount account) { + RefreshableKeycloakSecurityContext securityContext = (RefreshableKeycloakSecurityContext) account.getKeycloakSecurityContext(); + Set roles = account.getRoles(); + + SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext); + HttpSession httpSession = request.getSession(); + httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount); + if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(), account.getPrincipal().getName(), httpSession.getId()); + //String username = securityContext.getToken().getSubject(); + //log.fine("userSessionManagement.login: " + username); + } + + @Override + public void logout() { + HttpSession httpSession = request.getSession(false); + if (httpSession != null) { + SerializableKeycloakAccount account = (SerializableKeycloakAccount) httpSession.getAttribute(KeycloakAccount.class.getName()); + if (account != null) { + account.getKeycloakSecurityContext().logout(deployment); + } + cleanSession(httpSession); + } + } + + @Override + public void servletRequestLogout() { + logout(); + } + + @Override + public void refreshCallback(RefreshableKeycloakSecurityContext securityContext) { + // no-op + } +} diff --git a/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java new file mode 100755 index 0000000000..1232625fb3 --- /dev/null +++ b/integration/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCServletHttpFacade.java @@ -0,0 +1,23 @@ +package org.keycloak.adapters.servlet; + +import org.keycloak.KeycloakSecurityContext; +import org.keycloak.adapters.OIDCHttpFacade; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class OIDCServletHttpFacade extends ServletHttpFacade implements OIDCHttpFacade { + + public OIDCServletHttpFacade(HttpServletRequest request, HttpServletResponse response) { + super(request, response); + } + + @Override + public KeycloakSecurityContext getSecurityContext() { + return (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName()); + } +} 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 3347978eb6..51b89f81ab 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 @@ -77,6 +77,13 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat request.setUserPrincipal(null); } + protected void beforeStop() { + if (nodesRegistrationManagement != null) { + nodesRegistrationManagement.stop(); + } + } + + @SuppressWarnings("UseSpecificCatch") public void keycloakInit() { // Possible scenarios: @@ -119,11 +126,6 @@ public abstract class AbstractKeycloakAuthenticatorValve extends FormAuthenticat nodesRegistrationManagement = new NodesRegistrationManagement(); } - protected void beforeStop() { - if (nodesRegistrationManagement != null) { - nodesRegistrationManagement.stop(); - } - } private static InputStream getJSONFromServletContext(ServletContext servletContext) { String json = servletContext.getInitParameter(AdapterConstants.AUTH_DATA_PARAM_NAME); diff --git a/pom.xml b/pom.xml index 61cfa08987..d9d040e3e7 100755 --- a/pom.xml +++ b/pom.xml @@ -900,7 +900,12 @@ org.keycloak - keycloak-saml-server-filter-adapter + keycloak-saml-servlet-filter-adapter + ${project.version} + + + org.keycloak + keycloak-servlet-filter-adapter ${project.version} diff --git a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java index bcba3d05ff..c0f8dc4e76 100755 --- a/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java +++ b/saml/client-adapter/core/src/main/java/org/keycloak/adapters/saml/InitiateLogin.java @@ -31,6 +31,11 @@ public class InitiateLogin implements AuthChallenge { return false; } + @Override + public int getResponseCode() { + return 0; + } + @Override public boolean challenge(HttpFacade httpFacade) { try { diff --git a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java index a63b934fa0..ec97fc9c0e 100755 --- a/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java +++ b/saml/client-adapter/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java @@ -259,6 +259,7 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator { AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { + challenge.challenge(facade); if (challenge.errorPage() && errorPage != null) { Response response = (Response)res; try { @@ -268,7 +269,6 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator { } } - challenge.challenge(facade); } return Authentication.SEND_CONTINUE; } diff --git a/saml/client-adapter/servlet-filter/pom.xml b/saml/client-adapter/servlet-filter/pom.xml index 219110c5c8..c2de71545a 100755 --- a/saml/client-adapter/servlet-filter/pom.xml +++ b/saml/client-adapter/servlet-filter/pom.xml @@ -9,7 +9,7 @@ 4.0.0 - keycloak-saml-server-filter-adapter + keycloak-saml-servlet-filter-adapter Keycloak SAML Servlet Filter diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java index dc4cd32d79..2d3a7fd3da 100755 --- a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java +++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/FilterSamlSessionStore.java @@ -2,6 +2,7 @@ package org.keycloak.adapters.saml.servlet; import org.jboss.logging.Logger; import org.keycloak.adapters.HttpFacade; +import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.SessionIdMapper; import org.keycloak.adapters.saml.SamlSession; import org.keycloak.adapters.saml.SamlSessionStore; @@ -34,8 +35,6 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe this.idMapper = idMapper; } - protected boolean needRequestRestore; - @Override public void logoutAccount() { HttpSession session = request.getSession(false); @@ -107,91 +106,8 @@ public class FilterSamlSessionStore extends FilterSessionStore implements SamlSe public HttpServletRequestWrapper getWrap() { HttpSession session = request.getSession(true); final SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName()); - if (needRequestRestore) { - final String method = (String)session.getAttribute(SAVED_METHOD); - final byte[] body = (byte[])session.getAttribute(SAVED_BODY); - final MultivaluedHashMap headers = (MultivaluedHashMap)session.getAttribute(SAVED_HEADERS); - clearSavedRequest(session); - HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { - @Override - public boolean isUserInRole(String role) { - return samlSession.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - return samlSession.getPrincipal(); - } - - @Override - public String getMethod() { - if (needRequestRestore) { - return method; - } else { - return super.getMethod(); - - } - } - - @Override - public String getHeader(String name) { - if (needRequestRestore && headers != null) { - return headers.getFirst(name); - } - return super.getHeader(name); - } - - @Override - public Enumeration getHeaders(String name) { - if (needRequestRestore && headers != null) { - List values = headers.getList(name); - if (values == null) return Collections.emptyEnumeration(); - else return Collections.enumeration(values); - } - return super.getHeaders(name); - } - - @Override - public Enumeration getHeaderNames() { - if (needRequestRestore && headers != null) { - return Collections.enumeration(headers.keySet()); - } - return super.getHeaderNames(); - } - - @Override - public ServletInputStream getInputStream() throws IOException { - - if (needRequestRestore && body != null) { - final ByteArrayInputStream is = new ByteArrayInputStream(body); - return new ServletInputStream() { - @Override - public int read() throws IOException { - return is.read(); - } - }; - } - return super.getInputStream(); - } - }; - return wrapper; - } else { - return new HttpServletRequestWrapper(request) { - @Override - public boolean isUserInRole(String role) { - return samlSession.getRoles().contains(role); - } - - @Override - public Principal getUserPrincipal() { - return samlSession.getPrincipal(); - } - - }; - } - - - + final KeycloakAccount account = samlSession; + return buildWrapper(session, account); } @Override diff --git a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java index c1b6ad555d..818806683b 100755 --- a/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java +++ b/saml/client-adapter/servlet-filter/src/main/java/org/keycloak/adapters/saml/servlet/SamlFilter.java @@ -24,6 +24,8 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.logging.Level; @@ -52,10 +54,20 @@ public class SamlFilter implements Filter { //deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment()); } } else { - String path = "/WEB-INF/keycloak-saml.xml"; - String pathParam = filterConfig.getInitParameter("keycloak.config.file"); - if (pathParam != null) path = pathParam; - InputStream is = filterConfig.getServletContext().getResourceAsStream(path); + String fp = filterConfig.getInitParameter("keycloak.config.file"); + InputStream is = null; + if (fp != null) { + try { + is = new FileInputStream(fp); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } else { + String path = "/WEB-INF/keycloak-saml.xml"; + String pathParam = filterConfig.getInitParameter("keycloak.config.path"); + if (pathParam != null) path = pathParam; + is = filterConfig.getServletContext().getResourceAsStream(path); + } final SamlDeployment deployment; if (is == null) { log.info("No adapter configuration. Keycloak is unconfigured and will deny all requests."); @@ -124,19 +136,17 @@ public class SamlFilter implements Filter { AuthChallenge challenge = authenticator.getChallenge(); if (challenge != null) { log.fine("challenge"); + challenge.challenge(facade); if (challenge.errorPage()) { - response.sendError(403); + response.sendError(challenge.getResponseCode()); return; } log.fine("sending challenge"); - challenge.challenge(facade); - } - if (outcome == AuthOutcome.FAILED) { - response.sendError(403); - } else if (!facade.isEnded()) { - chain.doFilter(req, res); return; } + if (!facade.isEnded()) { + response.sendError(403); + } } diff --git a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java index febbb72830..cfc21a0f58 100755 --- a/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java +++ b/saml/client-adapter/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java @@ -212,14 +212,12 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i if (loginConfig == null) { loginConfig = request.getContext().getLoginConfig(); } + challenge.challenge(facade); if (challenge.errorPage()) { log.fine("error page"); if (forwardToErrorPageInternal(request, response, loginConfig))return false; } - log.fine("sending challenge"); - challenge.challenge(facade); } - log.fine("No challenge, but failed authentication"); return false; } diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 7587247b8b..dad411423b 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -107,7 +107,11 @@ org.keycloak - keycloak-saml-server-filter-adapter + keycloak-saml-servlet-filter-adapter + + + org.keycloak + keycloak-servlet-filter-adapter org.keycloak 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 a63132fd5a..d5f90728a2 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 @@ -604,6 +604,7 @@ public class AdapterTestStrategy extends ExternalResource { // logout sessions in account management accountSessionsPage.realm("demo"); accountSessionsPage.open(); + Assert.assertTrue(accountSessionsPage.isCurrent()); accountSessionsPage.logoutAll(); // Assert I need to login again (logout was propagated to the app) diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java new file mode 100755 index 0000000000..39c03688c7 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java @@ -0,0 +1,214 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.adapter; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; + +import java.net.URL; +import java.security.PublicKey; + +/** + * Tests Undertow Adapter + * + * @author Bill Burke + */ +public class FilterAdapterTest { + + public static PublicKey realmPublicKey; + @ClassRule + public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule() { + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + RealmModel realm = AdapterTestStrategy.baseAdapterTestInitialization(session, manager, adminRealm, getClass()); + realmPublicKey = realm.getPublicKey(); + + URL url = getClass().getResource("/adapter-test/cust-app-keycloak.json"); + createApplicationDeployment() + .name("customer-portal").contextPath("/customer-portal") + .servletClass(CustomerServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplicationWithFilter(); + + url = getClass().getResource("/adapter-test/secure-portal-keycloak.json"); + createApplicationDeployment() + .name("secure-portal").contextPath("/secure-portal") + .servletClass(CallAuthenticatedServlet.class).adapterConfigPath(url.getPath()) + .role("user") + .isConstrained(false).deployApplicationWithFilter(); + + url = getClass().getResource("/adapter-test/customer-db-keycloak.json"); + createApplicationDeployment() + .name("customer-db").contextPath("/customer-db") + .servletClass(CustomerDatabaseServlet.class).adapterConfigPath(url.getPath()) + .role("user") + .errorPage(null).deployApplicationWithFilter(); + + createApplicationDeployment() + .name("customer-db-error-page").contextPath("/customer-db-error-page") + .servletClass(CustomerDatabaseServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplicationWithFilter(); + + url = getClass().getResource("/adapter-test/product-keycloak.json"); + createApplicationDeployment() + .name("product-portal").contextPath("/product-portal") + .servletClass(ProductServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplicationWithFilter(); + + // Test that replacing system properties works for adapters + System.setProperty("app.server.base.url", "http://localhost:8081"); + System.setProperty("my.host.name", "localhost"); + url = getClass().getResource("/adapter-test/session-keycloak.json"); + createApplicationDeployment() + .name("session-portal").contextPath("/session-portal") + .servletClass(SessionServlet.class).adapterConfigPath(url.getPath()) + .role("user").deployApplicationWithFilter(); + + url = getClass().getResource("/adapter-test/input-keycloak.json"); + createApplicationDeployment() + .name("input-portal").contextPath("/input-portal") + .servletClass(InputServlet.class).adapterConfigPath(url.getPath()) + .role("user").constraintUrl("/secured/*").deployApplicationWithFilter(); + } + }; + + @Rule + public AdapterTestStrategy testStrategy = new AdapterTestStrategy("http://localhost:8081/auth", "http://localhost:8081", keycloakRule); + + @Test + public void testLoginSSOAndLogout() throws Exception { + testStrategy.testLoginSSOAndLogout(); + } + + @Test + public void testSavedPostRequest() throws Exception { + testStrategy.testSavedPostRequest(); + } + + @Test + public void testServletRequestLogout() throws Exception { + testStrategy.testServletRequestLogout(); + } + + @Test + public void testLoginSSOIdle() throws Exception { + testStrategy.testLoginSSOIdle(); + + } + + @Test + public void testLoginSSOIdleRemoveExpiredUserSessions() throws Exception { + testStrategy.testLoginSSOIdleRemoveExpiredUserSessions(); + } + + @Test + public void testLoginSSOMax() throws Exception { + testStrategy.testLoginSSOMax(); + } + + /** + * KEYCLOAK-518 + * @throws Exception + */ + @Test + public void testNullBearerToken() throws Exception { + testStrategy.testNullBearerToken(); + } + + /** + * KEYCLOAK-1368 + * @throws Exception + */ + /* + can't test because of the way filter works + @Test + public void testNullBearerTokenCustomErrorPage() throws Exception { + testStrategy.testNullBearerTokenCustomErrorPage(); + } + */ + + /** + * KEYCLOAK-518 + * @throws Exception + */ + @Test + public void testBadUser() throws Exception { + testStrategy.testBadUser(); + } + + @Test + public void testVersion() throws Exception { + testStrategy.testVersion(); + } + + /* + Don't need to test this because HttpServletRequest.authenticate doesn't make sense with filter implementation + + @Test + public void testAuthenticated() throws Exception { + testStrategy.testAuthenticated(); + } + */ + + /** + * KEYCLOAK-732 + * + * @throws Throwable + */ + @Test + public void testSingleSessionInvalidated() throws Throwable { + testStrategy.testSingleSessionInvalidated(); + } + + /** + * KEYCLOAK-741 + */ + @Test + public void testSessionInvalidatedAfterFailedRefresh() throws Throwable { + testStrategy.testSessionInvalidatedAfterFailedRefresh(); + + } + + /** + * KEYCLOAK-942 + */ + @Test + public void testAdminApplicationLogout() throws Throwable { + testStrategy.testAdminApplicationLogout(); + } + + /** + * KEYCLOAK-1216 + */ + /* + Can't test this because backchannel logout for filter does not invalidate the session + @Test + public void testAccountManagementSessionsLogout() throws Throwable { + testStrategy.testAccountManagementSessionsLogout(); + } + */ + +} 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 d18b615737..8c673a0ad3 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 @@ -1,6 +1,7 @@ package org.keycloak.testsuite.rule; 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; @@ -11,6 +12,8 @@ import org.junit.rules.ExternalResource; import org.junit.rules.TemporaryFolder; import org.keycloak.Config; import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.adapters.saml.servlet.SamlFilter; +import org.keycloak.adapters.servlet.KeycloakOIDCFilter; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.RealmModel; @@ -24,6 +27,7 @@ import org.keycloak.testsuite.KeycloakServer; import org.keycloak.util.JsonSerialization; import org.keycloak.util.Time; +import javax.servlet.DispatcherType; import javax.servlet.Servlet; import javax.ws.rs.core.Application; import java.io.ByteArrayOutputStream; @@ -350,6 +354,22 @@ public abstract class AbstractKeycloakRule extends ExternalResource { server.getServer().deploy(di); } + public void deployApplicationWithFilter() { + DeploymentInfo di = createDeploymentInfo(name, contextPath, servletClass); + FilterInfo filter = new FilterInfo("keycloak-filter", KeycloakOIDCFilter.class); + if (null == keycloakConfigResolver) { + filter.addInitParam("keycloak.config.file", adapterConfigPath); + } else { + filter.addInitParam("keycloak.config.resolver", keycloakConfigResolver.getCanonicalName()); + } + di.addFilter(filter); + di.addFilterUrlMapping("keycloak-filter", constraintUrl, DispatcherType.REQUEST); + server.getServer().deploy(di); + + + + } + } }