Merge pull request #187 from Unicon/Frontchannel-slo

Handle front-channel SLO
This commit is contained in:
Misagh Moayyed 2017-05-22 12:25:15 -04:00 committed by GitHub
commit f5a0ee6987
9 changed files with 52 additions and 132 deletions

View File

@ -77,6 +77,5 @@ public interface ConfigurationKeys {
ConfigurationKey<String> ALLOWED_PROXY_CHAINS = new ConfigurationKey<String>("allowedProxyChains", null);
ConfigurationKey<Class<? extends Cas20ServiceTicketValidator>> TICKET_VALIDATOR_CLASS = new ConfigurationKey<Class<? extends Cas20ServiceTicketValidator>>("ticketValidatorClass", null);
ConfigurationKey<String> PROXY_CALLBACK_URL = new ConfigurationKey<String>("proxyCallbackUrl", null);
ConfigurationKey<String> FRONT_LOGOUT_PARAMETER_NAME = new ConfigurationKey<String>("frontLogoutParameterName", "SAMLRequest");
ConfigurationKey<String> RELAY_STATE_PARAMETER_NAME = new ConfigurationKey<String>("relayStateParameterName", "RelayState");
}

View File

@ -46,7 +46,6 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter {
if (!isIgnoreInitConfiguration()) {
setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
setFrontLogoutParameterName(getString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME));
setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
setCasServerUrlPrefix(getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX));
HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
@ -63,11 +62,7 @@ public final class SingleSignOutFilter extends AbstractConfigurationFilter {
public void setLogoutParameterName(final String name) {
HANDLER.setLogoutParameterName(name);
}
public void setFrontLogoutParameterName(final String name) {
HANDLER.setFrontLogoutParameterName(name);
}
public void setRelayStateParameterName(final String name) {
HANDLER.setRelayStateParameterName(name);
}

View File

@ -57,12 +57,9 @@ public final class SingleSignOutHandler {
/** The name of the artifact parameter. This is used to capture the session identifier. */
private String artifactParameterName = Protocol.CAS2.getArtifactParameterName();
/** Parameter name that stores logout request for back channel SLO */
/** Parameter name that stores logout request for SLO */
private String logoutParameterName = ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue();
/** Parameter name that stores logout request for front channel SLO */
private String frontLogoutParameterName = ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue();
/** Parameter name that stores the state of the CAS server webflow for the callback */
private String relayStateParameterName = ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue();
@ -75,7 +72,7 @@ public final class SingleSignOutHandler {
private List<String> safeParameters;
private LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
private final LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
public void setSessionMappingStorage(final SessionMappingStorage storage) {
this.sessionMappingStorage = storage;
@ -97,7 +94,7 @@ public final class SingleSignOutHandler {
}
/**
* @param name Name of parameter containing CAS logout request message for back channel SLO.
* @param name Name of parameter containing CAS logout request message for SLO.
*/
public void setLogoutParameterName(final String name) {
this.logoutParameterName = name;
@ -109,14 +106,7 @@ public final class SingleSignOutHandler {
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.
*/
@ -135,7 +125,6 @@ public final class SingleSignOutHandler {
if (this.safeParameters == null) {
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.");
@ -165,32 +154,25 @@ public final class SingleSignOutHandler {
}
/**
* Determines whether the given request is a CAS back channel logout request.
* Determines whether the given request is a CAS logout request.
*
* @param request HTTP request.
*
* @return True if request is logout request, false otherwise.
*/
private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
return "POST".equals(request.getMethod())
&& !isMultipartRequest(request)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
private boolean isLogoutRequest(final HttpServletRequest request) {
if ("POST".equalsIgnoreCase(request.getMethod())) {
return !isMultipartRequest(request)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
}
if ("GET".equalsIgnoreCase(request.getMethod())) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters));
}
return false;
}
/**
* Determines whether the given request is a CAS front channel logout request. Front Channel log out requests are only supported
* when the 'casServerUrlPrefix' value is set.
*
* @param request HTTP request.
*
* @return True if request is logout request, false otherwise.
*/
private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(this.casServerUrlPrefix)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
}
/**
* Process a request regarding the SLO process: record the session or destroy it.
*
@ -203,26 +185,15 @@ public final class SingleSignOutHandler {
logger.trace("Received a token request");
recordSession(request);
return true;
} else if (isBackChannelLogoutRequest(request)) {
logger.trace("Received a back channel logout request");
}
if (isLogoutRequest(request)) {
logger.trace("Received a logout request");
destroySession(request);
return false;
} else if (isFrontChannelLogoutRequest(request)) {
logger.trace("Received a front channel logout request");
destroySession(request);
// redirection url to the CAS server
final String redirectionUrl = computeRedirectionToServer(request);
if (redirectionUrl != null) {
CommonUtils.sendRedirect(response, redirectionUrl);
}
return false;
} else {
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
}
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
/**
@ -245,7 +216,7 @@ public final class SingleSignOutHandler {
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception e) {
// ignore if the session is already marked as invalid. Nothing we can do!
// ignore if the session is already marked as invalid. Nothing we can do!
}
sessionMappingStorage.addSessionById(token, session);
}
@ -286,16 +257,17 @@ public final class SingleSignOutHandler {
* @param request HTTP request containing a CAS logout message.
*/
private void destroySession(final HttpServletRequest request) {
final String logoutMessage;
// front channel logout -> the message needs to be base64 decoded + decompressed
if (isFrontChannelLogoutRequest(request)) {
logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
this.frontLogoutParameterName));
} else {
logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
if (CommonUtils.isBlank(logoutMessage)) {
logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
return;
}
if (!logoutMessage.contains("SessionIndex")) {
logoutMessage = uncompressLogoutMessage(logoutMessage);
}
logger.trace("Logout request:\n{}", logoutMessage);
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
@ -314,33 +286,6 @@ public final class SingleSignOutHandler {
}
}
/**
* Compute the redirection url to the CAS server when it's a front channel SLO
* (depending on the relay state parameter).
*
* @param request The HTTP request.
* @return the redirection url to the CAS server.
*/
private String computeRedirectionToServer(final HttpServletRequest request) {
final String relayStateValue = CommonUtils.safeGetParameter(request, this.relayStateParameterName);
// if we have a state value -> redirect to the CAS server to continue the logout process
if (CommonUtils.isNotBlank(relayStateValue)) {
final StringBuilder buffer = new StringBuilder();
buffer.append(casServerUrlPrefix);
if (!this.casServerUrlPrefix.endsWith("/")) {
buffer.append("/");
}
buffer.append("logout?_eventId=next&");
buffer.append(this.relayStateParameterName);
buffer.append("=");
buffer.append(CommonUtils.urlEncode(relayStateValue));
final String redirectUrl = buffer.toString();
logger.debug("Redirection url to the CAS server: {}", redirectUrl);
return redirectUrl;
}
return null;
}
private boolean isMultipartRequest(final HttpServletRequest request) {
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");
}

View File

@ -54,7 +54,7 @@ public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketVal
TOLERANCE.getName(), IGNORE_PATTERN.getName(), IGNORE_URL_PATTERN_TYPE.getName(), HOSTNAME_VERIFIER.getName(), HOSTNAME_VERIFIER_CONFIG.getName(),
EXCEPTION_ON_VALIDATION_FAILURE.getName(), REDIRECT_AFTER_VALIDATION.getName(), USE_SESSION.getName(), SECRET_KEY.getName(), CIPHER_ALGORITHM.getName(), PROXY_RECEPTOR_URL.getName(),
PROXY_GRANTING_TICKET_STORAGE_CLASS.getName(), MILLIS_BETWEEN_CLEAN_UPS.getName(), ACCEPT_ANY_PROXY.getName(), ALLOWED_PROXY_CHAINS.getName(), TICKET_VALIDATOR_CLASS.getName(),
PROXY_CALLBACK_URL.getName(), FRONT_LOGOUT_PARAMETER_NAME.getName(), RELAY_STATE_PARAMETER_NAME.getName()
PROXY_CALLBACK_URL.getName(), RELAY_STATE_PARAMETER_NAME.getName()
};
/**

View File

@ -87,8 +87,8 @@ public class SingleSignOutFilterTests {
@Test
public void frontChannelRequest() throws IOException, ServletException {
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
request.setParameter(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
request.setQueryString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage);
request.setParameter(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
request.setQueryString(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage);
request.setMethod("GET");
final MockHttpSession session = new MockHttpSession();
SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage().addSessionById(TICKET, session);
@ -100,16 +100,14 @@ public class SingleSignOutFilterTests {
@Test
public void frontChannelRequestRelayState() throws IOException, ServletException {
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
request.setParameter(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
request.setParameter(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue(), logoutMessage);
request.setParameter(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue(), RELAY_STATE);
request.setQueryString(ConfigurationKeys.FRONT_LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage + "&" +
request.setQueryString(ConfigurationKeys.LOGOUT_PARAMETER_NAME.getDefaultValue() + "=" + logoutMessage + "&" +
ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue() + "=" + 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&" +
ConfigurationKeys.RELAY_STATE_PARAMETER_NAME.getDefaultValue() + "=" + RELAY_STATE, response.getRedirectedUrl());
}
}

View File

@ -31,7 +31,6 @@ import org.springframework.mock.web.MockHttpSession;
/**
* @author Matt Brown <matt.brown@citrix.com>
* @version $Revision$ $Date$
* @since 3.2.1
*/
public final class SingleSignOutHandlerTests {
@ -39,9 +38,8 @@ 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 LOGOUT_PARAMETER_NAME = "logoutRequest";
private final static String RELAY_STATE_PARAMETER_NAME = "RelayState";
private final static String ARTIFACT_PARAMETER_NAME = "ticket2";
private SingleSignOutHandler handler;
@ -52,7 +50,6 @@ public final class SingleSignOutHandlerTests {
public void setUp() throws Exception {
handler = new SingleSignOutHandler();
handler.setLogoutParameterName(LOGOUT_PARAMETER_NAME);
handler.setFrontLogoutParameterName(FRONT_LOGOUT_PARAMETER_NAME);
handler.setRelayStateParameterName(RELAY_STATE_PARAMETER_NAME);
handler.setArtifactParameterName(ARTIFACT_PARAMETER_NAME);
handler.setCasServerUrlPrefix(URL);
@ -143,8 +140,8 @@ public final class SingleSignOutHandlerTests {
@Test
public void frontChannelLogoutFailsIfNoSessionIndex() {
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage("");
request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage);
request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
request.setQueryString(LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
request.setMethod("GET");
final MockHttpSession session = new MockHttpSession();
handler.getSessionMappingStorage().addSessionById(TICKET, session);
@ -155,8 +152,8 @@ public final class SingleSignOutHandlerTests {
@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.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
request.setQueryString(LOGOUT_PARAMETER_NAME + "=" + logoutMessage);
request.setMethod("GET");
final MockHttpSession session = new MockHttpSession();
handler.getSessionMappingStorage().addSessionById(TICKET, session);
@ -168,15 +165,13 @@ public final class SingleSignOutHandlerTests {
@Test
public void frontChannelLogoutRelayStateOK() {
final String logoutMessage = LogoutMessageGenerator.generateFrontChannelLogoutMessage(TICKET);
request.setParameter(FRONT_LOGOUT_PARAMETER_NAME, logoutMessage);
request.setParameter(LOGOUT_PARAMETER_NAME, logoutMessage);
request.setParameter(RELAY_STATE_PARAMETER_NAME, TICKET);
request.setQueryString(FRONT_LOGOUT_PARAMETER_NAME + "=" + logoutMessage + "&" + RELAY_STATE_PARAMETER_NAME + "=" + TICKET);
request.setQueryString(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());
}
}

View File

@ -51,11 +51,7 @@ public class SingleSignOutValve extends AbstractLifecycleValve implements Sessio
public void setLogoutParameterName(final String name) {
this.handler.setLogoutParameterName(name);
}
public void setFrontLogoutParameterName(final String name) {
this.handler.setFrontLogoutParameterName(name);
}
public void setRelayStateParameterName(final String name) {
this.handler.setRelayStateParameterName(name);
}

View File

@ -55,11 +55,7 @@ public class SingleSignOutValve extends ValveBase implements SessionListener {
public void setLogoutParameterName(final String name) {
this.handler.setLogoutParameterName(name);
}
public void setFrontLogoutParameterName(final String name) {
this.handler.setFrontLogoutParameterName(name);
}
public void setRelayStateParameterName(final String name) {
this.handler.setRelayStateParameterName(name);
}

View File

@ -55,11 +55,7 @@ public class SingleSignOutValve extends ValveBase implements SessionListener {
public void setLogoutParameterName(final String name) {
this.handler.setLogoutParameterName(name);
}
public void setFrontLogoutParameterName(final String name) {
this.handler.setFrontLogoutParameterName(name);
}
public void setRelayStateParameterName(final String name) {
this.handler.setRelayStateParameterName(name);
}