From 47f825871ea19d1cd54df890f7e84e1051caa392 Mon Sep 17 00:00:00 2001 From: LELEU Jerome Date: Thu, 13 Mar 2014 19:03:41 +0100 Subject: [PATCH 1/6] CASC-220: Support front channel SLO logout First commit --- .../client/session/SingleSignOutFilter.java | 51 +++++- .../client/session/SingleSignOutHandler.java | 76 ++++++++- .../jasig/cas/client/util/CommonUtils.java | 15 +- .../session/LogoutMessageGenerator.java | 37 ++++ .../session/SingleSignOutFilterTests.java | 118 +++++++++++++ .../session/SingleSignOutHandlerTests.java | 160 ++++++++++++++++++ .../session/SingleSignoutHandlerTests.java | 68 -------- .../cas/client/util/CommonUtilsTests.java | 4 + 8 files changed, 449 insertions(+), 80 deletions(-) create mode 100644 cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java create mode 100644 cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutFilterTests.java create mode 100644 cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutHandlerTests.java delete mode 100644 cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignoutHandlerTests.java 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..1126218 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,7 +21,11 @@ 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.apache.commons.lang.StringUtils; import org.jasig.cas.client.util.AbstractConfigurationFilter; +import org.jasig.cas.client.util.CommonUtils; /** * Implements the Single Sign Out protocol. It handles registering the session and destroying the session. @@ -34,15 +38,25 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { private static final SingleSignOutHandler handler = new SingleSignOutHandler(); + /** The prefix url of the CAS server */ + private String casServerUrlPrefix; + + /** Parameter name that stores the state of the CAS server webflow for the callback */ + private String relayStateParameterName = SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME; + 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)); + setRelayStateParameterName(getPropertyFromInitParams(filterConfig, "relayStateParameterName", + SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME)); handler.setArtifactParameterOverPost(parseBoolean(getPropertyFromInitParams(filterConfig, "artifactParameterOverPost", "false"))); handler.setEagerlyCreateSessions(parseBoolean(getPropertyFromInitParams(filterConfig, "eagerlyCreateSessions", "true"))); + setCasServerUrlPrefix(getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null)); } handler.init(); } @@ -55,20 +69,51 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { handler.setLogoutParameterName(name); } + public void setRelayStateParameterName(final String name) { + this.relayStateParameterName = name; + handler.setRelayStateParameterName(name); + } + public void setSessionMappingStorage(final SessionMappingStorage storage) { handler.setSessionMappingStorage(storage); } + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + CommonUtils.assertNotNull(casServerUrlPrefix, "casServerUrlPrefix cannot be null."); + this.casServerUrlPrefix = casServerUrlPrefix; + } + 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)) { + } else if (handler.isBackChannelLogoutRequest(request)) { handler.destroySession(request); // Do not continue up filter chain return; + } else if (handler.isFrontChannelLogoutRequest(request)) { + handler.destroySession(request); + // relay state value + 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 StringBuffer buffer = new StringBuffer(); + 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("Redirecting back to the CAS server: {}", redirectUrl); + CommonUtils.sendRedirect(response, redirectUrl); + } + return; } else { logger.trace("Ignoring URI {}", request.getRequestURI()); } 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..873d322 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 @@ -18,14 +18,18 @@ */ package org.jasig.cas.client.session; +import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; + +import org.apache.commons.codec.binary.Base64; 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,10 @@ 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_RELAY_STATE_PARAMETER_NAME = "RelayState"; + /** Logger instance */ private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -47,11 +55,14 @@ 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"; + private String logoutParameterName = DEFAULT_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; + private boolean artifactParameterOverPost = false; private boolean eagerlyCreateSessions = true; @@ -84,6 +95,13 @@ public final class SingleSignOutHandler { this.logoutParameterName = 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; } @@ -95,6 +113,7 @@ public final class SingleSignOutHandler { CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null."); CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null."); CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null."); + CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null."); if (this.artifactParameterOverPost) { this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName); @@ -116,19 +135,31 @@ public final class SingleSignOutHandler { } /** - * 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) { + public 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. + */ + public boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { + return "GET".equals(request.getMethod()) + && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName)); + } + /** * Associates a token request with the current HTTP session by recording the mapping * in the the configured {@link SessionMappingStorage} container. @@ -154,14 +185,47 @@ 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) { + // base64 decode + final byte[] binaryMessage = Base64.decodeBase64(originalMessage); + + try { + // decompress the bytes + final Inflater decompresser = new Inflater(); + decompresser.setInput(binaryMessage); + byte[] result = new byte[binaryMessage.length * 10]; + int resultLength = decompresser.inflate(result); + decompresser.end(); + + // decode the bytes into a String + return new String(result, 0, resultLength, "UTF-8"); + } catch (DataFormatException e) { + logger.error("Unable to decompress logout message", e); + throw new RuntimeException(e); + } catch (UnsupportedEncodingException e) { + logger.error("Unable to decompress logout message", e); + throw new RuntimeException(e); + } + } + /** * 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, + String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters); + // front channel request -> the message needs to be base64 decoded + decompressed + if ("GET".equals(request.getMethod())) { + logoutMessage = uncompressLogoutMessage(logoutMessage); + } logger.trace("Logout request:\n{}", logoutMessage); final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); 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..be705f4 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..e30254d --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java @@ -0,0 +1,37 @@ +package org.jasig.cas.client.session; + +import java.nio.charset.Charset; +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.3.1 + */ +public final class LogoutMessageGenerator { + + private static final String LOGOUT_REQUEST_TEMPLATE = + "@NOT_USED@" + + "%s"; + + public static String generateLogoutMessage(String sessionIndex) { + return String.format(LOGOUT_REQUEST_TEMPLATE, sessionIndex); + } + + public static String generateCompressedLogoutMessage(String sessionIndex) { + final String logoutMessage = generateLogoutMessage(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..82ecd52 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutFilterTests.java @@ -0,0 +1,118 @@ +/* + * 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.generateLogoutMessage(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.generateCompressedLogoutMessage(TICKET); + request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setQueryString(SingleSignOutHandler.DEFAULT_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.generateCompressedLogoutMessage(TICKET); + request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, logoutMessage); + request.setParameter(SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME, RELAY_STATE); + request.setQueryString(SingleSignOutHandler.DEFAULT_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..1479536 --- /dev/null +++ b/cas-client-core/src/test/java/org/jasig/cas/client/session/SingleSignOutHandlerTests.java @@ -0,0 +1,160 @@ +/* + * 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.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 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 isBackChannelLogoutRequest() throws Exception { + request.setParameter(logoutParameterName, TICKET); + request.setMethod("POST"); + + assertTrue(handler.isBackChannelLogoutRequest(request)); + } + + /** + * Tests that a multipart request is not considered logoutRequest. Verifies issue CASC-147. + * + * @throws Exception + */ + @Test + public void isBackChannelLogoutRequestMultipart() throws Exception { + request.setParameter(logoutParameterName, TICKET); + request.setMethod("POST"); + request.setContentType("multipart/form-data"); + + assertFalse(handler.isBackChannelLogoutRequest(request)); + } + + @Test + public void isFrontChannelLogoutRequest() { + request.setParameter(logoutParameterName, TICKET); + request.setMethod("GET"); + request.setQueryString(logoutParameterName + "=" + TICKET); + + assertTrue(handler.isFrontChannelLogoutRequest(request)); + } + + @Test + public void isFrontChannelLogoutRequestKO() { + request.setParameter(ANOTHER_PARAMETER, TICKET); + request.setMethod("GET"); + request.setQueryString(ANOTHER_PARAMETER + "=" + TICKET); + + assertFalse(handler.isFrontChannelLogoutRequest(request)); + } + + @Test + public void recordSessionKOIfNoSession() { + handler.setEagerlyCreateSessions(false); + request.setSession(null); + request.setParameter(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME, TICKET); + request.setQueryString(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME + "=" + TICKET); + handler.recordSession(request); + final SessionMappingStorage storage = handler.getSessionMappingStorage(); + assertNull(storage.removeSessionByMappingId(TICKET)); + } + + @Test + public void recordSessionOK() { + final MockHttpSession session = new MockHttpSession(); + request.setSession(session); + request.setParameter(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME, TICKET); + request.setQueryString(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME + "=" + TICKET); + handler.recordSession(request); + final SessionMappingStorage storage = handler.getSessionMappingStorage(); + assertEquals(session, storage.removeSessionByMappingId(TICKET)); + } + + @Test + public void destorySessionPOSTKONoSessionIndex() { + final String logoutMessage = LogoutMessageGenerator.generateLogoutMessage(""); + request.setParameter(logoutParameterName, logoutMessage); + request.setMethod("POST"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + handler.destroySession(request); + assertFalse(session.isInvalid()); + } + + @Test + public void destorySessionPOST() { + final String logoutMessage = LogoutMessageGenerator.generateLogoutMessage(TICKET); + request.setParameter(logoutParameterName, logoutMessage); + request.setMethod("POST"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + handler.destroySession(request); + assertTrue(session.isInvalid()); + } + + @Test + public void destorySessionGETNoSessionIndex() { + final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(""); + request.setParameter(logoutParameterName, logoutMessage); + request.setQueryString(logoutParameterName + "=" + logoutMessage); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + handler.destroySession(request); + assertFalse(session.isInvalid()); + } + + @Test + public void destorySessionGET() { + final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(TICKET); + request.setParameter(logoutParameterName, logoutMessage); + request.setQueryString(logoutParameterName + "=" + logoutMessage); + request.setMethod("GET"); + final MockHttpSession session = new MockHttpSession(); + handler.getSessionMappingStorage().addSessionById(TICKET, session); + handler.destroySession(request); + assertTrue(session.isInvalid()); + } +} 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..66d49f1 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 @@ -167,4 +167,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 =%/")); + } } From 9b33321cc251e633232ae1d10d4eee49b3d39d19 Mon Sep 17 00:00:00 2001 From: LELEU Jerome Date: Fri, 14 Mar 2014 10:52:42 +0100 Subject: [PATCH 2/6] CASC-220: Support front channel SLO logout specific front logout parameter + Tomcat valves update --- .../client/session/SingleSignOutFilter.java | 47 +++++-------- .../client/session/SingleSignOutHandler.java | 68 +++++++++++++++++-- .../session/LogoutMessageGenerator.java | 6 +- .../session/SingleSignOutFilterTests.java | 15 ++-- .../session/SingleSignOutHandlerTests.java | 51 ++++++++++---- .../client/tomcat/v6/SingleSignOutValve.java | 23 ++++++- .../client/tomcat/v7/SingleSignOutValve.java | 23 ++++++- 7 files changed, 168 insertions(+), 65 deletions(-) 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 1126218..92235dc 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 @@ -23,7 +23,6 @@ import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang.StringUtils; import org.jasig.cas.client.util.AbstractConfigurationFilter; import org.jasig.cas.client.util.CommonUtils; @@ -38,25 +37,21 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { private static final SingleSignOutHandler handler = new SingleSignOutHandler(); - /** The prefix url of the CAS server */ - private String casServerUrlPrefix; - - /** Parameter name that stores the state of the CAS server webflow for the callback */ - private String relayStateParameterName = SingleSignOutHandler.DEFAULT_RELAY_STATE_PARAMETER_NAME; - public void init(final FilterConfig filterConfig) throws ServletException { if (!isIgnoreInitConfiguration()) { handler.setArtifactParameterName(getPropertyFromInitParams(filterConfig, "artifactParameterName", SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME)); handler.setLogoutParameterName(getPropertyFromInitParams(filterConfig, "logoutParameterName", SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME)); - setRelayStateParameterName(getPropertyFromInitParams(filterConfig, "relayStateParameterName", + 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, "eagerlyCreateSessions", "true"))); - setCasServerUrlPrefix(getPropertyFromInitParams(filterConfig, "casServerUrlPrefix", null)); } handler.init(); } @@ -69,20 +64,22 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { handler.setLogoutParameterName(name); } + public void setFrontLogoutParameterName(final String name) { + handler.setFrontLogoutParameterName(name); + } + public void setRelayStateParameterName(final String name) { - this.relayStateParameterName = name; handler.setRelayStateParameterName(name); } + public void setCasServerUrlPrefix(final String casServerUrlPrefix) { + handler.setCasServerUrlPrefix(casServerUrlPrefix); + } + public void setSessionMappingStorage(final SessionMappingStorage storage) { handler.setSessionMappingStorage(storage); } - public void setCasServerUrlPrefix(final String casServerUrlPrefix) { - CommonUtils.assertNotNull(casServerUrlPrefix, "casServerUrlPrefix cannot be null."); - this.casServerUrlPrefix = casServerUrlPrefix; - } - public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; @@ -96,22 +93,10 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { return; } else if (handler.isFrontChannelLogoutRequest(request)) { handler.destroySession(request); - // relay state value - 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 StringBuffer buffer = new StringBuffer(); - 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("Redirecting back to the CAS server: {}", redirectUrl); - CommonUtils.sendRedirect(response, redirectUrl); + // redirection url to the CAS server + final String redirectionUrl = handler.computeRedirectionToServer(request); + if (redirectionUrl != null) { + CommonUtils.sendRedirect(response, redirectionUrl); } return; } else { 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 873d322..1b40afd 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 @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest; 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.XmlUtils; import org.slf4j.Logger; @@ -46,6 +47,7 @@ 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"; /** Logger instance */ @@ -57,12 +59,18 @@ public final class SingleSignOutHandler { /** The name of the artifact parameter. This is used to capture the session identifier. */ private String artifactParameterName = DEFAULT_ARTIFACT_PARAMETER_NAME; - /** Parameter name that stores logout request */ + /** 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; private boolean eagerlyCreateSessions = true; @@ -89,12 +97,26 @@ 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. */ @@ -112,8 +134,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); @@ -157,7 +181,7 @@ public final class SingleSignOutHandler { */ public boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { return "GET".equals(request.getMethod()) - && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName)); + && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName)); } /** @@ -220,11 +244,13 @@ public final class SingleSignOutHandler { * @param request HTTP request containing a CAS logout message. */ public void destroySession(final HttpServletRequest request) { - String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, - this.safeParameters); - // front channel request -> the message needs to be base64 decoded + decompressed + String logoutMessage; + // front channel logout -> the message needs to be base64 decoded + decompressed if ("GET".equals(request.getMethod())) { - logoutMessage = uncompressLogoutMessage(logoutMessage); + logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request, + this.frontLogoutParameterName)); + } else { + logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters); } logger.trace("Logout request:\n{}", logoutMessage); @@ -251,6 +277,34 @@ 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. + */ + public String computeRedirectionToServer(final HttpServletRequest request) { + // relay state value + 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 StringBuffer buffer = new StringBuffer(); + 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/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java b/cas-client-core/src/test/java/org/jasig/cas/client/session/LogoutMessageGenerator.java index e30254d..b43ea65 100644 --- 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 @@ -19,12 +19,12 @@ public final class LogoutMessageGenerator { + "IssueInstant=\"\">@NOT_USED@" + "%s"; - public static String generateLogoutMessage(String sessionIndex) { + public static String generateBackChannelLogoutMessage(String sessionIndex) { return String.format(LOGOUT_REQUEST_TEMPLATE, sessionIndex); } - public static String generateCompressedLogoutMessage(String sessionIndex) { - final String logoutMessage = generateLogoutMessage(sessionIndex); + public static String generateFrontChannelLogoutMessage(String sessionIndex) { + final String logoutMessage = generateBackChannelLogoutMessage(sessionIndex); final Deflater deflater = new Deflater(); deflater.setInput(logoutMessage.getBytes(Charset.forName("ASCII"))); deflater.finish(); 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 index 82ecd52..64114f9 100644 --- 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 @@ -79,7 +79,8 @@ public class SingleSignOutFilterTests { @Test public void backChannelRequest() throws IOException, ServletException { - request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, LogoutMessageGenerator.generateLogoutMessage(TICKET)); + request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, + LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET)); request.setMethod("POST"); final MockHttpSession session = new MockHttpSession(); SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session); @@ -89,9 +90,9 @@ public class SingleSignOutFilterTests { @Test public void frontChannelRequest() throws IOException, ServletException { - final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(TICKET); - request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, logoutMessage); - request.setQueryString(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage); + 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); @@ -102,10 +103,10 @@ public class SingleSignOutFilterTests { @Test public void frontChannelRequestRelayState() throws IOException, ServletException { - final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(TICKET); - request.setParameter(SingleSignOutHandler.DEFAULT_LOGOUT_PARAMETER_NAME, logoutMessage); + 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_LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + + 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(); 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 index 1479536..c7670e5 100644 --- 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 @@ -37,15 +37,23 @@ 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 SingleSignOutHandler handler; private MockHttpServletRequest request; - private final static String logoutParameterName = "logoutRequest"; + private final static String logoutParameterName = "logoutRequest2"; + private final static String frontLogoutParameterName = "SAMLRequest2"; + private final static String relayStateParameterName = "RelayState2"; + private final static String artifactParameterName = "ticket2"; @Before public void setUp() throws Exception { handler = new SingleSignOutHandler(); handler.setLogoutParameterName(logoutParameterName); + handler.setFrontLogoutParameterName(frontLogoutParameterName); + handler.setRelayStateParameterName(relayStateParameterName); + handler.setArtifactParameterName(artifactParameterName); + handler.setCasServerUrlPrefix(URL); handler.init(); request = new MockHttpServletRequest(); } @@ -74,9 +82,9 @@ public final class SingleSignOutHandlerTests { @Test public void isFrontChannelLogoutRequest() { - request.setParameter(logoutParameterName, TICKET); + request.setParameter(frontLogoutParameterName, TICKET); request.setMethod("GET"); - request.setQueryString(logoutParameterName + "=" + TICKET); + request.setQueryString(frontLogoutParameterName + "=" + TICKET); assertTrue(handler.isFrontChannelLogoutRequest(request)); } @@ -94,8 +102,8 @@ public final class SingleSignOutHandlerTests { public void recordSessionKOIfNoSession() { handler.setEagerlyCreateSessions(false); request.setSession(null); - request.setParameter(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME, TICKET); - request.setQueryString(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME + "=" + TICKET); + request.setParameter(artifactParameterName, TICKET); + request.setQueryString(artifactParameterName + "=" + TICKET); handler.recordSession(request); final SessionMappingStorage storage = handler.getSessionMappingStorage(); assertNull(storage.removeSessionByMappingId(TICKET)); @@ -105,8 +113,8 @@ public final class SingleSignOutHandlerTests { public void recordSessionOK() { final MockHttpSession session = new MockHttpSession(); request.setSession(session); - request.setParameter(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME, TICKET); - request.setQueryString(SingleSignOutHandler.DEFAULT_ARTIFACT_PARAMETER_NAME + "=" + TICKET); + request.setParameter(artifactParameterName, TICKET); + request.setQueryString(artifactParameterName + "=" + TICKET); handler.recordSession(request); final SessionMappingStorage storage = handler.getSessionMappingStorage(); assertEquals(session, storage.removeSessionByMappingId(TICKET)); @@ -114,7 +122,7 @@ public final class SingleSignOutHandlerTests { @Test public void destorySessionPOSTKONoSessionIndex() { - final String logoutMessage = LogoutMessageGenerator.generateLogoutMessage(""); + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(""); request.setParameter(logoutParameterName, logoutMessage); request.setMethod("POST"); final MockHttpSession session = new MockHttpSession(); @@ -125,7 +133,7 @@ public final class SingleSignOutHandlerTests { @Test public void destorySessionPOST() { - final String logoutMessage = LogoutMessageGenerator.generateLogoutMessage(TICKET); + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET); request.setParameter(logoutParameterName, logoutMessage); request.setMethod("POST"); final MockHttpSession session = new MockHttpSession(); @@ -136,9 +144,9 @@ public final class SingleSignOutHandlerTests { @Test public void destorySessionGETNoSessionIndex() { - final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(""); - request.setParameter(logoutParameterName, logoutMessage); - request.setQueryString(logoutParameterName + "=" + logoutMessage); + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(""); + request.setParameter(frontLogoutParameterName, logoutMessage); + request.setQueryString(frontLogoutParameterName + "=" + logoutMessage); request.setMethod("GET"); final MockHttpSession session = new MockHttpSession(); handler.getSessionMappingStorage().addSessionById(TICKET, session); @@ -148,13 +156,26 @@ public final class SingleSignOutHandlerTests { @Test public void destorySessionGET() { - final String logoutMessage = LogoutMessageGenerator.generateCompressedLogoutMessage(TICKET); - request.setParameter(logoutParameterName, logoutMessage); - request.setQueryString(logoutParameterName + "=" + logoutMessage); + final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); + request.setParameter(frontLogoutParameterName, logoutMessage); + request.setQueryString(frontLogoutParameterName + "=" + logoutMessage); request.setMethod("GET"); final MockHttpSession session = new MockHttpSession(); handler.getSessionMappingStorage().addSessionById(TICKET, session); handler.destroySession(request); assertTrue(session.isInvalid()); } + + @Test + public void computeRedirectionNoRelayState() { + assertNull(handler.computeRedirectionToServer(request)); + } + + @Test + public void computeRedirection() { + request.setParameter(relayStateParameterName, TICKET); + request.setQueryString(relayStateParameterName + "=" + TICKET); + assertEquals(URL + "/logout?_eventId=next&" + relayStateParameterName + "=" + TICKET, + handler.computeRedirectionToServer(request)); + } } 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..d483e41 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 @@ -28,6 +28,7 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.jasig.cas.client.session.SessionMappingStorage; import org.jasig.cas.client.session.SingleSignOutHandler; +import org.jasig.cas.client.util.CommonUtils; /** * Handles logout request messages sent from the CAS server by ending the current @@ -52,6 +53,18 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio 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); } @@ -68,10 +81,18 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio if (this.handler.isTokenRequest(request)) { this.handler.recordSession(request); request.getSessionInternal(true).addSessionListener(this); - } else if (this.handler.isLogoutRequest(request)) { + } else if (this.handler.isBackChannelLogoutRequest(request)) { this.handler.destroySession(request); // Do not proceed up valve chain return; + } else if (this.handler.isFrontChannelLogoutRequest(request)) { + this.handler.destroySession(request); + // redirection url to the CAS server + final String redirectionUrl = handler.computeRedirectionToServer(request); + if (redirectionUrl != null) { + CommonUtils.sendRedirect(response, redirectionUrl); + } + return; } else { logger.debug("Ignoring URI {}", request.getRequestURI()); } 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..8ecc865 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 @@ -29,6 +29,7 @@ import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; import org.jasig.cas.client.session.SessionMappingStorage; import org.jasig.cas.client.session.SingleSignOutHandler; +import org.jasig.cas.client.util.CommonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +57,18 @@ public class SingleSignOutValve extends ValveBase implements SessionListener { 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); } @@ -65,10 +78,18 @@ public class SingleSignOutValve extends ValveBase implements SessionListener { if (this.handler.isTokenRequest(request)) { this.handler.recordSession(request); request.getSessionInternal(true).addSessionListener(this); - } else if (this.handler.isLogoutRequest(request)) { + } else if (this.handler.isBackChannelLogoutRequest(request)) { this.handler.destroySession(request); // Do not proceed up valve chain return; + } else if (this.handler.isFrontChannelLogoutRequest(request)) { + this.handler.destroySession(request); + // redirection url to the CAS server + final String redirectionUrl = handler.computeRedirectionToServer(request); + if (redirectionUrl != null) { + CommonUtils.sendRedirect(response, redirectionUrl); + } + return; } else { logger.debug("Ignoring URI {}", request.getRequestURI()); } From 6aa237926885bc2ae452db2eef2d1b32ee172bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LELEU?= Date: Fri, 21 Mar 2014 14:10:33 +0100 Subject: [PATCH 3/6] CASC-220: Support front channel SLO logout Updates after Misagh's code review --- .../client/session/SingleSignOutFilter.java | 21 +----- .../client/session/SingleSignOutHandler.java | 75 ++++++++++++++++--- .../session/LogoutMessageGenerator.java | 7 +- .../client/tomcat/v6/SingleSignOutValve.java | 36 +++------ .../client/tomcat/v7/SingleSignOutValve.java | 35 +++------ 5 files changed, 90 insertions(+), 84 deletions(-) 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 92235dc..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 @@ -24,7 +24,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.jasig.cas.client.util.AbstractConfigurationFilter; -import org.jasig.cas.client.util.CommonUtils; /** * Implements the Single Sign Out protocol. It handles registering the session and destroying the session. @@ -85,25 +84,9 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; - if (handler.isTokenRequest(request)) { - handler.recordSession(request); - } else if (handler.isBackChannelLogoutRequest(request)) { - handler.destroySession(request); - // Do not continue up filter chain - return; - } else if (handler.isFrontChannelLogoutRequest(request)) { - handler.destroySession(request); - // redirection url to the CAS server - final String redirectionUrl = handler.computeRedirectionToServer(request); - if (redirectionUrl != null) { - CommonUtils.sendRedirect(response, redirectionUrl); - } - 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 1b40afd..11e43c3 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 @@ -26,6 +26,7 @@ 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; @@ -153,7 +154,7 @@ public final class SingleSignOutHandler { * * @return True if request contains authentication token, false otherwise. */ - public boolean isTokenRequest(final HttpServletRequest request) { + protected boolean isTokenRequest(final HttpServletRequest request) { return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters)); } @@ -165,7 +166,7 @@ public final class SingleSignOutHandler { * * @return True if request is logout request, false otherwise. */ - public boolean isBackChannelLogoutRequest(final HttpServletRequest request) { + protected boolean isBackChannelLogoutRequest(final HttpServletRequest request) { return "POST".equals(request.getMethod()) && !isMultipartRequest(request) && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, @@ -179,18 +180,46 @@ public final class SingleSignOutHandler { * * @return True if request is logout request, false otherwise. */ - public boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { + protected 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)) { + recordSession(request); + } else if (isBackChannelLogoutRequest(request)) { + destroySession(request); + // Do not continue up filter chain + return false; + } else if (isFrontChannelLogoutRequest(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 {}", 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) { + protected void recordSession(final HttpServletRequest request) { final HttpSession session = request.getSession(this.eagerlyCreateSessions); if (session == null) { @@ -215,17 +244,41 @@ public final class SingleSignOutHandler { * @param originalMessage the original logout message. * @return the uncompressed logout message. */ - private String uncompressLogoutMessage(final String originalMessage) { + protected String uncompressLogoutMessage(final String originalMessage) { // base64 decode final byte[] binaryMessage = Base64.decodeBase64(originalMessage); + Inflater decompresser = null; try { // decompress the bytes - final Inflater decompresser = new Inflater(); + decompresser = new Inflater(); decompresser.setInput(binaryMessage); + + /* The received logout message is compressed, so this number (10) is the multiplier of the original size + * of the logout message (binaryMessage.length) to compute the size of the buffer where the logout message + * will be decompressed. + * It's somehow the decompression factor. + * + * For the buffer, we could also have a fixed size for the buffer (like 10k), but I thought that ten times + * would be a sufficient multiplier... + * + * A real test: + * String sessionIndex = "ST-45-fs45646r84ffs1d31f554f5d4f64fg6r8eq5s4d6f4fddsf46-cas"; + * String bm = LogoutMessageGenerator.generateBackChannelLogoutMessage(sessionIndex); + * System.out.println("bm.size = " + bm.length()); + * String fm = new String(Base64.decodeBase64(LogoutMessageGenerator. + * generateFrontChannelLogoutMessage(sessionIndex))); + * System.out.println("fm.size = " + fm.length()); + * + * And the result: + * bm.size = 354 + * fm.size = 224 + * + * So ten times is enough, it's even too much... + */ byte[] result = new byte[binaryMessage.length * 10]; + int resultLength = decompresser.inflate(result); - decompresser.end(); // decode the bytes into a String return new String(result, 0, resultLength, "UTF-8"); @@ -235,6 +288,10 @@ public final class SingleSignOutHandler { } catch (UnsupportedEncodingException e) { logger.error("Unable to decompress logout message", e); throw new RuntimeException(e); + } finally { + if (decompresser != null) { + decompresser.end(); + } } } @@ -243,7 +300,7 @@ public final class SingleSignOutHandler { * * @param request HTTP request containing a CAS logout message. */ - public void destroySession(final HttpServletRequest request) { + protected void destroySession(final HttpServletRequest request) { String logoutMessage; // front channel logout -> the message needs to be base64 decoded + decompressed if ("GET".equals(request.getMethod())) { @@ -284,7 +341,7 @@ public final class SingleSignOutHandler { * @param request The HTTP request. * @return the redirection url to the CAS server. */ - public String computeRedirectionToServer(final HttpServletRequest request) { + protected String computeRedirectionToServer(final HttpServletRequest request) { // relay state value final String relayStateValue = CommonUtils.safeGetParameter(request, this.relayStateParameterName); // if we have a state value -> redirect to the CAS server to continue the logout process 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 index b43ea65..30ee253 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -16,13 +17,13 @@ public final class LogoutMessageGenerator { private static final String LOGOUT_REQUEST_TEMPLATE = "@NOT_USED@" + + "IssueInstant=\"%s\">@NOT_USED@" + "%s"; public static String generateBackChannelLogoutMessage(String sessionIndex) { - return String.format(LOGOUT_REQUEST_TEMPLATE, sessionIndex); + return String.format(LOGOUT_REQUEST_TEMPLATE, new Date(), sessionIndex); } - + public static String generateFrontChannelLogoutMessage(String sessionIndex) { final String logoutMessage = generateBackChannelLogoutMessage(sessionIndex); final Deflater deflater = new Deflater(); 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 d483e41..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 @@ -28,7 +28,6 @@ import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Response; import org.jasig.cas.client.session.SessionMappingStorage; import org.jasig.cas.client.session.SingleSignOutHandler; -import org.jasig.cas.client.util.CommonUtils; /** * Handles logout request messages sent from the CAS server by ending the current @@ -46,57 +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) { - handler.setFrontLogoutParameterName(name); + this.handler.setFrontLogoutParameterName(name); } public void setRelayStateParameterName(final String name) { - handler.setRelayStateParameterName(name); + this.handler.setRelayStateParameterName(name); } public void setCasServerUrlPrefix(final String casServerUrlPrefix) { - handler.setCasServerUrlPrefix(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.isBackChannelLogoutRequest(request)) { - this.handler.destroySession(request); - // Do not proceed up valve chain - return; - } else if (this.handler.isFrontChannelLogoutRequest(request)) { - this.handler.destroySession(request); - // redirection url to the CAS server - final String redirectionUrl = handler.computeRedirectionToServer(request); - if (redirectionUrl != null) { - CommonUtils.sendRedirect(response, redirectionUrl); - } - return; - } else { - logger.debug("Ignoring URI {}", request.getRequestURI()); + if (this.handler.process(request, response)) { + getNext().invoke(request, response); } - getNext().invoke(request, response); } /** {@inheritDoc} */ @@ -111,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 8ecc865..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 @@ -29,7 +29,6 @@ import org.apache.catalina.connector.Response; import org.apache.catalina.valves.ValveBase; import org.jasig.cas.client.session.SessionMappingStorage; import org.jasig.cas.client.session.SingleSignOutHandler; -import org.jasig.cas.client.util.CommonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,50 +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) { - handler.setFrontLogoutParameterName(name); + this.handler.setFrontLogoutParameterName(name); } public void setRelayStateParameterName(final String name) { - handler.setRelayStateParameterName(name); + this.handler.setRelayStateParameterName(name); } public void setCasServerUrlPrefix(final String casServerUrlPrefix) { - handler.setCasServerUrlPrefix(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.isBackChannelLogoutRequest(request)) { - this.handler.destroySession(request); - // Do not proceed up valve chain - return; - } else if (this.handler.isFrontChannelLogoutRequest(request)) { - this.handler.destroySession(request); - // redirection url to the CAS server - final String redirectionUrl = handler.computeRedirectionToServer(request); - if (redirectionUrl != null) { - CommonUtils.sendRedirect(response, redirectionUrl); - } - return; - } else { - logger.debug("Ignoring URI {}", request.getRequestURI()); + if (this.handler.process(request, response)) { + getNext().invoke(request, response); } - getNext().invoke(request, response); } /** {@inheritDoc} */ @@ -108,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."); } } From 7069a4f6fb2b816f77a18396095fb22123f1032e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LELEU?= Date: Thu, 27 Mar 2014 21:02:30 +0100 Subject: [PATCH 4/6] CASC-220: Support front channel SLO logout Update after Scott's code review --- .../client/session/SingleSignOutHandler.java | 68 +++---- .../session/LogoutMessageGenerator.java | 6 +- .../session/SingleSignOutHandlerTests.java | 191 +++++++++--------- 3 files changed, 122 insertions(+), 143 deletions(-) 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 11e43c3..8026ed1 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 @@ -18,10 +18,8 @@ */ package org.jasig.cas.client.session; -import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; -import java.util.zip.DataFormatException; import java.util.zip.Inflater; import javax.servlet.ServletException; @@ -50,7 +48,9 @@ public final class SingleSignOutHandler { 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()); @@ -154,7 +154,7 @@ public final class SingleSignOutHandler { * * @return True if request contains authentication token, false otherwise. */ - protected boolean isTokenRequest(final HttpServletRequest request) { + private boolean isTokenRequest(final HttpServletRequest request) { return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters)); } @@ -166,7 +166,7 @@ public final class SingleSignOutHandler { * * @return True if request is logout request, false otherwise. */ - protected boolean isBackChannelLogoutRequest(final HttpServletRequest request) { + private boolean isBackChannelLogoutRequest(final HttpServletRequest request) { return "POST".equals(request.getMethod()) && !isMultipartRequest(request) && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, @@ -180,7 +180,7 @@ public final class SingleSignOutHandler { * * @return True if request is logout request, false otherwise. */ - protected boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { + private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) { return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName)); } @@ -194,12 +194,17 @@ public final class SingleSignOutHandler { */ 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); - // Do not continue up filter chain 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); @@ -207,10 +212,11 @@ public final class SingleSignOutHandler { CommonUtils.sendRedirect(response, redirectionUrl); } return false; + } else { logger.trace("Ignoring URI {}", request.getRequestURI()); + return true; } - return true; } /** @@ -219,7 +225,7 @@ public final class SingleSignOutHandler { * * @param request HTTP request containing an authentication token. */ - protected void recordSession(final HttpServletRequest request) { + private void recordSession(final HttpServletRequest request) { final HttpSession session = request.getSession(this.eagerlyCreateSessions); if (session == null) { @@ -244,8 +250,7 @@ public final class SingleSignOutHandler { * @param originalMessage the original logout message. * @return the uncompressed logout message. */ - protected String uncompressLogoutMessage(final String originalMessage) { - // base64 decode + private String uncompressLogoutMessage(final String originalMessage) { final byte[] binaryMessage = Base64.decodeBase64(originalMessage); Inflater decompresser = null; @@ -253,39 +258,13 @@ public final class SingleSignOutHandler { // decompress the bytes decompresser = new Inflater(); decompresser.setInput(binaryMessage); + final byte[] result = new byte[binaryMessage.length * DECOMPRESSION_FACTOR]; - /* The received logout message is compressed, so this number (10) is the multiplier of the original size - * of the logout message (binaryMessage.length) to compute the size of the buffer where the logout message - * will be decompressed. - * It's somehow the decompression factor. - * - * For the buffer, we could also have a fixed size for the buffer (like 10k), but I thought that ten times - * would be a sufficient multiplier... - * - * A real test: - * String sessionIndex = "ST-45-fs45646r84ffs1d31f554f5d4f64fg6r8eq5s4d6f4fddsf46-cas"; - * String bm = LogoutMessageGenerator.generateBackChannelLogoutMessage(sessionIndex); - * System.out.println("bm.size = " + bm.length()); - * String fm = new String(Base64.decodeBase64(LogoutMessageGenerator. - * generateFrontChannelLogoutMessage(sessionIndex))); - * System.out.println("fm.size = " + fm.length()); - * - * And the result: - * bm.size = 354 - * fm.size = 224 - * - * So ten times is enough, it's even too much... - */ - byte[] result = new byte[binaryMessage.length * 10]; - - int resultLength = decompresser.inflate(result); + final int resultLength = decompresser.inflate(result); // decode the bytes into a String return new String(result, 0, resultLength, "UTF-8"); - } catch (DataFormatException e) { - logger.error("Unable to decompress logout message", e); - throw new RuntimeException(e); - } catch (UnsupportedEncodingException e) { + } catch (Exception e) { logger.error("Unable to decompress logout message", e); throw new RuntimeException(e); } finally { @@ -300,8 +279,8 @@ public final class SingleSignOutHandler { * * @param request HTTP request containing a CAS logout message. */ - protected void destroySession(final HttpServletRequest request) { - String logoutMessage; + private void destroySession(final HttpServletRequest request) { + final String logoutMessage; // front channel logout -> the message needs to be base64 decoded + decompressed if ("GET".equals(request.getMethod())) { logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request, @@ -341,12 +320,11 @@ public final class SingleSignOutHandler { * @param request The HTTP request. * @return the redirection url to the CAS server. */ - protected String computeRedirectionToServer(final HttpServletRequest request) { - // relay state value + 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 StringBuffer buffer = new StringBuffer(); + final StringBuilder buffer = new StringBuilder(); buffer.append(casServerUrlPrefix); if (!this.casServerUrlPrefix.endsWith("/")) { buffer.append("/"); 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 index 30ee253..1ebc862 100644 --- 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 @@ -11,7 +11,7 @@ import org.apache.commons.codec.binary.Base64; * Greatly inspired by the source code in the CAS server itself. * * @author Jerome Leleu - * @since 3.3.1 + * @since 3.4.0 */ public final class LogoutMessageGenerator { @@ -20,11 +20,11 @@ public final class LogoutMessageGenerator { + "IssueInstant=\"%s\">@NOT_USED@" + "%s"; - public static String generateBackChannelLogoutMessage(String sessionIndex) { + public static String generateBackChannelLogoutMessage(final String sessionIndex) { return String.format(LOGOUT_REQUEST_TEMPLATE, new Date(), sessionIndex); } - public static String generateFrontChannelLogoutMessage(String 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"))); 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 index c7670e5..afdf7bd 100644 --- 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 @@ -26,6 +26,7 @@ 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; /** @@ -38,144 +39,144 @@ 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 final static String logoutParameterName = "logoutRequest2"; - private final static String frontLogoutParameterName = "SAMLRequest2"; - private final static String relayStateParameterName = "RelayState2"; - private final static String artifactParameterName = "ticket2"; + private MockHttpServletResponse response; @Before public void setUp() throws Exception { handler = new SingleSignOutHandler(); - handler.setLogoutParameterName(logoutParameterName); - handler.setFrontLogoutParameterName(frontLogoutParameterName); - handler.setRelayStateParameterName(relayStateParameterName); - handler.setArtifactParameterName(artifactParameterName); + 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 isBackChannelLogoutRequest() throws Exception { - request.setParameter(logoutParameterName, TICKET); - request.setMethod("POST"); - - assertTrue(handler.isBackChannelLogoutRequest(request)); - } - - /** - * Tests that a multipart request is not considered logoutRequest. Verifies issue CASC-147. - * - * @throws Exception - */ - @Test - public void isBackChannelLogoutRequestMultipart() throws Exception { - request.setParameter(logoutParameterName, TICKET); - request.setMethod("POST"); - request.setContentType("multipart/form-data"); - - assertFalse(handler.isBackChannelLogoutRequest(request)); - } - - @Test - public void isFrontChannelLogoutRequest() { - request.setParameter(frontLogoutParameterName, TICKET); - request.setMethod("GET"); - request.setQueryString(frontLogoutParameterName + "=" + TICKET); - - assertTrue(handler.isFrontChannelLogoutRequest(request)); - } - - @Test - public void isFrontChannelLogoutRequestKO() { - request.setParameter(ANOTHER_PARAMETER, TICKET); - request.setMethod("GET"); - request.setQueryString(ANOTHER_PARAMETER + "=" + TICKET); - - assertFalse(handler.isFrontChannelLogoutRequest(request)); - } - - @Test - public void recordSessionKOIfNoSession() { + public void tokenRequestKOIfNoSession() { handler.setEagerlyCreateSessions(false); request.setSession(null); - request.setParameter(artifactParameterName, TICKET); - request.setQueryString(artifactParameterName + "=" + TICKET); - handler.recordSession(request); + 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 recordSessionOK() { + public void tokenRequestKOBadParameter() { final MockHttpSession session = new MockHttpSession(); request.setSession(session); - request.setParameter(artifactParameterName, TICKET); - request.setQueryString(artifactParameterName + "=" + TICKET); - handler.recordSession(request); + 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 destorySessionPOSTKONoSessionIndex() { - final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(""); - request.setParameter(logoutParameterName, logoutMessage); - request.setMethod("POST"); - final MockHttpSession session = new MockHttpSession(); - handler.getSessionMappingStorage().addSessionById(TICKET, session); - handler.destroySession(request); - assertFalse(session.isInvalid()); - } @Test - public void destorySessionPOST() { + public void backChannelLogoutKOMultipart() { final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET); - request.setParameter(logoutParameterName, logoutMessage); + request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); request.setMethod("POST"); + request.setContentType("multipart/form-data"); final MockHttpSession session = new MockHttpSession(); handler.getSessionMappingStorage().addSessionById(TICKET, session); - handler.destroySession(request); - assertTrue(session.isInvalid()); - } - - @Test - public void destorySessionGETNoSessionIndex() { - final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(""); - request.setParameter(frontLogoutParameterName, logoutMessage); - request.setQueryString(frontLogoutParameterName + "=" + logoutMessage); - request.setMethod("GET"); - final MockHttpSession session = new MockHttpSession(); - handler.getSessionMappingStorage().addSessionById(TICKET, session); - handler.destroySession(request); + assertTrue(handler.process(request, response)); assertFalse(session.isInvalid()); } @Test - public void destorySessionGET() { - final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); - request.setParameter(frontLogoutParameterName, logoutMessage); - request.setQueryString(frontLogoutParameterName + "=" + logoutMessage); - request.setMethod("GET"); + public void backChannelLogoutKONoSessionIndex() { + final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(""); + request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); + request.setMethod("POST"); final MockHttpSession session = new MockHttpSession(); handler.getSessionMappingStorage().addSessionById(TICKET, session); - handler.destroySession(request); + 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 computeRedirectionNoRelayState() { - assertNull(handler.computeRedirectionToServer(request)); + public void frontChannelLogoutKOBadParameter() { + 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 computeRedirection() { - request.setParameter(relayStateParameterName, TICKET); - request.setQueryString(relayStateParameterName + "=" + TICKET); - assertEquals(URL + "/logout?_eventId=next&" + relayStateParameterName + "=" + TICKET, - handler.computeRedirectionToServer(request)); + public void frontChannelLogoutKONoSessionIndex() { + 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()); } } From 4a0e960c32519a351b845e8efecbaf76979b1a12 Mon Sep 17 00:00:00 2001 From: LELEU Jerome Date: Tue, 1 Apr 2014 16:46:19 +0200 Subject: [PATCH 5/6] CASC-220: Support front channel SLO logout Updates after Marvin's code review --- .../client/session/SingleSignOutHandlerTests.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index afdf7bd..365a25e 100644 --- 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 @@ -62,7 +62,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void tokenRequestKOIfNoSession() { + public void tokenRequestFailsIfNoSession() { handler.setEagerlyCreateSessions(false); request.setSession(null); request.setParameter(ARTIFACT_PARAMETER_NAME, TICKET); @@ -73,7 +73,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void tokenRequestKOBadParameter() { + public void tokenRequestFailsIfBadParameter() { final MockHttpSession session = new MockHttpSession(); request.setSession(session); request.setParameter(ANOTHER_PARAMETER, TICKET); @@ -95,7 +95,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void backChannelLogoutKOMultipart() { + public void backChannelLogoutFailsIfMultipart() { final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(TICKET); request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); request.setMethod("POST"); @@ -107,7 +107,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void backChannelLogoutKONoSessionIndex() { + public void backChannelLogoutFailsIfNoSessionIndex() { final String logoutMessage = LogoutMessageGenerator.generateBackChannelLogoutMessage(""); request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage); request.setMethod("POST"); @@ -129,7 +129,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void frontChannelLogoutKOBadParameter() { + public void frontChannelLogoutFailsIfBadParameter() { final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET); request.setParameter(ANOTHER_PARAMETER, logoutMessage); request.setMethod("GET"); @@ -141,7 +141,7 @@ public final class SingleSignOutHandlerTests { } @Test - public void frontChannelLogoutKONoSessionIndex() { + public void frontChannelLogoutFailsIfNoSessionIndex() { final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(""); request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage); request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage); From c4c3223140ff4e3fc3e5011647119f0c6d12cfe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20LELEU?= Date: Mon, 7 Apr 2014 18:54:40 +0200 Subject: [PATCH 6/6] CASC-220: Support front channel SLO logout Update after new Misagh's comments --- .../org/jasig/cas/client/session/SingleSignOutHandler.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 8026ed1..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 @@ -214,7 +214,7 @@ public final class SingleSignOutHandler { return false; } else { - logger.trace("Ignoring URI {}", request.getRequestURI()); + logger.trace("Ignoring URI for logout: {}", request.getRequestURI()); return true; } } @@ -264,7 +264,7 @@ public final class SingleSignOutHandler { // decode the bytes into a String return new String(result, 0, resultLength, "UTF-8"); - } catch (Exception e) { + } catch (final Exception e) { logger.error("Unable to decompress logout message", e); throw new RuntimeException(e); } finally { @@ -282,7 +282,7 @@ public final class SingleSignOutHandler { private void destroySession(final HttpServletRequest request) { final String logoutMessage; // front channel logout -> the message needs to be base64 decoded + decompressed - if ("GET".equals(request.getMethod())) { + if (isFrontChannelLogoutRequest(request)) { logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName)); } else {