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/CommonUtils.java b/cas-client-core/src/main/java/org/jasig/cas/client/util/CommonUtils.java
index 8fb90c7..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);
}
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 43967ab..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
@@ -188,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-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/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.");
}
}