CASC-220: Support front channel SLO logout
Update after Scott's code review
This commit is contained in:
parent
6aa2379268
commit
7069a4f6fb
|
|
@ -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("/");
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue