diff --git a/cas-client-core/pom.xml b/cas-client-core/pom.xml index 1e6be56..1482cc6 100644 --- a/cas-client-core/pom.xml +++ b/cas-client-core/pom.xml @@ -1,7 +1,7 @@ org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT cas-client 4.0.0 diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java index 2e73556..b77c64c 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/AuthenticationFilter.java @@ -19,10 +19,14 @@ package org.jasig.cas.client.authentication; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; + import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.util.ReflectUtils; @@ -42,11 +46,10 @@ import org.jasig.cas.client.validation.Assertion; *

Please see AbstractCasFilter for additional properties.

* * @author Scott Battaglia - * @version $Revision: 11768 $ $Date: 2007-02-07 15:44:16 -0500 (Wed, 07 Feb 2007) $ + * @author Misagh Moayyed * @since 3.0 */ public class AuthenticationFilter extends AbstractCasFilter { - /** * The URL to the CAS Server login. */ @@ -65,7 +68,18 @@ public class AuthenticationFilter extends AbstractCasFilter { private GatewayResolver gatewayStorage = new DefaultGatewayResolverImpl(); private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy(); - + + private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null; + + private static final Map> PATTERN_MATCHER_TYPES = + new HashMap>(); + + static { + PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class); + PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class); + PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class); + } + protected void initInternal(final FilterConfig filterConfig) throws ServletException { if (!isIgnoreInitConfiguration()) { super.initInternal(filterConfig); @@ -75,13 +89,36 @@ public class AuthenticationFilter extends AbstractCasFilter { logger.trace("Loaded renew parameter: {}", this.renew); setGateway(parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false"))); logger.trace("Loaded gateway parameter: {}", this.gateway); - + + final String ignorePattern = getPropertyFromInitParams(filterConfig, "ignorePattern", null); + logger.trace("Loaded ignorePattern parameter: {}", ignorePattern); + + final String ignoreUrlPatternType = getPropertyFromInitParams(filterConfig, "ignoreUrlPatternType", "REGEX"); + logger.trace("Loaded ignoreUrlPatternType parameter: {}", ignoreUrlPatternType); + + if (ignorePattern != null) { + final Class ignoreUrlMatcherClass = PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType); + if (ignoreUrlMatcherClass != null) { + this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlMatcherClass.getName()); + } else { + try { + logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType); + this.ignoreUrlPatternMatcherStrategyClass = ReflectUtils.newInstance(ignoreUrlPatternType); + } catch (final IllegalArgumentException e) { + logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, e); + } + } + if (this.ignoreUrlPatternMatcherStrategyClass != null) { + this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern); + } + } + final String gatewayStorageClass = getPropertyFromInitParams(filterConfig, "gatewayStorageClass", null); if (gatewayStorageClass != null) { this.gatewayStorage = ReflectUtils.newInstance(gatewayStorageClass); } - + final String authenticationRedirectStrategyClass = getPropertyFromInitParams(filterConfig, "authenticationRedirectStrategyClass", null); @@ -98,8 +135,16 @@ public class AuthenticationFilter extends AbstractCasFilter { public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; + + if (isRequestUrlExcluded(request)) { + logger.debug("Request is ignored."); + filterChain.doFilter(request, response); + return; + } + final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; @@ -151,4 +196,17 @@ public class AuthenticationFilter extends AbstractCasFilter { public final void setGatewayStorage(final GatewayResolver gatewayStorage) { this.gatewayStorage = gatewayStorage; } + + private boolean isRequestUrlExcluded(final HttpServletRequest request) { + if (this.ignoreUrlPatternMatcherStrategyClass == null) { + return false; + } + + final StringBuffer urlBuffer = request.getRequestURL(); + if (request.getQueryString() != null) { + urlBuffer.append("?").append(request.getQueryString()); + } + final String requestUri = urlBuffer.toString(); + return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri); + } } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ContainsPatternUrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ContainsPatternUrlPatternMatcherStrategy.java new file mode 100644 index 0000000..48c9f5f --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ContainsPatternUrlPatternMatcherStrategy.java @@ -0,0 +1,38 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.authentication; + +/** + * A pattern matcher that looks inside the url to find the exact pattern specified. + * + * @author Misagh Moayyed + * @since 3.3.1 + */ +public final class ContainsPatternUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy { + + private String pattern; + + public boolean matches(final String url) { + return url.contains(this.pattern); + } + + public void setPattern(final String pattern) { + this.pattern = pattern; + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ExactUrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ExactUrlPatternMatcherStrategy.java new file mode 100644 index 0000000..64f20eb --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/ExactUrlPatternMatcherStrategy.java @@ -0,0 +1,40 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.authentication; + +/** + * A pattern matcher that produces a successful match if the pattern + * specified matches the given url exactly and equally. + * + * @author Misagh Moayyed + * @since 3.3.1 + */ +public final class ExactUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy { + + private String pattern; + + public boolean matches(final String url) { + return url.equals(this.pattern); + } + + public void setPattern(final String pattern) { + this.pattern = pattern; + } + +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java new file mode 100644 index 0000000..a941459 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/RegexUrlPatternMatcherStrategy.java @@ -0,0 +1,41 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.authentication; + +import java.util.regex.Pattern; + +/** + * A pattern matcher that looks inside the url to find the pattern,. that + * is assumed to have been specified via regular expressions syntax. + * + * @author Misagh Moayyed + * @since 3.3.1 + */ +public final class RegexUrlPatternMatcherStrategy implements UrlPatternMatcherStrategy { + + private Pattern pattern; + + public boolean matches(final String url) { + return this.pattern.matcher(url).find(); + } + + public void setPattern(final String pattern) { + this.pattern = Pattern.compile(pattern); + } +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/authentication/UrlPatternMatcherStrategy.java b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/UrlPatternMatcherStrategy.java new file mode 100644 index 0000000..a2e70e9 --- /dev/null +++ b/cas-client-core/src/main/java/org/jasig/cas/client/authentication/UrlPatternMatcherStrategy.java @@ -0,0 +1,42 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.authentication; +/** + * Defines an abstraction by which request urls can be matches against a given pattern. + * New instances for all extensions for this strategy interface will be created per + * each request. The client will ultimately invoke the {@link #matches(String)} method + * having already applied and set the pattern via the {@link #setPattern(String)} method. + * The pattern itself will be retrieved via the client configuration. + * @author Misagh Moayyed + * @since 3.3.1 + */ +public interface UrlPatternMatcherStrategy { + /** + * Execute the match between the given pattern and the url + * @param url the request url typically with query strings included + * @return true if match is successful + */ + boolean matches(String url); + + /** + * The pattern against which the url is compared + * @param pattern + */ + void setPattern(String pattern); +} diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java index 109791d..33b2094 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutFilter.java @@ -21,6 +21,8 @@ package org.jasig.cas.client.session; import java.io.IOException; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.jasig.cas.client.util.AbstractConfigurationFilter; /** @@ -36,9 +38,15 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { public void init(final FilterConfig filterConfig) throws ServletException { if (!isIgnoreInitConfiguration()) { - handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", "ticket")); + handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", + SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME)); handler.setLogoutParameterName(getPropertyFromInitParams(filterConfig, "logoutParameterName", - "logoutRequest")); + SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME)); + handler.setFrontLogoutParameterName(getPropertyFromInitParams(filterConfig, "frontLogoutParameterName", + SingleSignOutHandler.DEFAULT_FRONT_LOGOUT_PARAMETER_NAME)); + handler.setRelayStateParameterName(getPropertyFromInitParams(filterConfig, "relayStateParameterName", + SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME)); + handler.setCasServerUrlPrefix(getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null)); handler.setArtifactParameterOverPost(parseBoolean(getPropertyFromInitParams(filterConfig, "artifactParameterOverPost", "false"))); handler.setEagerlyCreateSessions(parseBoolean(getPropertyFromInitParams(filterConfig, @@ -55,6 +63,18 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { handler.setLogoutParameterName(name); } + public void setFrontLogoutParameterName(final String name) { + handler.setFrontLogoutParameterName(name); + } + + public void setRelayStateParameterName(final String name) { + handler.setRelayStateParameterName(name); + } + + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + handler.setCasServerUrlPrefix(casServerUrlPrefix); + } + public void setSessionMappingStorage(final SessionMappingStorage storage) { handler.setSessionMappingStorage(storage); } @@ -62,18 +82,11 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; - if (handler.isTokenRequest(request)) { - handler.recordSession(request); - } else if (handler.isLogoutRequest(request)) { - handler.destroySession(request); - // Do not continue up filter chain - return; - } else { - logger.trace("Ignoring URI {}", request.getRequestURI()); + if (handler.process(request, response)) { + filterChain.doFilter(servletRequest, servletResponse); } - - filterChain.doFilter(servletRequest, servletResponse); } public void destroy() { diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java b/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java index 943aa6b..c93d9b3 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/session/SingleSignOutHandler.java @@ -20,12 +20,16 @@ package org.jasig.cas.client.session; import java.util.Arrays; import java.util.List; +import java.util.zip.Inflater; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; import org.jasig.cas.client.util.CommonUtils; -import org.jasig.cas.client.util.ReflectUtils; import org.jasig.cas.client.util.XmlUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +44,13 @@ import org.slf4j.LoggerFactory; */ public final class SingleSignOutHandler { + public final static String DEFAULT_ARTIFACT_PARAMETER_NAME = "ticket"; + public final static String DEFAULT_LOGOUT_PARAMETER_NAME = "logoutRequest"; + public final static String DEFAULT_FRONT_LOGOUT_PARAMETER_NAME = "SAMLRequest"; + public final static String DEFAULT_RELAY_STATE_PARAMETER_NAME = "RelayState"; + + private final static int DECOMPRESSION_FACTOR = 10; + /** Logger instance */ private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -47,10 +58,19 @@ public final class SingleSignOutHandler { private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage(); /** The name of the artifact parameter. This is used to capture the session identifier. */ - private String artifactParameterName = "ticket"; + private String artifactParameterName = DEFAULT_ARTIFACT_PARAMETER_NAME; - /** Parameter name that stores logout request */ - private String logoutParameterName = "logoutRequest"; + /** Parameter name that stores logout request for back channel SLO */ + private String logoutParameterName = DEFAULT_LOGOUT_PARAMETER_NAME; + + /** Parameter name that stores logout request for front channel SLO */ + private String frontLogoutParameterName = DEFAULT_FRONT_LOGOUT_PARAMETER_NAME; + + /** Parameter name that stores the state of the CAS server webflow for the callback */ + private String relayStateParameterName = DEFAULT_RELAY_STATE_PARAMETER_NAME; + + /** The prefix url of the CAS server */ + private String casServerUrlPrefix; private boolean artifactParameterOverPost = false; @@ -78,12 +98,33 @@ public final class SingleSignOutHandler { } /** - * @param name Name of parameter containing CAS logout request message. + * @param name Name of parameter containing CAS logout request message for back channel SLO. */ public void setLogoutParameterName(final String name) { this.logoutParameterName = name; } + /** + * @param casServerUrlPrefix The prefix url of the CAS server. + */ + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + this.casServerUrlPrefix = casServerUrlPrefix; + } + + /** + * @param name Name of parameter containing CAS logout request message for front channel SLO. + */ + public void setFrontLogoutParameterName(final String name) { + this.frontLogoutParameterName = name; + } + + /** + * @param name Name of parameter containing the state of the CAS server webflow. + */ + public void setRelayStateParameterName(final String name) { + this.relayStateParameterName = name; + } + public void setEagerlyCreateSessions(final boolean eagerlyCreateSessions) { this.eagerlyCreateSessions = eagerlyCreateSessions; } @@ -94,7 +135,10 @@ public final class SingleSignOutHandler { public void init() { CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null."); CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null."); + CommonUtils.assertNotNull(this.frontLogoutParameterName, "frontLogoutParameterName cannot be null."); CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null."); + CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null."); + CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null."); if (this.artifactParameterOverPost) { this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName); @@ -110,32 +154,78 @@ public final class SingleSignOutHandler { * * @return True if request contains authentication token, false otherwise. */ - public boolean isTokenRequest(final HttpServletRequest request) { + private boolean isTokenRequest(final HttpServletRequest request) { return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters)); } /** - * Determines whether the given request is a CAS logout request. + * Determines whether the given request is a CAS back channel logout request. * * @param request HTTP request. * * @return True if request is logout request, false otherwise. */ - public boolean isLogoutRequest(final HttpServletRequest request) { + private boolean isBackChannelLogoutRequest(final HttpServletRequest request) { return "POST".equals(request.getMethod()) && !isMultipartRequest(request) && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters)); } + /** + * Determines whether the given request is a CAS front channel logout request. + * + * @param request HTTP request. + * + * @return True if request is logout request, false otherwise. + */ + private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { + return "GET".equals(request.getMethod()) + && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName)); + } + + /** + * Process a request regarding the SLO process: record the session or destroy it. + * + * @param request the incoming HTTP request. + * @param response the HTTP response. + * @return if the request should continue to be processed. + */ + public boolean process(final HttpServletRequest request, final HttpServletResponse response) { + if (isTokenRequest(request)) { + logger.trace("Received a token request"); + recordSession(request); + return true; + + } else if (isBackChannelLogoutRequest(request)) { + logger.trace("Received a back channel logout request"); + destroySession(request); + return false; + + } else if (isFrontChannelLogoutRequest(request)) { + logger.trace("Received a front channel logout request"); + destroySession(request); + // redirection url to the CAS server + final String redirectionUrl = computeRedirectionToServer(request); + if (redirectionUrl != null) { + CommonUtils.sendRedirect(response, redirectionUrl); + } + return false; + + } else { + logger.trace("Ignoring URI for logout: {}", request.getRequestURI()); + return true; + } + } + /** * Associates a token request with the current HTTP session by recording the mapping * in the the configured {@link SessionMappingStorage} container. * * @param request HTTP request containing an authentication token. */ - public void recordSession(final HttpServletRequest request) { + private void recordSession(final HttpServletRequest request) { final HttpSession session = request.getSession(this.eagerlyCreateSessions); if (session == null) { @@ -154,14 +244,50 @@ public final class SingleSignOutHandler { sessionMappingStorage.addSessionById(token, session); } + /** + * Uncompress a logout message (base64 + deflate). + * + * @param originalMessage the original logout message. + * @return the uncompressed logout message. + */ + private String uncompressLogoutMessage(final String originalMessage) { + final byte[] binaryMessage = Base64.decodeBase64(originalMessage); + + Inflater decompresser = null; + try { + // decompress the bytes + decompresser = new Inflater(); + decompresser.setInput(binaryMessage); + final byte[] result = new byte[binaryMessage.length * DECOMPRESSION_FACTOR]; + + final int resultLength = decompresser.inflate(result); + + // decode the bytes into a String + return new String(result, 0, resultLength, "UTF-8"); + } catch (final Exception e) { + logger.error("Unable to decompress logout message", e); + throw new RuntimeException(e); + } finally { + if (decompresser != null) { + decompresser.end(); + } + } + } + /** * Destroys the current HTTP session for the given CAS logout request. * * @param request HTTP request containing a CAS logout message. */ - public void destroySession(final HttpServletRequest request) { - final String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, - this.safeParameters); + private void destroySession(final HttpServletRequest request) { + final String logoutMessage; + // front channel logout -> the message needs to be base64 decoded + decompressed + if (isFrontChannelLogoutRequest(request)) { + logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request, + this.frontLogoutParameterName)); + } else { + logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters); + } logger.trace("Logout request:\n{}", logoutMessage); final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); @@ -187,6 +313,33 @@ public final class SingleSignOutHandler { } } + /** + * Compute the redirection url to the CAS server when it's a front channel SLO + * (depending on the relay state parameter). + * + * @param request The HTTP request. + * @return the redirection url to the CAS server. + */ + private String computeRedirectionToServer(final HttpServletRequest request) { + final String relayStateValue = CommonUtils.safeGetParameter(request, this.relayStateParameterName); + // if we have a state value -> redirect to the CAS server to continue the logout process + if (StringUtils.isNotBlank(relayStateValue)) { + final StringBuilder buffer = new StringBuilder(); + buffer.append(casServerUrlPrefix); + if (!this.casServerUrlPrefix.endsWith("/")) { + buffer.append("/"); + } + buffer.append("logout?_eventId=next&"); + buffer.append(this.relayStateParameterName); + buffer.append("="); + buffer.append(CommonUtils.urlEncode(relayStateValue)); + final String redirectUrl = buffer.toString(); + logger.debug("Redirection url to the CAS server: {}", redirectUrl); + return redirectUrl; + } + return null; + } + private boolean isMultipartRequest(final HttpServletRequest request) { return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart"); } diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java index 78f5f7b..8cf47ef 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/AbstractCasFilter.java @@ -34,11 +34,11 @@ import javax.servlet.http.HttpServletResponse; *

Please note that one of the two above parameters must be set.

* * @author Scott Battaglia - * @version $Revision$ $Date$ + * @author Misagh Moayyed * @since 3.1 */ public abstract class AbstractCasFilter extends AbstractConfigurationFilter { - + /** Represents the constant for where the assertion will be located in memory. */ public static final String CONST_CAS_ASSERTION = "_const_cas_assertion_"; @@ -47,7 +47,7 @@ public abstract class AbstractCasFilter extends AbstractConfigurationFilter { /** Defines the parameter to look for for the service. */ private String serviceParameterName = "service"; - + /** Sets where response.encodeUrl should be called on service urls when constructed. */ private boolean encodeServiceUrl = true; @@ -71,12 +71,13 @@ public abstract class AbstractCasFilter extends AbstractConfigurationFilter { logger.trace("Loading serviceParameterName property: {} ", this.serviceParameterName); setEncodeServiceUrl(parseBoolean(getPropertyFromInitParams(filterConfig, "encodeServiceUrl", "true"))); logger.trace("Loading encodeServiceUrl property: {}", this.encodeServiceUrl); - + initInternal(filterConfig); } init(); } + /** Controls the ordering of filter initialization and checking by defining a method that runs before the init. * @param filterConfig the original filter configuration. * @throws ServletException if there is a problem. @@ -147,7 +148,7 @@ public abstract class AbstractCasFilter extends AbstractConfigurationFilter { public final String getServiceParameterName() { return this.serviceParameterName; } - + /** * Template method to allow you to change how you retrieve the ticket. * diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java index c767c56..b483c56 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java @@ -160,10 +160,19 @@ public final class CommonUtils { */ public static String constructRedirectUrl(final String casServerLoginUrl, final String serviceParameterName, final String serviceUrl, final boolean renew, final boolean gateway) { + return casServerLoginUrl + (casServerLoginUrl.contains("?") ? "&" : "?") + serviceParameterName + "=" + + urlEncode(serviceUrl) + (renew ? "&renew=true" : "") + (gateway ? "&gateway=true" : ""); + } + + /** + * Url encode a value using UTF-8 encoding. + * + * @param value the value to encode. + * @return the encoded value. + */ + public static String urlEncode(String value) { try { - return casServerLoginUrl + (casServerLoginUrl.contains("?") ? "&" : "?") + serviceParameterName + "=" - + URLEncoder.encode(serviceUrl, "UTF-8") + (renew ? "&renew=true" : "") - + (gateway ? "&gateway=true" : ""); + return URLEncoder.encode(value, "UTF-8"); } catch (final UnsupportedEncodingException e) { throw new RuntimeException(e); } @@ -225,6 +234,21 @@ public final class CommonUtils { return serverNames[0]; } + private static boolean serverNameContainsPort(final boolean containsScheme, final String serverName) { + if (!containsScheme && serverName.contains(":")) { + return true; + } + + final int schemeIndex = serverName.indexOf(":"); + final int portIndex = serverName.lastIndexOf(":"); + return schemeIndex != portIndex; + } + + private static boolean requestIsOnStandardPort(final HttpServletRequest request) { + final int serverPort = request.getServerPort(); + return serverPort == 80 || serverPort == 443; + } + /** * Constructs a service url from the HttpServletRequest or from the given * serviceUrl. Prefers the serviceUrl provided if both a serviceUrl and a @@ -250,11 +274,19 @@ public final class CommonUtils { final String serverName = findMatchingServerName(request, serverNames); + boolean containsScheme = true; if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) { buffer.append(request.isSecure() ? "https://" : "http://"); + containsScheme = false; } buffer.append(serverName); + + if (!serverNameContainsPort(containsScheme, serverName) && !requestIsOnStandardPort(request)) { + buffer.append(":"); + buffer.append(request.getServerPort()); + } + buffer.append(request.getRequestURI()); if (CommonUtils.isNotBlank(request.getQueryString())) { diff --git a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java index 58f57c4..1d27314 100644 --- a/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java +++ b/cas-client-core/src/main/java/org/jasig/cas/client/validation/AbstractTicketValidationFilter.java @@ -254,4 +254,4 @@ public abstract class AbstractTicketValidationFilter extends AbstractCasFilter { public final void setUseSession(final boolean useSession) { this.useSession = useSession; } -} +} \ No newline at end of file diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java index 396c536..48479a5 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/authentication/AuthenticationFilterTests.java @@ -19,13 +19,17 @@ package org.jasig.cas.client.authentication; import static org.junit.Assert.*; + import java.io.IOException; import java.lang.reflect.Field; +import java.net.URL; import java.net.URLEncoder; + import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; + import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.validation.AssertionImpl; import org.junit.After; @@ -37,7 +41,6 @@ import org.springframework.mock.web.*; * Tests for the AuthenticationFilter. * * @author Scott Battaglia - * @version $Revision: 11753 $ $Date: 2007-01-03 13:37:26 -0500 (Wed, 03 Jan 2007) $ * @since 3.0 */ public final class AuthenticationFilterTests { @@ -50,11 +53,10 @@ public final class AuthenticationFilterTests { @Before public void setUp() throws Exception { - // TODO CAS_SERVICE_URL, false, CAS_LOGIN_URL this.filter = new AuthenticationFilter(); final MockFilterConfig config = new MockFilterConfig(); config.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - config.addInitParameter("service", "https://localhost:8443/service"); + config.addInitParameter("service", CAS_SERVICE_URL); this.filter.init(config); } @@ -184,7 +186,7 @@ public final class AuthenticationFilterTests { final AuthenticationFilter f = new AuthenticationFilter(); final MockFilterConfig config = new MockFilterConfig(); config.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); - config.addInitParameter("service", "https://localhost:8443/service"); + config.addInitParameter("service", CAS_SERVICE_URL); config.addInitParameter("renew", "true"); try { f.init(config); @@ -198,8 +200,8 @@ public final class AuthenticationFilterTests { public void testAllowsRenewContextParam() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); - context.addInitParameter("casServerLoginUrl", "https://cas.example.com/login"); - context.addInitParameter("service", "https://localhost:8443/service"); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + context.addInitParameter("service", CAS_SERVICE_URL); context.addInitParameter("renew", "true"); f.init(new MockFilterConfig(context)); final Field renewField = AuthenticationFilter.class.getDeclaredField("renew"); @@ -211,10 +213,159 @@ public final class AuthenticationFilterTests { public void customRedirectStrategy() throws Exception { final AuthenticationFilter f = new AuthenticationFilter(); final MockServletContext context = new MockServletContext(); - context.addInitParameter("casServerLoginUrl", "https://cas.example.com/login"); - context.addInitParameter("service", "https://localhost:8443/service"); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + context.addInitParameter("service", CAS_SERVICE_URL); context.addInitParameter("authenticationRedirectStrategyClass", "org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy"); f.init(new MockFilterConfig(context)); } + + @Test + public void testIgnorePatterns() throws Exception { + final AuthenticationFilter f = new AuthenticationFilter(); + final MockServletContext context = new MockServletContext(); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + + context.addInitParameter("ignorePattern", "=valueTo(\\w+)"); + context.addInitParameter("service", CAS_SERVICE_URL); + f.init(new MockFilterConfig(context)); + + final MockHttpServletRequest request = new MockHttpServletRequest(); + final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; + request.setRequestURI(URL); + + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + final MockHttpServletResponse response = new MockHttpServletResponse(); + + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + }; + + f.doFilter(request, response, filterChain); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void testIgnorePatternsWithContainsMatching() throws Exception { + final AuthenticationFilter f = new AuthenticationFilter(); + final MockServletContext context = new MockServletContext(); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + + context.addInitParameter("ignorePattern", "=valueToIgnore"); + context.addInitParameter("ignoreUrlPatternType", "CONTAINS"); + context.addInitParameter("service", CAS_SERVICE_URL); + f.init(new MockFilterConfig(context)); + + final MockHttpServletRequest request = new MockHttpServletRequest(); + final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; + request.setRequestURI(URL); + + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + final MockHttpServletResponse response = new MockHttpServletResponse(); + + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + }; + + f.doFilter(request, response, filterChain); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void testIgnorePatternsWithExactMatching() throws Exception { + final AuthenticationFilter f = new AuthenticationFilter(); + final MockServletContext context = new MockServletContext(); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + + final URL url = new URL(CAS_SERVICE_URL + "?param=valueToIgnore"); + + context.addInitParameter("ignorePattern", url.toExternalForm()); + context.addInitParameter("ignoreUrlPatternType", "EXACT"); + context.addInitParameter("service", CAS_SERVICE_URL); + f.init(new MockFilterConfig(context)); + + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme(url.getProtocol()); + request.setServerName(url.getHost()); + request.setServerPort(url.getPort()); + request.setQueryString(url.getQuery()); + request.setRequestURI(url.getPath()); + + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + final MockHttpServletResponse response = new MockHttpServletResponse(); + + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + }; + + f.doFilter(request, response, filterChain); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void testIgnorePatternsWithExactClassname() throws Exception { + final AuthenticationFilter f = new AuthenticationFilter(); + final MockServletContext context = new MockServletContext(); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + + context.addInitParameter("ignorePattern", "=valueToIgnore"); + context.addInitParameter("ignoreUrlPatternType", ContainsPatternUrlPatternMatcherStrategy.class.getName()); + context.addInitParameter("service", CAS_SERVICE_URL); + f.init(new MockFilterConfig(context)); + + final MockHttpServletRequest request = new MockHttpServletRequest(); + final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; + request.setRequestURI(URL); + + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + final MockHttpServletResponse response = new MockHttpServletResponse(); + + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + }; + + f.doFilter(request, response, filterChain); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void testIgnorePatternsWithInvalidClassname() throws Exception { + final AuthenticationFilter f = new AuthenticationFilter(); + final MockServletContext context = new MockServletContext(); + context.addInitParameter("casServerLoginUrl", CAS_LOGIN_URL); + + context.addInitParameter("ignorePattern", "=valueToIgnore"); + context.addInitParameter("ignoreUrlPatternType", "unknown.class.name"); + context.addInitParameter("service", CAS_SERVICE_URL); + f.init(new MockFilterConfig(context)); + + final MockHttpServletRequest request = new MockHttpServletRequest(); + final String URL = CAS_SERVICE_URL + "?param=valueToIgnore"; + request.setRequestURI(URL); + + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + + final MockHttpServletResponse response = new MockHttpServletResponse(); + + final FilterChain filterChain = new FilterChain() { + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + } + }; + + f.doFilter(request, response, filterChain); + System.out.println(response.getRedirectedUrl()); + } } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java b/cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java new file mode 100644 index 0000000..1ebc862 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java @@ -0,0 +1,38 @@ +package org.jasig.cas.client.session; + +import java.nio.charset.Charset; +import java.util.Date; +import java.util.zip.Deflater; + +import org.apache.commons.codec.binary.Base64; + +/** + * Logout message generator to perform tests on Single Sign Out feature. + * Greatly inspired by the source code in the CAS server itself. + * + * @author Jerome Leleu + * @since 3.4.0 + */ +public final class LogoutMessageGenerator { + + private static final String LOGOUT_REQUEST_TEMPLATE = + "@NOT_USED@" + + "%s"; + + public static String generateBackChannelLogoutMessage(final String sessionIndex) { + return String.format(LOGOUT_REQUEST_TEMPLATE, new Date(), sessionIndex); + } + + public static String generateFrontChannelLogoutMessage(final String sessionIndex) { + final String logoutMessage = generateBackChannelLogoutMessage(sessionIndex); + final Deflater deflater = new Deflater(); + deflater.setInput(logoutMessage.getBytes(Charset.forName("ASCII"))); + deflater.finish(); + final byte[] buffer = new byte[logoutMessage.length()]; + final int resultSize = deflater.deflate(buffer); + final byte[] output = new byte[resultSize]; + System.arraycopy(buffer, 0, output, 0, resultSize); + return Base64.encodeBase64String(output); + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutFilterTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutFilterTests.java new file mode 100644 index 0000000..64114f9 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutFilterTests.java @@ -0,0 +1,119 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.session; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockFilterConfig; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; + +/** + * Tests {@link SingleSignOutFilter}. + * + * @author Jerome Leleu + * @since 3.3.1 + */ +public class SingleSignOutFilterTests { + + private final static String CAS_SERVER_URL_PREFIX = "http://myhost.com/mycasserver"; + private final static String TICKET = "ST-yyyyy"; + private final static String RELAY_STATE = "e1s1"; + + private SingleSignOutFilter filter = new SingleSignOutFilter(); + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private MockFilterChain filterChain; + + @Before + public void setUp() throws Exception { + filter = new SingleSignOutFilter(); + filter.setCasServerUrlPrefix(CAS_SERVER_URL_PREFIX); + filter.setIgnoreInitConfiguration(true); + filter.init(new MockFilterConfig()); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + filterChain = new MockFilterChain(); + } + + @Test(expected = IllegalArgumentException.class) + public void initWithoutCasServerUrlPrefix() throws ServletException { + filter = new SingleSignOutFilter(); + filter.init(new MockFilterConfig()); + } + + @Test + public void tokenRequest() throws IOException, ServletException { + request.setParameter(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME, TICKET); + request.setQueryString(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME + "=" + TICKET); + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + filter.doFilter(request, response, filterChain); + assertEquals(session, SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().removeSessionByMappingId(TICKET)); + } + + @Test + public void backChannelRequest() throws IOException, ServletException { + request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, + LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET)); + request.setMethod("POST"); + final MockHttpSession session = new MockHttpSession(); + SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session); + filter.doFilter(request, response, filterChain); + assertNull(SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().removeSessionByMappingId(TICKET)); + } + + @Test + public void frontChannelRequest() throws IOException, ServletException { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(SingleSignOutHandler.DEFAULT_FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setQueryString(SingleSignOutHandler.DEFAULT_FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session); + filter.doFilter(request, response, filterChain); + assertNull(SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().removeSessionByMappingId(TICKET)); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void frontChannelRequestRelayState() throws IOException, ServletException { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(SingleSignOutHandler.DEFAULT_FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setParameter(SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME, RELAY_STATE); + request.setQueryString(SingleSignOutHandler.DEFAULT_FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + + SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME + "=" + RELAY_STATE); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session); + filter.doFilter(request, response, filterChain); + assertNull(SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().removeSessionByMappingId(TICKET)); + assertEquals(CAS_SERVER_URL_PREFIX + "/logout?_eventId=next&" + + SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME + "=" + RELAY_STATE, response.getRedirectedUrl()); + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutHandlerTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutHandlerTests.java new file mode 100644 index 0000000..365a25e --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutHandlerTests.java @@ -0,0 +1,182 @@ +/* + * Licensed to Jasig under one or more contributor license + * agreements. See the NOTICE file distributed with this work + * for additional information regarding copyright ownership. + * Jasig licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a + * copy of the License at the following location: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jasig.cas.client.session; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockHttpSession; + +/** + * @author Matt Brown + * @version $Revision$ $Date$ + * @since 3.2.1 + */ +public final class SingleSignOutHandlerTests { + + private final static String ANOTHER_PARAMETER = "anotherParameter"; + private final static String TICKET = "ST-xxxxxxxx"; + private final static String URL = "http://mycasserver"; + private final static String LOGOUT_PARAMETER_NAME = "logoutRequest2"; + private final static String FRONT_LOGOUT_PARAMETER_NAME = "SAMLRequest2"; + private final static String RELAY_STATE_PARAMETER_NAME = "RelayState2"; + private final static String ARTIFACT_PARAMETER_NAME = "ticket2"; + + private SingleSignOutHandler handler; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + + @Before + public void setUp() throws Exception { + handler = new SingleSignOutHandler(); + handler.setLogoutParameterName(LOGOUT_PARAMETER_NAME); + handler.setFrontLogoutParameterName(FRONT_LOGOUT_PARAMETER_NAME); + handler.setRelayStateParameterName(RELAY_STATE_PARAMETER_NAME); + handler.setArtifactParameterName(ARTIFACT_PARAMETER_NAME); + handler.setCasServerUrlPrefix(URL); + handler.init(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + } + + @Test + public void tokenRequestFailsIfNoSession() { + handler.setEagerlyCreateSessions(false); + request.setSession(null); + request.setParameter(ARTIFACT_PARAMETER_NAME, TICKET); + request.setQueryString(ARTIFACT_PARAMETER_NAME + "=" + TICKET); + assertTrue(handler.process(request, response)); + final SessionMappingStorage storage = handler.getSessionMappingStorage(); + assertNull(storage.removeSessionByMappingId(TICKET)); + } + + @Test + public void tokenRequestFailsIfBadParameter() { + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + request.setParameter(ANOTHER_PARAMETER, TICKET); + request.setQueryString(ANOTHER_PARAMETER + "=" + TICKET); + assertTrue(handler.process(request, response)); + final SessionMappingStorage storage = handler.getSessionMappingStorage(); + assertNull(storage.removeSessionByMappingId(TICKET)); + } + + @Test + public void tokenRequestOK() { + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + request.setParameter(ARTIFACT_PARAMETER_NAME, TICKET); + request.setQueryString(ARTIFACT_PARAMETER_NAME + "=" + TICKET); + assertTrue(handler.process(request, response)); + final SessionMappingStorage storage = handler.getSessionMappingStorage(); + assertEquals(session, storage.removeSessionByMappingId(TICKET)); + } + + @Test + public void backChannelLogoutFailsIfMultipart() { + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET); + request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertTrue(handler.process(request, response)); + assertFalse(session.isInvalid()); + } + + @Test + public void backChannelLogoutFailsIfNoSessionIndex() { + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(""); + request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); + request.setMethod("POST"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertFalse(handler.process(request, response)); + assertFalse(session.isInvalid()); + } + + @Test + public void backChannelLogoutOK() { + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET); + request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); + request.setMethod("POST"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertFalse(handler.process(request, response)); + assertTrue(session.isInvalid()); + } + + @Test + public void frontChannelLogoutFailsIfBadParameter() { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(ANOTHER_PARAMETER, logoutMessage); + request.setMethod("GET"); + request.setQueryString(ANOTHER_PARAMETER + "=" + logoutMessage); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertTrue(handler.process(request, response)); + assertFalse(session.isInvalid()); + } + + @Test + public void frontChannelLogoutFailsIfNoSessionIndex() { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(""); + request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertFalse(handler.process(request, response)); + assertFalse(session.isInvalid()); + } + + @Test + public void frontChannelLogoutOK() { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertFalse(handler.process(request, response)); + assertTrue(session.isInvalid()); + assertNull(response.getRedirectedUrl()); + } + + @Test + public void frontChannelLogoutRelayStateOK() { + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setParameter(RELAY_STATE_PARAMETER_NAME, TICKET); + request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + assertFalse(handler.process(request, response)); + assertTrue(session.isInvalid()); + assertEquals(URL + "/logout?_eventId=next&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET, + response.getRedirectedUrl()); + } +} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignoutHandlerTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignoutHandlerTests.java deleted file mode 100644 index 0e42830..0000000 --- a/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignoutHandlerTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to Jasig under one or more contributor license - * agreements. See the NOTICE file distributed with this work - * for additional information regarding copyright ownership. - * Jasig licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a - * copy of the License at the following location: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.jasig.cas.client.session; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import org.junit.Before; -import org.junit.Test; -import org.springframework.mock.web.MockHttpServletRequest; - -/** - * @author Matt Brown - * @version $Revision$ $Date$ - * @since 3.2.1 - */ -public final class SingleSignoutHandlerTests { - - private SingleSignOutHandler handler; - private MockHttpServletRequest request; - private final static String logoutParameterName = "logoutRequest"; - - @Before - public void setUp() throws Exception { - handler = new SingleSignOutHandler(); - handler.setLogoutParameterName(logoutParameterName); - handler.init(); - request = new MockHttpServletRequest(); - } - - @Test - public void isLogoutRequest() throws Exception { - request.setParameter(logoutParameterName, "true"); - request.setMethod("POST"); - - assertTrue(handler.isLogoutRequest(request)); - } - - /** - * Tests that a multipart request is not considered logoutRequest. Verifies issue CASC-147. - * - * @throws Exception - */ - @Test - public void isLogoutRequestMultipart() throws Exception { - request.setParameter(logoutParameterName, "true"); - request.setMethod("POST"); - request.setContentType("multipart/form-data"); - - assertFalse(handler.isLogoutRequest(request)); - } - -} diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java index f6e686c..8b585b4 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/util/CommonUtilsTests.java @@ -136,6 +136,27 @@ public final class CommonUtilsTests extends TestCase { assertEquals(CONST_MY_URL, constructedUrl); } + private void constructUrlNonStandardPortAndNoPortInConfigTest(final String serverNameList) { + final String CONST_MY_URL = "https://www.myserver.com:555/hello/hithere/"; + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello/hithere/"); + request.addHeader("Host", "www.myserver.com"); + request.setScheme("https"); + request.setSecure(true); + request.setServerPort(555); + final MockHttpServletResponse response = new MockHttpServletResponse(); + final String constructedUrl = CommonUtils.constructServiceUrl(request, response, null, + serverNameList, "ticket", false); + assertEquals(CONST_MY_URL, constructedUrl); + } + + public void testConstructUrlNonStandardPortAndNoScheme() { + constructUrlNonStandardPortAndNoPortInConfigTest("www.myserver.com"); + } + + public void testConstructUrlNonStandardPortAndScheme() { + constructUrlNonStandardPortAndNoPortInConfigTest("https://www.myserver.com"); + } + public void testConstructUrlWithMultipleHostsNoPortsOrProtocol() { final String CONST_MY_URL = "https://www.myserver.com/hello/hithere/"; final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello/hithere/"); @@ -167,4 +188,8 @@ public final class CommonUtilsTests extends TestCase { final String responsedContent = CommonUtils.getResponseFromServer(new URL("http://localhost:8090"), new HttpsURLConnectionFactory(), null); assertEquals(RESPONSE, responsedContent); } + + public void testUrlEncode() { + assertEquals("this+is+a+very+special+parameter+with+%3D%25%2F", CommonUtils.urlEncode("this is a very special parameter with =%/")); + } } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Cas10TicketValidationFilterTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Cas10TicketValidationFilterTests.java index e8daab4..8ccdb2f 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Cas10TicketValidationFilterTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Cas10TicketValidationFilterTests.java @@ -54,4 +54,4 @@ public class Cas10TicketValidationFilterTests { assertTrue(validator instanceof Cas10TicketValidator); assertTrue(((Cas10TicketValidator) validator).isRenew()); } -} +} \ No newline at end of file diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidationFilterTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidationFilterTests.java index 804b46f..53d1875 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidationFilterTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidationFilterTests.java @@ -54,4 +54,4 @@ public class Saml11TicketValidationFilterTests { assertTrue(validator instanceof Saml11TicketValidator); assertTrue(((Saml11TicketValidator) validator).isRenew()); } -} +} \ No newline at end of file diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java index 951e610..417db57 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java @@ -141,4 +141,4 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes private Interval currentTimeRangeInterval() { return new Interval(new DateTime(DateTimeZone.UTC).minus(5000), new DateTime(DateTimeZone.UTC).plus(200000000)); } -} +} \ No newline at end of file diff --git a/cas-client-integration-atlassian/pom.xml b/cas-client-integration-atlassian/pom.xml index bb6761c..83c039b 100644 --- a/cas-client-integration-atlassian/pom.xml +++ b/cas-client-integration-atlassian/pom.xml @@ -1,7 +1,7 @@ org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT cas-client 4.0.0 diff --git a/cas-client-integration-jboss/pom.xml b/cas-client-integration-jboss/pom.xml index e0eb030..6c5c96b 100644 --- a/cas-client-integration-jboss/pom.xml +++ b/cas-client-integration-jboss/pom.xml @@ -1,7 +1,7 @@ org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT cas-client 4.0.0 diff --git a/cas-client-integration-tomcat-common/pom.xml b/cas-client-integration-tomcat-common/pom.xml index d3c45ae..72be093 100644 --- a/cas-client-integration-tomcat-common/pom.xml +++ b/cas-client-integration-tomcat-common/pom.xml @@ -3,7 +3,7 @@ cas-client org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT 4.0.0 diff --git a/cas-client-integration-tomcat-v6/pom.xml b/cas-client-integration-tomcat-v6/pom.xml index fa147c7..3ce7551 100644 --- a/cas-client-integration-tomcat-v6/pom.xml +++ b/cas-client-integration-tomcat-v6/pom.xml @@ -3,7 +3,7 @@ cas-client org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT 4.0.0 diff --git a/cas-client-integration-tomcat-v6/src/main/java/org/jasig/cas/client/tomcat/v6/SingleSignOutValve.java b/cas-client-integration-tomcat-v6/src/main/java/org/jasig/cas/client/tomcat/v6/SingleSignOutValve.java index 1d95f43..9941651 100644 --- a/cas-client-integration-tomcat-v6/src/main/java/org/jasig/cas/client/tomcat/v6/SingleSignOutValve.java +++ b/cas-client-integration-tomcat-v6/src/main/java/org/jasig/cas/client/tomcat/v6/SingleSignOutValve.java @@ -45,37 +45,41 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio private final SingleSignOutHandler handler = new SingleSignOutHandler(); public void setArtifactParameterName(final String name) { - handler.setArtifactParameterName(name); + this.handler.setArtifactParameterName(name); } public void setLogoutParameterName(final String name) { - handler.setLogoutParameterName(name); + this.handler.setLogoutParameterName(name); + } + + public void setFrontLogoutParameterName(final String name) { + this.handler.setFrontLogoutParameterName(name); + } + + public void setRelayStateParameterName(final String name) { + this.handler.setRelayStateParameterName(name); + } + + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + this.handler.setCasServerUrlPrefix(casServerUrlPrefix); } public void setSessionMappingStorage(final SessionMappingStorage storage) { - handler.setSessionMappingStorage(storage); + this.handler.setSessionMappingStorage(storage); } /** {@inheritDoc} */ public void start() throws LifecycleException { super.start(); - handler.init(); + this.handler.init(); logger.info("Startup completed."); } /** {@inheritDoc} */ public void invoke(final Request request, final Response response) throws IOException, ServletException { - if (this.handler.isTokenRequest(request)) { - this.handler.recordSession(request); - request.getSessionInternal(true).addSessionListener(this); - } else if (this.handler.isLogoutRequest(request)) { - this.handler.destroySession(request); - // Do not proceed up valve chain - return; - } else { - logger.debug("Ignoring URI {}", request.getRequestURI()); + if (this.handler.process(request, response)) { + getNext().invoke(request, response); } - getNext().invoke(request, response); } /** {@inheritDoc} */ @@ -90,5 +94,4 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio protected String getName() { return NAME; } - } diff --git a/cas-client-integration-tomcat-v7/pom.xml b/cas-client-integration-tomcat-v7/pom.xml index 0c6b4f1..486d932 100644 --- a/cas-client-integration-tomcat-v7/pom.xml +++ b/cas-client-integration-tomcat-v7/pom.xml @@ -3,7 +3,7 @@ cas-client org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT 4.0.0 diff --git a/cas-client-integration-tomcat-v7/src/main/java/org/jasig/cas/client/tomcat/v7/SingleSignOutValve.java b/cas-client-integration-tomcat-v7/src/main/java/org/jasig/cas/client/tomcat/v7/SingleSignOutValve.java index c1bee5b..62ac214 100644 --- a/cas-client-integration-tomcat-v7/src/main/java/org/jasig/cas/client/tomcat/v7/SingleSignOutValve.java +++ b/cas-client-integration-tomcat-v7/src/main/java/org/jasig/cas/client/tomcat/v7/SingleSignOutValve.java @@ -49,30 +49,34 @@ public class SingleSignOutValve extends ValveBase implements SessionListener { private final SingleSignOutHandler handler = new SingleSignOutHandler(); public void setArtifactParameterName(final String name) { - handler.setArtifactParameterName(name); + this.handler.setArtifactParameterName(name); } public void setLogoutParameterName(final String name) { - handler.setLogoutParameterName(name); + this.handler.setLogoutParameterName(name); + } + + public void setFrontLogoutParameterName(final String name) { + this.handler.setFrontLogoutParameterName(name); + } + + public void setRelayStateParameterName(final String name) { + this.handler.setRelayStateParameterName(name); + } + + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + this.handler.setCasServerUrlPrefix(casServerUrlPrefix); } public void setSessionMappingStorage(final SessionMappingStorage storage) { - handler.setSessionMappingStorage(storage); + this.handler.setSessionMappingStorage(storage); } /** {@inheritDoc} */ public void invoke(final Request request, final Response response) throws IOException, ServletException { - if (this.handler.isTokenRequest(request)) { - this.handler.recordSession(request); - request.getSessionInternal(true).addSessionListener(this); - } else if (this.handler.isLogoutRequest(request)) { - this.handler.destroySession(request); - // Do not proceed up valve chain - return; - } else { - logger.debug("Ignoring URI {}", request.getRequestURI()); + if (this.handler.process(request, response)) { + getNext().invoke(request, response); } - getNext().invoke(request, response); } /** {@inheritDoc} */ @@ -87,7 +91,7 @@ public class SingleSignOutValve extends ValveBase implements SessionListener { protected void startInternal() throws LifecycleException { super.startInternal(); logger.info("Starting..."); - handler.init(); + this.handler.init(); logger.info("Startup completed."); } } diff --git a/cas-client-support-distributed-ehcache/pom.xml b/cas-client-support-distributed-ehcache/pom.xml index 493656b..e6ee05f 100644 --- a/cas-client-support-distributed-ehcache/pom.xml +++ b/cas-client-support-distributed-ehcache/pom.xml @@ -3,7 +3,7 @@ cas-client org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT 4.0.0 Jasig CAS Client for Java - Distributed Proxy Storage Support: EhCache diff --git a/cas-client-support-distributed-memcached/pom.xml b/cas-client-support-distributed-memcached/pom.xml index 369878a..e20df13 100644 --- a/cas-client-support-distributed-memcached/pom.xml +++ b/cas-client-support-distributed-memcached/pom.xml @@ -3,7 +3,7 @@ cas-client org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 09cd1fd..8b08eb4 100644 --- a/pom.xml +++ b/pom.xml @@ -2,17 +2,17 @@ org.jasig.parent jasig-parent - 38 + 39 4.0.0 org.jasig.cas.client - 3.3.1-SNAPSHOT + 3.3.2-SNAPSHOT cas-client pom Jasig CAS Client for Java - Jasig CAS Client for Java is the integration point forapplications that want to speak with a CAS + Jasig CAS Client for Java is the integration point for applications that want to speak with a CAS server, either via the CAS 1.0 or CAS 2.0 protocol. http://www.jasig.org/cas @@ -26,7 +26,8 @@ scm:git:git@github.com:Jasig/java-cas-client.git scm:git:git@github.com:Jasig/java-cas-client.git https://github.com/Jasig/java-cas-client - + HEAD + 2006 @@ -103,15 +104,6 @@ - - org.apache.maven.plugins - maven-release-plugin - 2.2.1 - - forked-path - v@{project.version} - - org.apache.maven.plugins maven-enforcer-plugin