CASC-220: Support front channel SLO logout

Update after Scott's code review
This commit is contained in:
Jérôme LELEU 2014-03-27 21:02:30 +01:00
parent 6aa2379268
commit 7069a4f6fb
3 changed files with 122 additions and 143 deletions

View File

@ -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("/");

View File

@ -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\"><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">@NOT_USED@"
+ "</saml:NameID><samlp:SessionIndex>%s</samlp:SessionIndex></samlp:LogoutRequest>";
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")));

View File

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