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()); } }