From 9ffca231a0c86617bf236342769505a2108398a5 Mon Sep 17 00:00:00 2001 From: Scott Battaglia Date: Sun, 29 Aug 2010 21:53:32 +0000 Subject: [PATCH] CASC-33 support for Tomcat: logout, proxy callback. --- .../jasig/cas/client/util/CommonUtils.java | 6 +- .../cas/client/util/CommonUtilsTests.java | 16 +++++ .../Saml11TicketValidatorTests.java | 3 +- .../tomcat/AbstractLogoutValveBase.java | 62 +++++++++++++++++ .../cas/client/tomcat/CasAuthenticator.java | 45 +++++++++---- .../cas/client/tomcat/ProxyCallbackValve.java | 66 +++++++++++++++++++ .../client/tomcat/RegExpBasedLogoutValue.java | 52 +++++++++++++++ .../client/tomcat/UrlBasedLogoutValve.java | 55 ++++++++++++++++ 8 files changed, 287 insertions(+), 18 deletions(-) create mode 100644 cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/AbstractLogoutValveBase.java create mode 100644 cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/ProxyCallbackValve.java create mode 100644 cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/RegExpBasedLogoutValue.java create mode 100644 cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/UrlBasedLogoutValve.java 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 52006dd..4034b47 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 @@ -217,12 +217,10 @@ public final class CommonUtils { buffer.append(request.getRequestURI()); if (CommonUtils.isNotBlank(request.getQueryString())) { - final int location = request.getQueryString().indexOf( - artifactParameterName + "="); + final int location = request.getQueryString().indexOf(artifactParameterName + "="); if (location == 0) { - final String returnValue = encode ? response.encodeURL(buffer - .toString()): buffer.toString(); + final String returnValue = encode ? response.encodeURL(buffer.toString()): buffer.toString(); if (LOG.isDebugEnabled()) { LOG.debug("serviceUrl generated: " + returnValue); } 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 99cb6e2..df56d3f 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 @@ -6,6 +6,8 @@ package org.jasig.cas.client.util; import junit.framework.TestCase; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; import java.util.ArrayList; import java.util.Collection; @@ -104,4 +106,18 @@ public final class CommonUtilsTests extends TestCase { assertFalse(CommonUtils.isNotBlank(null)); assertFalse(CommonUtils.isNotBlank(" ")); } + + public void testConstructServiceUrlWithTrailingSlash() { + final String CONST_MY_URL = "https://www.myserver.com/hello/hithere/"; + final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hello/hithere/"); + request.setScheme("https"); + request.setSecure(true); + final MockHttpServletResponse response = new MockHttpServletResponse(); + final String constructedUrl = CommonUtils.constructServiceUrl(request, response, null, "www.myserver.com", "ticket", false); + + assertEquals(CONST_MY_URL, constructedUrl); + + + + } } diff --git a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java index b943da4..cb03a49 100644 --- a/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java +++ b/cas-client-core/src/test/java/org/jasig/cas/client/validation/Saml11TicketValidatorTests.java @@ -33,8 +33,7 @@ public final class Saml11TicketValidatorTests extends AbstractTicketValidatorTes " ResponseID=\"_3b62bece2e8da1c10279db04882012ac\">Success"; - PublicTestHttpServer.instance().content = RESPONSE - .getBytes(PublicTestHttpServer.instance().encoding); + PublicTestHttpServer.instance().content = RESPONSE.getBytes(PublicTestHttpServer.instance().encoding); try { this.validator.validate("test", "test"); fail("ValidationException expected due to 'no' response"); diff --git a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/AbstractLogoutValveBase.java b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/AbstractLogoutValveBase.java new file mode 100644 index 0000000..9a4bccd --- /dev/null +++ b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/AbstractLogoutValveBase.java @@ -0,0 +1,62 @@ +package org.jasig.cas.client.tomcat; + +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jasig.cas.client.util.AbstractCasFilter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +/** + * Abstract base class for Container-managed log out. Removes the attributes + * from the session. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.12 + */ +public abstract class AbstractLogoutValveBase extends ValveBase { + + protected final Log log = LogFactory.getLog(getClass()); + + public final void invoke(final Request request, final Response response) throws IOException, ServletException { + + if (!isLogoutRequest(request)) { + log.debug("Current request URI [ " + request.getRequestURI() + "] is not a logout request."); + getNext().invoke(request, response); + return; + } + + final HttpSession httpSession = request.getSession(false); + + if (httpSession != null) { + httpSession.removeAttribute(AbstractCasFilter.CONST_CAS_ASSERTION); + } + + final String redirectUrl = constructRedirectUrl(request); + + if (redirectUrl != null) { + response.sendRedirect(redirectUrl); + } + } + + /** + * Determines if this is a request to destroy the container-managed single sign on session. + * + * @param request the request. CANNOT be NULL. + * @return true if it is a logout request, false otherwise. + */ + protected abstract boolean isLogoutRequest(Request request); + + /** + * Constructs a url to redirect to. + * + * @param request the original request. + * @return the url to redirect to. CAN be NULL. + */ + protected abstract String constructRedirectUrl(Request request); +} diff --git a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/CasAuthenticator.java b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/CasAuthenticator.java index 5f41826..952060e 100644 --- a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/CasAuthenticator.java +++ b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/CasAuthenticator.java @@ -3,9 +3,13 @@ package org.jasig.cas.client.tomcat; import org.apache.catalina.authenticator.AuthenticatorBase; import org.apache.catalina.connector.Request; import org.apache.catalina.deploy.LoginConfig; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.jasig.cas.client.util.AbstractCasFilter; import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.validation.Assertion; +import org.jasig.cas.client.validation.TicketValidationException; +import org.jasig.cas.client.validation.TicketValidator; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -21,6 +25,8 @@ public abstract class CasAuthenticator extends AuthenticatorBase { private static final String INFO = "org.jasig.cas.client.tomcat.CasAuthenticator/1.0"; + private static final Log log = LogFactory.getLog(CasAuthenticator.class); + private String serverName; private String serviceUrl; @@ -35,6 +41,8 @@ public abstract class CasAuthenticator extends AuthenticatorBase { protected abstract String getServiceParameterName(); + private TicketValidator ticketValidator; + public String getInfo() { return INFO; } @@ -43,25 +51,38 @@ public abstract class CasAuthenticator extends AuthenticatorBase { final Assertion assertion = (Assertion) request.getSession(true).getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION); if (assertion != null) { - return true; + return isKnownUser(assertion); } - final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); - - if (CommonUtils.isBlank(ticket)) { - final String serviceUrl = CommonUtils.constructServiceUrl(request, httpServletResponse, this.serviceUrl, this.serverName, getArtifactParameterName(), this.encode); - final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), serviceUrl, this.renew, false); - - httpServletResponse.sendRedirect(urlToRedirectTo); - return false; - } - - final Principal principal = this.context.getRealm().authenticate(null, ticket); return false; //To change body of implemented methods use File | Settings | File Templates. } + protected boolean isKnownUser(final Assertion assertion) { + return true; + } + + protected boolean isAuthenticatedRequest(final HttpServletRequest request, final HttpServletResponse response) throws IOException { + final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName()); + final String serviceUrl = CommonUtils.constructServiceUrl(request, response, this.serviceUrl, this.serverName, getArtifactParameterName(), this.encode); + + if (CommonUtils.isBlank(ticket)) { + final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(), serviceUrl, this.renew, false); + response.sendRedirect(urlToRedirectTo); + return false; + } + + try { + final Assertion assertion = this.ticketValidator.validate(ticket, serviceUrl); + request.getSession(true).setAttribute(AbstractCasFilter.CONST_CAS_ASSERTION, assertion); + return isKnownUser(assertion); + } catch (final TicketValidationException e) { + return false; + } + + } + } diff --git a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/ProxyCallbackValve.java b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/ProxyCallbackValve.java new file mode 100644 index 0000000..6eba1e1 --- /dev/null +++ b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/ProxyCallbackValve.java @@ -0,0 +1,66 @@ +package org.jasig.cas.client.tomcat; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.ValveBase; +import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage; +import org.jasig.cas.client.util.CommonUtils; + +import javax.servlet.ServletException; +import java.io.IOException; + +/** + * Handles watching a url for the proxy callback. + *

+ * Because its tough to share state between valves, we expose the storage mechanism via a static variable. + *

+ * This valve should be ordered before the authentication valves. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.12 + */ +public final class ProxyCallbackValve extends ValveBase { + + private static ProxyGrantingTicketStorage PROXY_GRANTING_TICKET_STORAGE; + + private String proxyGrantingTicketStorageClass; + + private String proxyCallbackUrl; + + public static ProxyGrantingTicketStorage getProxyGrantingTicketStorage() { + return PROXY_GRANTING_TICKET_STORAGE; + } + + public void setProxyGrantingTicketStorageClass(final String proxyGrantingTicketStorageClass) { + this.proxyGrantingTicketStorageClass = proxyGrantingTicketStorageClass; + } + + public void setProxyCallbackUrl(final String proxyCallbackUrl) { + this.proxyCallbackUrl = proxyCallbackUrl; + } + + protected void startInternal() throws LifecycleException { + super.startInternal(); + + try { + CommonUtils.assertNotNull(this.proxyCallbackUrl, "the proxy callback url cannot be null"); + CommonUtils.assertTrue(this.proxyCallbackUrl.startsWith("/"), "proxy callback url must start with \"/\""); + + final Class proxyGrantingTicketStorage = Class.forName(proxyGrantingTicketStorageClass); + PROXY_GRANTING_TICKET_STORAGE = (ProxyGrantingTicketStorage) proxyGrantingTicketStorage.newInstance(); + } catch (final Exception e) { + throw new LifecycleException(e); + } + } + + public void invoke(final Request request, final Response response) throws IOException, ServletException { + if (this.proxyCallbackUrl.equals(request.getRequestURI())) { + CommonUtils.readAndRespondToProxyReceptorRequest(request, response, PROXY_GRANTING_TICKET_STORAGE); + return; + } + + getNext().invoke(request, response); + } +} diff --git a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/RegExpBasedLogoutValue.java b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/RegExpBasedLogoutValue.java new file mode 100644 index 0000000..042205e --- /dev/null +++ b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/RegExpBasedLogoutValue.java @@ -0,0 +1,52 @@ +package org.jasig.cas.client.tomcat; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.jasig.cas.client.util.CommonUtils; + +import java.util.regex.Pattern; + +/** + * Matches a number of urls (based on the regular expression) for handling + * log out. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.12 + */ +public final class RegExpBasedLogoutValue extends AbstractLogoutValveBase { + + private String regexpUri; + + private Pattern regexpUriPattern; + + private String redirectUrl; + + public void setRegexpUri(final String regexpUri) { + this.regexpUri = regexpUri; + } + + public void setRedirectUrl(final String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + protected void startInternal() throws LifecycleException { + super.startInternal(); + + try { + CommonUtils.assertNotNull(this.regexpUri, "A Regular Expression must be provided."); + + this.regexpUriPattern = Pattern.compile(this.regexpUri); + } catch (final Exception e) { + throw new LifecycleException(e); + } + } + + protected boolean isLogoutRequest(final Request request) { + return this.regexpUriPattern.matcher(request.getRequestURI()).matches(); + } + + protected String constructRedirectUrl(final Request request) { + return this.redirectUrl; + } +} diff --git a/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/UrlBasedLogoutValve.java b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/UrlBasedLogoutValve.java new file mode 100644 index 0000000..2410755 --- /dev/null +++ b/cas-client-integration-tomcat/src/main/java/org/jasig/cas/client/tomcat/UrlBasedLogoutValve.java @@ -0,0 +1,55 @@ +package org.jasig.cas.client.tomcat; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Request; +import org.jasig.cas.client.util.CommonUtils; + +/** + * Monitors a specific url for logout requests. + * + * @author Scott Battaglia + * @version $Revision$ $Date$ + * @since 3.1.12 + */ +public final class UrlBasedLogoutValve extends AbstractLogoutValveBase { + + private String logoutUri; + + private String redirectUrl; + + /** + * The logout url to watch for logout requests. + * + * @param logoutUri the url. CANNOT be null. MUST be relative and start with "/" + */ + public void setLogoutUri(final String logoutUri) { + this.logoutUri = logoutUri; + } + + /** + * Optional url to redirect to after logout is complete. + * + * @param redirectUrl the url. CAN be NULL. + */ + public void setRedirectUrl(final String redirectUrl) { + this.redirectUrl = redirectUrl; + } + + protected void startInternal() throws LifecycleException { + super.startInternal(); + try { + CommonUtils.assertNotNull(this.logoutUri, "logoutUri cannot be null."); + CommonUtils.assertTrue(this.logoutUri.startsWith("/"), "logoutUri must start with \"/\""); + } catch (final IllegalArgumentException e) { + throw new LifecycleException(e); + } + } + + protected boolean isLogoutRequest(final Request request) { + return this.logoutUri.equals(request.getRequestURI()); + } + + protected String constructRedirectUrl(final Request request) { + return redirectUrl; + } +}