Browse Source

Initial CAS support.

Ben Alex 21 năm trước cách đây
mục cha
commit
fa9b872570
60 tập tin đã thay đổi với 5561 bổ sung17 xóa
  1. 3 0
      .classpath
  2. 116 0
      adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandler.java
  3. 142 0
      adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxy.java
  4. 7 0
      adapters/cas/src/main/java/org/acegisecurity/adapters/cas/package.html
  5. 53 0
      adapters/cas/src/main/resources/org/acegisecurity/adapters/cas/applicationContext.xml
  6. 112 0
      adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxyTests.java
  7. 107 0
      adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerTests.java
  8. 47 0
      adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-invalid.xml
  9. 49 0
      adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-valid.xml
  10. 220 0
      core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationProvider.java
  11. 172 0
      core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationToken.java
  12. 59 0
      core/src/main/java/org/acegisecurity/providers/cas/CasAuthoritiesPopulator.java
  13. 72 0
      core/src/main/java/org/acegisecurity/providers/cas/CasProxyDecider.java
  14. 50 0
      core/src/main/java/org/acegisecurity/providers/cas/ProxyUntrustedException.java
  15. 116 0
      core/src/main/java/org/acegisecurity/providers/cas/StatelessTicketCache.java
  16. 102 0
      core/src/main/java/org/acegisecurity/providers/cas/TicketResponse.java
  17. 53 0
      core/src/main/java/org/acegisecurity/providers/cas/TicketValidator.java
  18. 108 0
      core/src/main/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCache.java
  19. 6 0
      core/src/main/java/org/acegisecurity/providers/cas/package.html
  20. 67 0
      core/src/main/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulator.java
  21. 5 0
      core/src/main/java/org/acegisecurity/providers/cas/populator/package.html
  22. 55 0
      core/src/main/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxy.java
  23. 87 0
      core/src/main/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDecider.java
  24. 63 0
      core/src/main/java/org/acegisecurity/providers/cas/proxy/RejectProxyTickets.java
  25. 6 0
      core/src/main/java/org/acegisecurity/providers/cas/proxy/package.html
  26. 96 0
      core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidator.java
  27. 128 0
      core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidator.java
  28. 5 0
      core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/package.html
  29. 111 0
      core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilter.java
  30. 102 0
      core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPoint.java
  31. 86 0
      core/src/main/java/org/acegisecurity/ui/cas/ServiceProperties.java
  32. 6 0
      core/src/main/java/org/acegisecurity/ui/cas/package.html
  33. 6 3
      core/src/main/java/org/acegisecurity/userdetails/User.java
  34. 1 1
      core/src/test/java/org/acegisecurity/MockHttpServletRequest.java
  35. 404 0
      core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java
  36. 283 0
      core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java
  37. 102 0
      core/src/test/java/org/acegisecurity/providers/cas/TicketResponseTests.java
  38. 89 0
      core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java
  39. 148 0
      core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java
  40. 66 0
      core/src/test/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxyTests.java
  41. 141 0
      core/src/test/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDeciderTests.java
  42. 84 0
      core/src/test/java/org/acegisecurity/providers/cas/proxy/RejectProxyTicketsTests.java
  43. 143 0
      core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidatorTests.java
  44. 138 0
      core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidatorTests.java
  45. 126 0
      core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPointTests.java
  46. 93 0
      core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterTests.java
  47. 71 0
      core/src/test/java/org/acegisecurity/ui/cas/ServicePropertiesTests.java
  48. 606 13
      docs/reference/src/index.xml
  49. 29 0
      lib/cas/LICENSE
  50. 2 0
      lib/cas/version.txt
  51. 58 0
      lib/ehcache/LICENSE.txt
  52. 1 0
      lib/ehcache/version.txt
  53. 52 0
      samples/contacts/build.xml
  54. 229 0
      samples/contacts/etc/cas/applicationContext.xml
  55. 20 0
      samples/contacts/etc/cas/casfailed.jsp
  56. 160 0
      samples/contacts/etc/cas/web.xml
  57. 15 0
      samples/contacts/etc/ssl/acegisecurity.txt
  58. 82 0
      samples/contacts/etc/ssl/howto.txt
  59. BIN
      samples/contacts/etc/ssl/keystore
  60. 1 0
      samples/contacts/project.properties

+ 3 - 0
.classpath

@@ -21,5 +21,8 @@
 	<classpathentry kind="lib" path="lib/regexp/jakarta-oro.jar"/>
 	<classpathentry kind="lib" path="lib/jakarta-commons/commons-codec.jar"/>
 	<classpathentry kind="lib" path="lib/hsqldb/hsqldb.jar"/>
+	<classpathentry kind="lib" path="lib/cas/cas.jar"/>
+	<classpathentry kind="lib" path="lib/cas/casclient.jar"/>
+	<classpathentry kind="lib" path="lib/ehcache/ehcache.jar"/>
 	<classpathentry kind="output" path="target/eclipseclasses"/>
 </classpath>

+ 116 - 0
adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandler.java

@@ -0,0 +1,116 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.adapters.cas;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationManager;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import javax.servlet.ServletRequest;
+
+
+/**
+ * Provides actual CAS authentication by delegation to an
+ * <code>AuthenticationManager</code>.
+ * 
+ * <P>
+ * Do not use this class directly. Instead configure CAS to use the {@link
+ * CasPasswordHandlerProxy}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public final class CasPasswordHandler implements InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(CasPasswordHandler.class);
+
+    //~ Instance fields ========================================================
+
+    private AuthenticationManager authenticationManager;
+
+    //~ Methods ================================================================
+
+    public void setAuthenticationManager(
+        AuthenticationManager authenticationManager) {
+        this.authenticationManager = authenticationManager;
+    }
+
+    public AuthenticationManager getAuthenticationManager() {
+        return authenticationManager;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (this.authenticationManager == null) {
+            throw new IllegalArgumentException(
+                "An AuthenticationManager is required");
+        }
+    }
+
+    /**
+     * Called by <code>CasPasswordHandlerProxy</code> for individual
+     * authentication requests.
+     * 
+     * <P>
+     * Delegates to the configured <code>AuthenticationManager</code>.
+     * </p>
+     *
+     * @param servletRequest as provided by CAS
+     * @param username provided to CAS
+     * @param password provided to CAS
+     *
+     * @return whether authentication was successful or not
+     */
+    public boolean authenticate(ServletRequest servletRequest, String username,
+        String password) {
+        if ((username == null) || "".equals(username)) {
+            return false;
+        }
+
+        if (password == null) {
+            password = "";
+        }
+
+        Authentication request = new UsernamePasswordAuthenticationToken(username
+                .toString(), password.toString());
+        Authentication response = null;
+
+        try {
+            response = authenticationManager.authenticate(request);
+        } catch (AuthenticationException failed) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Authentication request for user: " + username
+                    + " failed: " + failed.toString());
+            }
+
+            return false;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Authentication request for user: " + username
+                + " successful");
+        }
+
+        return true;
+    }
+}

+ 142 - 0
adapters/cas/src/main/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxy.java

@@ -0,0 +1,142 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.adapters.cas;
+
+import edu.yale.its.tp.cas.auth.PasswordHandler;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.context.ApplicationContext;
+
+import org.springframework.web.context.support.WebApplicationContextUtils;
+
+import java.util.Map;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * Enables CAS to use the Acegi Security System for authentication.
+ * 
+ * <P>
+ * This class works along with {@link CasPasswordHandler} to enable users to
+ * easily migrate from stand-alone Acegi Security System deployments to
+ * enterprise-wide CAS deployments.
+ * </p>
+ * 
+ * <p>
+ * It should be noted that the Acegi Security System will operate as a CAS
+ * client irrespective of the <code>PasswordHandler</code> used on the CAS
+ * server. In other words, this class need <B>not</B> be used on the CAS
+ * server if not desired. It exists solely for the convenience of users
+ * wishing have CAS delegate to an Acegi Security System-based
+ * <code>AuthenticationManager</code>.
+ * </p>
+ * 
+ * <p>
+ * This class works requires a properly configured
+ * <code>CasPasswordHandler</code>. On the first authentication request, the
+ * class will use Spring's {@link
+ * WebApplicationContextUtils#getWebApplicationContext(ServletContext sc)}
+ * method to obtain an <code>ApplicationContext</code> instance, inside which
+ * must be a configured <code>CasPasswordHandler</code> instance. The
+ * <code>CasPasswordHandlerProxy</code> will then delegate authentication
+ * requests to that instance.
+ * </p>
+ * 
+ * <p>
+ * To configure CAS to use this class, edit CAS' <code>web.xml</code> and
+ * define the <code>edu.yale.its.tp.cas.authHandler</code> context parameter
+ * with the value
+ * <code>net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasPasswordHandlerProxy implements PasswordHandler {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(CasPasswordHandlerProxy.class);
+
+    //~ Instance fields ========================================================
+
+    private ApplicationContext ctx;
+    private CasPasswordHandler handler;
+
+    //~ Methods ================================================================
+
+    /**
+     * Called by CAS when authentication is required.
+     * 
+     * <P>
+     * Delegates to the <code>CasPasswordHandler</code>.
+     * </p>
+     *
+     * @param request as provided by CAS
+     * @param username provided to CAS
+     * @param password provided to CAS
+     *
+     * @return whether authentication was successful or not
+     *
+     * @throws IllegalArgumentException if the application context does not
+     *         contain a <code>CasPasswordHandler</code> or the
+     *         <code>ServletRequest</code> was not of type
+     *         <code>HttpServletRequest</code>
+     */
+    public boolean authenticate(ServletRequest request, String username,
+        String password) {
+        if (ctx == null) {
+            if (!(request instanceof HttpServletRequest)) {
+                throw new IllegalArgumentException(
+                    "Can only process HttpServletRequest");
+            }
+
+            HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+            ctx = this.getContext(httpRequest);
+        }
+
+        if (handler == null) {
+            Map beans = ctx.getBeansOfType(CasPasswordHandler.class, true, true);
+
+            if (beans.size() == 0) {
+                throw new IllegalArgumentException(
+                    "Bean context must contain at least one bean of type CasPasswordHandler");
+            }
+
+            String beanName = (String) beans.keySet().iterator().next();
+            handler = (CasPasswordHandler) beans.get(beanName);
+        }
+
+        return handler.authenticate(request, username, password);
+    }
+
+    /**
+     * Allows test cases to override where application context obtained from.
+     *
+     * @param httpRequest which can be used to find the
+     *        <code>ServletContext</code>
+     *
+     * @return the Spring application context
+     */
+    protected ApplicationContext getContext(HttpServletRequest httpRequest) {
+        return WebApplicationContextUtils.getRequiredWebApplicationContext(httpRequest.getSession()
+                                                                                      .getServletContext());
+    }
+}

+ 7 - 0
adapters/cas/src/main/java/org/acegisecurity/adapters/cas/package.html

@@ -0,0 +1,7 @@
+<html>
+<body>
+Adapter to Yale Central Authentication Service (CAS).
+<p>
+</body>
+</html>
+

+ 53 - 0
adapters/cas/src/main/resources/org/acegisecurity/adapters/cas/applicationContext.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+<!--
+ * Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Demonstration of the applicationContext.xml that should be placed in
+ * CAS' WEB-INF directory. Note the CasPasswordHandler bean.
+ * 
+ * $Id$
+-->
+
+<beans>
+
+	<!-- Data access object which stores authentication information -->
+	<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
+  		<property name="userMap">
+			<value>
+				marissa=koala,ROLES_IGNORED_BY_CAS
+				dianne=emu,ROLES_IGNORED_BY_CAS
+				scott=wombat,ROLES_IGNORED_BY_CAS
+				peter=opal,disabled,ROLES_IGNORED_BY_CAS
+			</value>
+		</property>
+	</bean>
+	
+	<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
+     	<property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property>
+	</bean>
+
+	<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
+		<property name="providers">
+		  <list>
+		    <ref bean="daoAuthenticationProvider"/>
+		  </list>
+		</property>
+	</bean>
+
+	<bean id="casPasswordHandler" class="net.sf.acegisecurity.adapters.cas.CasPasswordHandler">
+		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+	</bean>
+</beans>

+ 112 - 0
adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerProxyTests.java

@@ -0,0 +1,112 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.adapters.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.MockHttpServletRequest;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * Tests {@link CasPasswordHandlerProxy}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasPasswordHandlerProxyTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasPasswordHandlerProxyTests() {
+        super();
+    }
+
+    public CasPasswordHandlerProxyTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasPasswordHandlerProxyTests.class);
+    }
+
+    public void testDetectsIfHttpServletRequestNotPassed() {
+        CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy(
+                "net/sf/acegisecurity/adapters/cas/applicationContext-valid.xml");
+
+        try {
+            proxy.authenticate(null, "x", "y");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("Can only process HttpServletRequest",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingDelegate() {
+        CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy(
+                "net/sf/acegisecurity/adapters/cas/applicationContext-invalid.xml");
+
+        try {
+            proxy.authenticate(new MockHttpServletRequest("/"), "x", "y");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("Bean context must contain at least one bean of type CasPasswordHandler",
+                expected.getMessage());
+        }
+    }
+
+    public void testNormalOperation() {
+        CasPasswordHandlerProxy proxy = new MockCasPasswordHandlerProxy(
+                "net/sf/acegisecurity/adapters/cas/applicationContext-valid.xml");
+        assertTrue(proxy.authenticate(new MockHttpServletRequest("/"),
+                "marissa", "koala"));
+        assertFalse(proxy.authenticate(new MockHttpServletRequest("/"),
+                "marissa", "WRONG_PASSWORD"));
+        assertFalse(proxy.authenticate(new MockHttpServletRequest("/"),
+                "INVALID_USER_NAME", "WRONG_PASSWORD"));
+    }
+
+    //~ Inner Classes ==========================================================
+
+    /**
+     * Mock object so that application context source can be specified.
+     */
+    private class MockCasPasswordHandlerProxy extends CasPasswordHandlerProxy {
+        private ApplicationContext ctx;
+
+        public MockCasPasswordHandlerProxy(String appContextLocation) {
+            ctx = new ClassPathXmlApplicationContext(appContextLocation);
+        }
+
+        private MockCasPasswordHandlerProxy() {
+            super();
+        }
+
+        protected ApplicationContext getContext(HttpServletRequest httpRequest) {
+            return ctx;
+        }
+    }
+}

+ 107 - 0
adapters/cas/src/test/java/org/acegisecurity/adapters/cas/CasPasswordHandlerTests.java

@@ -0,0 +1,107 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.adapters.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.MockAuthenticationManager;
+import net.sf.acegisecurity.MockHttpServletRequest;
+
+
+/**
+ * Tests {@link CasPasswordHandler}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasPasswordHandlerTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasPasswordHandlerTests() {
+        super();
+    }
+
+    public CasPasswordHandlerTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasPasswordHandlerTests.class);
+    }
+
+    public void testDeniesAccessWhenAuthenticationManagerThrowsException()
+        throws Exception {
+        CasPasswordHandler handler = new CasPasswordHandler();
+        handler.setAuthenticationManager(new MockAuthenticationManager(false));
+        handler.afterPropertiesSet();
+
+        assertFalse(handler.authenticate(new MockHttpServletRequest("/"),
+                "username", "password"));
+    }
+
+    public void testDetectsEmptyAuthenticationManager()
+        throws Exception {
+        CasPasswordHandler handler = new CasPasswordHandler();
+
+        try {
+            handler.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("An AuthenticationManager is required",
+                expected.getMessage());
+        }
+    }
+
+    public void testGettersSetters() {
+        CasPasswordHandler handler = new CasPasswordHandler();
+        handler.setAuthenticationManager(new MockAuthenticationManager(false));
+        assertTrue(handler.getAuthenticationManager() != null);
+    }
+
+    public void testGracefullyHandlesEmptyUsernamesAndPassword()
+        throws Exception {
+        CasPasswordHandler handler = new CasPasswordHandler();
+        handler.setAuthenticationManager(new MockAuthenticationManager(true));
+        handler.afterPropertiesSet();
+
+        // If empty or null username we return false
+        assertFalse(handler.authenticate(new MockHttpServletRequest("/"), "",
+                "password"));
+        assertFalse(handler.authenticate(new MockHttpServletRequest("/"), null,
+                "password"));
+
+        // We authenticate with null passwords (they might not have one)
+        assertTrue(handler.authenticate(new MockHttpServletRequest("/"),
+                "user", null));
+        assertTrue(handler.authenticate(new MockHttpServletRequest("/"),
+                "user", ""));
+    }
+
+    public void testNormalOperation() throws Exception {
+        CasPasswordHandler handler = new CasPasswordHandler();
+        handler.setAuthenticationManager(new MockAuthenticationManager(true));
+        handler.afterPropertiesSet();
+
+        assertTrue(handler.authenticate(new MockHttpServletRequest("/"),
+                "username", "password"));
+    }
+}

+ 47 - 0
adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-invalid.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+<!--
+ * Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * $Id$
+-->
+
+<beans>
+
+	<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
+  		<property name="userMap">
+			<value>
+				marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR
+				dianne=emu,ROLE_TELLER
+				scott=wombat,ROLE_TELLER
+				peter=opal,disabled,ROLE_TELLER
+			</value>
+		</property>
+	</bean>
+	
+	<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
+     	<property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property>
+ 	</bean>
+
+	<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
+		<property name="providers">
+		  <list>
+		    <ref bean="daoAuthenticationProvider"/>
+		  </list>
+		</property>
+	</bean>
+
+	<!-- No CasPasswordHandler -->
+</beans>

+ 49 - 0
adapters/cas/src/test/java/org/acegisecurity/adapters/cas/applicationContext-valid.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+<!--
+ * Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * $Id$
+-->
+
+<beans>
+
+	<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
+  		<property name="userMap">
+			<value>
+				marissa=koala,ROLE_TELLER,ROLE_SUPERVISOR
+				dianne=emu,ROLE_TELLER
+				scott=wombat,ROLE_TELLER
+				peter=opal,disabled,ROLE_TELLER
+			</value>
+		</property>
+	</bean>
+	
+	<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
+     	<property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property>
+	</bean>
+
+	<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
+		<property name="providers">
+		  <list>
+		    <ref bean="daoAuthenticationProvider"/>
+		  </list>
+		</property>
+	</bean>
+
+	<bean id="casPasswordHandler" class="net.sf.acegisecurity.adapters.cas.CasPasswordHandler">
+		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+	</bean>
+</beans>

+ 220 - 0
core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationProvider.java

@@ -0,0 +1,220 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.providers.AuthenticationProvider;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.ui.cas.CasProcessingFilter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+
+/**
+ * An {@link AuthenticationProvider} implementation that integrates with Yale
+ * Central Authentication Service (CAS).
+ * 
+ * <p>
+ * This <code>AuthenticationProvider</code> is capable of validating  {@link
+ * UsernamePasswordAuthenticationToken} requests which contain a
+ * <code>principal</code> name equal to either {@link
+ * CasProcessingFilter#CAS_STATEFUL_IDENTIFIER} or {@link
+ * CasProcessingFilter#CAS_STATELESS_IDENTIFIER}. It can also validate a
+ * previously created {@link CasAuthenticationToken}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasAuthenticationProvider implements AuthenticationProvider,
+    InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
+
+    //~ Instance fields ========================================================
+
+    private CasAuthoritiesPopulator casAuthoritiesPopulator;
+    private CasProxyDecider casProxyDecider;
+    private StatelessTicketCache statelessTicketCache;
+    private String key;
+    private TicketValidator ticketValidator;
+
+    //~ Methods ================================================================
+
+    public void setCasAuthoritiesPopulator(
+        CasAuthoritiesPopulator casAuthoritiesPopulator) {
+        this.casAuthoritiesPopulator = casAuthoritiesPopulator;
+    }
+
+    public CasAuthoritiesPopulator getCasAuthoritiesPopulator() {
+        return casAuthoritiesPopulator;
+    }
+
+    public void setCasProxyDecider(CasProxyDecider casProxyDecider) {
+        this.casProxyDecider = casProxyDecider;
+    }
+
+    public CasProxyDecider getCasProxyDecider() {
+        return casProxyDecider;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setStatelessTicketCache(
+        StatelessTicketCache statelessTicketCache) {
+        this.statelessTicketCache = statelessTicketCache;
+    }
+
+    public StatelessTicketCache getStatelessTicketCache() {
+        return statelessTicketCache;
+    }
+
+    public void setTicketValidator(TicketValidator ticketValidator) {
+        this.ticketValidator = ticketValidator;
+    }
+
+    public TicketValidator getTicketValidator() {
+        return ticketValidator;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (this.casAuthoritiesPopulator == null) {
+            throw new IllegalArgumentException(
+                "A casAuthoritiesPopulator must be set");
+        }
+
+        if (this.ticketValidator == null) {
+            throw new IllegalArgumentException("A ticketValidator must be set");
+        }
+
+        if (this.casProxyDecider == null) {
+            throw new IllegalArgumentException("A casProxyDecider must be set");
+        }
+
+        if (this.statelessTicketCache == null) {
+            throw new IllegalArgumentException(
+                "A statelessTicketCache must be set");
+        }
+
+        if (key == null) {
+            throw new IllegalArgumentException(
+                "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
+        }
+    }
+
+    public Authentication authenticate(Authentication authentication)
+        throws AuthenticationException {
+        if (!supports(authentication.getClass())) {
+            return null;
+        }
+
+        if (authentication instanceof UsernamePasswordAuthenticationToken
+            && (!CasProcessingFilter.CAS_STATEFUL_IDENTIFIER.equals(
+                authentication.getPrincipal().toString())
+            && !CasProcessingFilter.CAS_STATELESS_IDENTIFIER.equals(
+                authentication.getPrincipal().toString()))) {
+            // UsernamePasswordAuthenticationToken not CAS related
+            return null;
+        }
+
+        // If an existing CasAuthenticationToken, just check we created it
+        if (authentication instanceof CasAuthenticationToken) {
+            if (this.key.hashCode() == ((CasAuthenticationToken) authentication)
+                .getKeyHash()) {
+                return authentication;
+            } else {
+                throw new BadCredentialsException(
+                    "The presented CasAuthenticationToken does not contain the expected key");
+            }
+        }
+
+        // Ensure credentials are presented
+        if (authentication.getCredentials() == null || "".equals(authentication.getCredentials())) {
+            throw new BadCredentialsException(
+                "Failed to provide a CAS service ticket to validate");
+        }
+
+        boolean stateless = false;
+
+        if (authentication instanceof UsernamePasswordAuthenticationToken
+            && CasProcessingFilter.CAS_STATELESS_IDENTIFIER.equals(
+                authentication.getPrincipal())) {
+            stateless = true;
+        }
+
+        CasAuthenticationToken result = null;
+
+        if (stateless) {
+            // Try to obtain from cache
+            result = statelessTicketCache.getByTicketId(authentication.getCredentials()
+                                                                      .toString());
+        }
+
+        if (result == null) {
+            result = this.authenticateNow(authentication);
+        }
+
+        if (stateless) {
+            // Add to cache
+            statelessTicketCache.putTicketInCache(result);
+        }
+
+        return result;
+    }
+
+    public boolean supports(Class authentication) {
+        if (UsernamePasswordAuthenticationToken.class.isAssignableFrom(
+                authentication)) {
+            return true;
+        } else if (CasAuthenticationToken.class.isAssignableFrom(authentication)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private CasAuthenticationToken authenticateNow(
+        Authentication authentication) throws AuthenticationException {
+        // Validate
+        TicketResponse response = ticketValidator.confirmTicketValid(authentication.getCredentials()
+                                                                                   .toString());
+
+        // Check proxy list is trusted
+        this.casProxyDecider.confirmProxyListTrusted(response.getProxyList());
+
+        // Build list of granted authorities
+        GrantedAuthority[] ga = this.casAuthoritiesPopulator.getAuthorities(response
+                .getUser());
+
+        // Construct CasAuthenticationToken
+        return new CasAuthenticationToken(this.key, response.getUser(),
+            authentication.getCredentials(), ga, response.getProxyList(),
+            response.getProxyGrantingTicketIou());
+    }
+}

+ 172 - 0
core/src/main/java/org/acegisecurity/providers/cas/CasAuthenticationToken.java

@@ -0,0 +1,172 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.providers.AbstractAuthenticationToken;
+
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * Represents a successful CAS <code>Authentication</code>.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
+    //~ Instance fields ========================================================
+
+    private List proxyList;
+    private Object credentials;
+    private Object principal;
+    private String proxyGrantingTicketIou;
+    private GrantedAuthority[] authorities;
+    private int keyHash;
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructor.
+     *
+     * @param key to identify if this object made by a given {@link
+     *        CasAuthenticationProvider}
+     * @param principal the username from CAS (cannot be <code>null</code>)
+     * @param credentials the service/proxy ticket ID from CAS (cannot be
+     *        <code>null</code>)
+     * @param authorities the authorities granted to the user (from {@link
+     *        CasAuthoritiesPopulator}) (cannot be <code>null</code>)
+     * @param proxyList the list of proxies from CAS (cannot be
+     *        <code>null</code>)
+     * @param proxyGrantingTicketIou the PGT-IOU ID from CAS (cannot be
+     *        <code>null</code>)
+     *
+     * @throws IllegalArgumentException if a <code>null</code> was passed
+     */
+    public CasAuthenticationToken(String key, Object principal,
+        Object credentials, GrantedAuthority[] authorities, List proxyList,
+        String proxyGrantingTicketIou) {
+        if ((key == null) || ("".equals(key)) || (principal == null)
+            || "".equals(principal) || (credentials == null)
+            || "".equals(credentials) || (authorities == null)
+            || (proxyList == null) || (proxyGrantingTicketIou == null)
+            || ("".equals(proxyGrantingTicketIou))) {
+            throw new IllegalArgumentException(
+                "Cannot pass null or empty values to constructor");
+        }
+
+        for (int i = 0; i < authorities.length; i++) {
+            if (authorities[i] == null) {
+                throw new IllegalArgumentException("Granted authority element "
+                    + i
+                    + " is null - GrantedAuthority[] cannot contain any null elements");
+            }
+        }
+
+        this.keyHash = key.hashCode();
+        this.principal = principal;
+        this.credentials = credentials;
+        this.authorities = authorities;
+        this.proxyList = proxyList;
+        this.proxyGrantingTicketIou = proxyGrantingTicketIou;
+    }
+
+    protected CasAuthenticationToken() {
+        throw new IllegalArgumentException("Cannot use default constructor");
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Ignored (always <code>true</code>).
+     *
+     * @param isAuthenticated ignored
+     */
+    public void setAuthenticated(boolean isAuthenticated) {
+        // ignored
+    }
+
+    /**
+     * Always returns <code>true</code>.
+     *
+     * @return true
+     */
+    public boolean isAuthenticated() {
+        return true;
+    }
+
+    public GrantedAuthority[] getAuthorities() {
+        return this.authorities;
+    }
+
+    public Object getCredentials() {
+        return this.credentials;
+    }
+
+    public int getKeyHash() {
+        return this.keyHash;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public String getProxyGrantingTicketIou() {
+        return proxyGrantingTicketIou;
+    }
+
+    public List getProxyList() {
+        return proxyList;
+    }
+
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+
+        if (obj instanceof CasAuthenticationToken) {
+            CasAuthenticationToken test = (CasAuthenticationToken) obj;
+
+            // proxyGrantingTicketIou is never null due to constructor
+            if (!this.getProxyGrantingTicketIou().equals(test
+                    .getProxyGrantingTicketIou())) {
+                return false;
+            }
+
+            // proxyList is never null due to constructor
+            if (!this.getProxyList().equals(test.getProxyList())) {
+                return false;
+            }
+
+            return true;
+        }
+
+        System.out.println("THey're not equal");
+
+        return false;
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append(super.toString());
+        sb.append("; Credentials (Service/Proxy Ticket): " + this.credentials);
+        sb.append("; Proxy-Granting Ticket IOU: " + this.proxyGrantingTicketIou);
+        sb.append("; Proxy List: " + this.proxyList.toString());
+
+        return sb.toString();
+    }
+}

+ 59 - 0
core/src/main/java/org/acegisecurity/providers/cas/CasAuthoritiesPopulator.java

@@ -0,0 +1,59 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.GrantedAuthority;
+
+
+/**
+ * Populates the <code>GrantedAuthority[]</code> objects for a CAS
+ * authenticated user.
+ * 
+ * <P>
+ * CAS does not provide the authorities (roles) granted to a user. It merely
+ * authenticates their identity. As the Acegi Security System for Spring needs
+ * to know the authorities granted to a user in order to construct a valid
+ * <code>Authentication</code> object, implementations of this interface will
+ * provide this information.
+ * </p>
+ * 
+ * <P>
+ * Implementations should not perform any caching. They will only be called
+ * when a refresh is required.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface CasAuthoritiesPopulator {
+    //~ Methods ================================================================
+
+    /**
+     * Obtains the granted authorities for the specified user.
+     * 
+     * <P>
+     * May throw any <code>AuthenticationException</code> or return
+     * <code>null</code> if the authorities are unavailable.
+     * </p>
+     *
+     * @param casUserId as obtained from the CAS validation service
+     *
+     * @return the granted authorities for the indicated user
+     */
+    public GrantedAuthority[] getAuthorities(String casUserId)
+        throws AuthenticationException;
+}

+ 72 - 0
core/src/main/java/org/acegisecurity/providers/cas/CasProxyDecider.java

@@ -0,0 +1,72 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import java.util.List;
+
+
+/**
+ * Decides whether a proxy list presented via CAS is trusted or not.
+ * 
+ * <P>
+ * CAS 1.0 allowed services to receive a service ticket and then validate it.
+ * CAS 2.0 allows services to receive a service ticket and then validate it
+ * with a proxy callback URL. The callback will enable the CAS server to
+ * authenticate the service. In doing so the service will receive a
+ * proxy-granting ticket and a proxy-granting ticket IOU. The IOU is just an
+ * internal record that a proxy-granting ticket is due to be received via the
+ * callback URL.
+ * </p>
+ * 
+ * <P>
+ * With a proxy-granting ticket, a service can request the CAS server provides
+ * it with a proxy ticket. A proxy ticket is just a service ticket, but the
+ * CAS server internally tracks the list (chain) of services used to build the
+ * proxy ticket. The proxy ticket is then presented to the target service.
+ * </p>
+ * 
+ * <P>
+ * If this application is a target service of a proxy ticket, the
+ * <code>CasProxyDecider</code> resolves whether or not the proxy list is
+ * trusted. Applications should only trust services they allow to impersonate
+ * an end user.
+ * </p>
+ * 
+ * <P>
+ * If this application is a service that should never accept proxy-granting
+ * tickets, the implementation should reject tickets that present a proxy list
+ * with any members. If the list has no members, it indicates the CAS server
+ * directly authenticated the user (ie there are no services which proxied the
+ * user authentication).
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface CasProxyDecider {
+    //~ Methods ================================================================
+
+    /**
+     * Decides whether the proxy list is trusted.
+     * 
+     * <P>
+     * Must throw any <code>ProxyUntrustedException</code> if the proxy list is
+     * untrusted.
+     * </p>
+     */
+    public void confirmProxyListTrusted(List proxyList)
+        throws ProxyUntrustedException;
+}

+ 50 - 0
core/src/main/java/org/acegisecurity/providers/cas/ProxyUntrustedException.java

@@ -0,0 +1,50 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import net.sf.acegisecurity.AuthenticationException;
+
+
+/**
+ * Thrown if a CAS proxy ticket is presented from an untrusted proxy.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ProxyUntrustedException extends AuthenticationException {
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructs a <code>ProxyUntrustedException</code> with the specified
+     * message.
+     *
+     * @param msg the detail message.
+     */
+    public ProxyUntrustedException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Constructs a <code>ProxyUntrustedException</code> with the specified
+     * message and root cause.
+     *
+     * @param msg the detail message.
+     * @param t root cause
+     */
+    public ProxyUntrustedException(String msg, Throwable t) {
+        super(msg, t);
+    }
+}

+ 116 - 0
core/src/main/java/org/acegisecurity/providers/cas/StatelessTicketCache.java

@@ -0,0 +1,116 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+/**
+ * Caches CAS service tickets and CAS proxy tickets for stateless connections.
+ * 
+ * <p>
+ * When a service ticket or proxy ticket is validated against the CAS server,
+ * it is unable to be used again. Most types of callers  are stateful and are
+ * associated with a given <code>HttpSession</code>. This allows the
+ * affirmative CAS validation outcome to be stored in the
+ * <code>HttpSession</code>, meaning the removal of the ticket from the CAS
+ * server is not an issue issue.
+ * </p>
+ * 
+ * <P>
+ * Stateless callers, such as remoting protocols, cannot take advantage of
+ * <code>HttpSession</code>. If the stateless caller is located a significant
+ * network distance from the CAS server, acquiring a fresh service ticket or
+ * proxy ticket for each invocation would be expensive.
+ * </p>
+ * 
+ * <P>
+ * To avoid this issue with stateless callers, it is expected stateless callers
+ * will obtain a single service ticket or proxy ticket, and then present this
+ * same ticket to the Acegi Security System secured application on each
+ * occasion. As no <code>HttpSession</code> is available for such callers, the
+ * affirmative CAS validation outcome cannot be stored in this location.
+ * </p>
+ * 
+ * <P>
+ * The <code>StatelessTicketCache</code> enables the service tickets and proxy
+ * tickets belonging to stateless callers to be placed in a cache. This
+ * in-memory cache stores the <code>CasAuthenticationToken</code>, effectively
+ * providing the same capability as a <code>HttpSession</code> with the ticket
+ * identifier being the key rather than a session identifier.
+ * </p>
+ * 
+ * <P>
+ * Implementations should provide a reasonable timeout on stored entries,  such
+ * that the stateless caller are not required to unnecessarily acquire  fresh
+ * CAS service tickets or proxy tickets.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface StatelessTicketCache {
+    //~ Methods ================================================================
+
+    /**
+     * Retrieves the <code>CasAuthenticationToken</code> associated with the
+     * specified ticket.
+     * 
+     * <P>
+     * If not found, returns a
+     * <code>null</code><code>CasAuthenticationToken</code>.
+     * </p>
+     *
+     * @return the fully populated authentication token
+     */
+    public CasAuthenticationToken getByTicketId(String serviceTicket);
+
+    /**
+     * Adds the specified <code>CasAuthenticationToken</code> to the cache.
+     * 
+     * <P>
+     * The {@link CasAuthenticationToken#getCredentials()} method is used to
+     * retrieve the service ticket number.
+     * </p>
+     *
+     * @param token to be added to the cache
+     */
+    public void putTicketInCache(CasAuthenticationToken token);
+
+    /**
+     * Removes the specified ticket from the cache, as per  {@link
+     * #removeTicketFromCache(String)}.
+     * 
+     * <P>
+     * Implementations should use {@link
+     * CasAuthenticationToken#getCredentials()} to obtain the ticket and then
+     * delegate to to the  {@link #removeTicketFromCache(String)} method.
+     * </p>
+     *
+     * @param token to be removed
+     */
+    public void removeTicketFromCache(CasAuthenticationToken token);
+
+    /**
+     * Removes the specified ticket from the cache, meaning that future calls
+     * will require a new service ticket.
+     * 
+     * <P>
+     * This is in case applications wish to provide a session termination
+     * capability for their stateless clients.
+     * </p>
+     *
+     * @param serviceTicket to be removed
+     */
+    public void removeTicketFromCache(String serviceTicket);
+}

+ 102 - 0
core/src/main/java/org/acegisecurity/providers/cas/TicketResponse.java

@@ -0,0 +1,102 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Represents a CAS service ticket in native CAS form.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class TicketResponse {
+    //~ Instance fields ========================================================
+
+    private List proxyList;
+    private String proxyGrantingTicketIou;
+    private String user;
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructor.
+     * 
+     * <P>
+     * If <code>null</code> is passed into the <code>proxyList</code> or
+     * <code>proxyGrantingTicketIou</code>, suitable defaults are established.
+     * However, <code>null</code> cannot be passed for the <code>user</code>
+     * argument.
+     * </p>
+     *
+     * @param user the user as indicated by CAS (cannot be <code>null</code> or
+     *        an empty <code>String</code>)
+     * @param proxyList as provided by CAS (may be <code>null</code>)
+     * @param proxyGrantingTicketIou as provided by CAS (may be
+     *        <code>null</code>)
+     *
+     * @throws IllegalArgumentException DOCUMENT ME!
+     */
+    public TicketResponse(String user, List proxyList,
+        String proxyGrantingTicketIou) {
+        if (proxyList == null) {
+            proxyList = new Vector();
+        }
+
+        if (proxyGrantingTicketIou == null) {
+            proxyGrantingTicketIou = "";
+        }
+
+        if ((user == null) || "".equals(user)) {
+            throw new IllegalArgumentException(
+                "Cannot pass null or empty String for User");
+        }
+
+        this.user = user;
+        this.proxyList = proxyList;
+        this.proxyGrantingTicketIou = proxyGrantingTicketIou;
+    }
+
+    protected TicketResponse() {
+        throw new IllegalArgumentException("Cannot use default constructor");
+    }
+
+    //~ Methods ================================================================
+
+    public String getProxyGrantingTicketIou() {
+        return proxyGrantingTicketIou;
+    }
+
+    public List getProxyList() {
+        return proxyList;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        sb.append(super.toString());
+        sb.append(": User: " + this.user);
+        sb.append("; Proxy-Granting Ticket IOU: " + this.proxyGrantingTicketIou);
+        sb.append("; Proxy List: " + this.proxyList.toString());
+
+        return sb.toString();
+    }
+}

+ 53 - 0
core/src/main/java/org/acegisecurity/providers/cas/TicketValidator.java

@@ -0,0 +1,53 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import net.sf.acegisecurity.AuthenticationException;
+
+
+/**
+ * Validates a CAS service ticket.
+ * 
+ * <P>
+ * Implementations must accept CAS proxy tickets, in addition to CAS service
+ * tickets. If proxy tickets should be rejected, this is resolved by a {@link
+ * CasProxyDecider} implementation (not by the <code>TicketValidator</code>).
+ * </p>
+ * 
+ * <P>
+ * Implementations may request a proxy granting ticket if wish,  although this
+ * behaviour is not mandatory.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface TicketValidator {
+    //~ Methods ================================================================
+
+    /**
+     * Returns information about the ticket, if it is valid for this service.
+     * 
+     * <P>
+     * Must throw an <code>AuthenticationException</code> if the ticket is not
+     * valid for this service.
+     * </p>
+     *
+     * @return details of the CAS service ticket
+     */
+    public TicketResponse confirmTicketValid(String serviceTicket)
+        throws AuthenticationException;
+}

+ 108 - 0
core/src/main/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCache.java

@@ -0,0 +1,108 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.cache;
+
+import net.sf.acegisecurity.providers.cas.CasAuthenticationToken;
+import net.sf.acegisecurity.providers.cas.StatelessTicketCache;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.dao.DataRetrievalFailureException;
+
+
+/**
+ * Caches tickets using  <A HREF="http://ehcache.sourceforge.net">EHCACHE</a>.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class EhCacheBasedTicketCache implements StatelessTicketCache,
+    InitializingBean {
+    //~ Instance fields ========================================================
+
+    private Cache cache;
+    private CacheManager manager;
+    private int minutesToIdle = 20;
+
+    //~ Methods ================================================================
+
+    public CasAuthenticationToken getByTicketId(String serviceTicket) {
+        Element element = null;
+
+        try {
+            element = cache.get(serviceTicket);
+        } catch (CacheException cacheException) {
+            throw new DataRetrievalFailureException("Cache failure: "
+                + cacheException.getMessage());
+        }
+
+        if (element == null) {
+            System.out.println("not found");
+
+            return null;
+        } else {
+            System.out.println("found");
+
+            return (CasAuthenticationToken) element.getValue();
+        }
+    }
+
+    public void setMinutesToIdle(int minutesToIdle) {
+        this.minutesToIdle = minutesToIdle;
+    }
+
+    /**
+     * Specifies how many minutes an entry will remain in the cache from  when
+     * it was last accessed. This is effectively the session duration.
+     * 
+     * <P>
+     * Defaults to 20 minutes.
+     * </p>
+     *
+     * @return Returns the minutes an element remains in the cache
+     */
+    public int getMinutesToIdle() {
+        return minutesToIdle;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        manager = CacheManager.create();
+
+        // Cache name, max memory, overflowToDisk, eternal, timeToLive, timeToIdle
+        cache = new Cache("ehCacheBasedTicketCache", Integer.MAX_VALUE, false,
+                false, minutesToIdle * 60, minutesToIdle * 60);
+        manager.addCache(cache);
+    }
+
+    public void putTicketInCache(CasAuthenticationToken token) {
+        Element element = new Element(token.getCredentials().toString(), token);
+        System.out.println("Adding " + element.getKey());
+        cache.put(element);
+    }
+
+    public void removeTicketFromCache(CasAuthenticationToken token) {
+        this.removeTicketFromCache(token.getCredentials().toString());
+    }
+
+    public void removeTicketFromCache(String serviceTicket) {
+        cache.remove(serviceTicket);
+    }
+}

+ 6 - 0
core/src/main/java/org/acegisecurity/providers/cas/package.html

@@ -0,0 +1,6 @@
+<html>
+<body>
+An authentication provider that can process Yale Central Authentication Service (CAS)
+service tickets and proxy tickets.
+</body>
+</html>

+ 67 - 0
core/src/main/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulator.java

@@ -0,0 +1,67 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.populator;
+
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.providers.cas.CasAuthoritiesPopulator;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+
+import org.springframework.beans.factory.InitializingBean;
+
+
+/**
+ * Populates the CAS authorities via an {@link AuthenticationDao}.
+ * 
+ * <P>
+ * The additional information (username, password, enabled status etc)  an
+ * <code>AuthenticationDao</code> implementation provides about  a
+ * <code>User</code> is ignored. Only the <code>GrantedAuthority</code>s are
+ * relevant to this class.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class DaoCasAuthoritiesPopulator implements CasAuthoritiesPopulator,
+    InitializingBean {
+    //~ Instance fields ========================================================
+
+    private AuthenticationDao authenticationDao;
+
+    //~ Methods ================================================================
+
+    public void setAuthenticationDao(AuthenticationDao authenticationDao) {
+        this.authenticationDao = authenticationDao;
+    }
+
+    public AuthenticationDao getAuthenticationDao() {
+        return authenticationDao;
+    }
+
+    public GrantedAuthority[] getAuthorities(String casUserId)
+        throws AuthenticationException {
+        return this.authenticationDao.loadUserByUsername(casUserId)
+                                     .getAuthorities();
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (this.authenticationDao == null) {
+            throw new IllegalArgumentException(
+                "An authenticationDao must be set");
+        }
+    }
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/providers/cas/populator/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+Implementations that populate GrantedAuthority[]s of CAS authentications.
+</body>
+</html>

+ 55 - 0
core/src/main/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxy.java

@@ -0,0 +1,55 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import net.sf.acegisecurity.providers.cas.CasProxyDecider;
+import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.List;
+
+
+/**
+ * Accepts a proxied request from any other service.
+ * 
+ * <P>
+ * Also accepts the request if there was no proxy (ie the user directly
+ * authenticated against this service).
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AcceptAnyCasProxy implements CasProxyDecider {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(AcceptAnyCasProxy.class);
+
+    //~ Methods ================================================================
+
+    public void confirmProxyListTrusted(List proxyList)
+        throws ProxyUntrustedException {
+        if (proxyList == null) {
+            throw new IllegalArgumentException("proxyList cannot be null");
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Always accepting proxy list: " + proxyList.toString());
+        }
+    }
+}

+ 87 - 0
core/src/main/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDecider.java

@@ -0,0 +1,87 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import net.sf.acegisecurity.providers.cas.CasProxyDecider;
+import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.List;
+
+
+/**
+ * Accepts proxied requests if the closest proxy is named in the
+ * <code>validProxies</code> list.
+ * 
+ * <P>
+ * Also accepts the request if there was no proxy (ie the user directly
+ * authenticated against this service).
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NamedCasProxyDecider implements CasProxyDecider, InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(NamedCasProxyDecider.class);
+
+    //~ Instance fields ========================================================
+
+    private List validProxies;
+
+    //~ Methods ================================================================
+
+    public void setValidProxies(List validProxies) {
+        this.validProxies = validProxies;
+    }
+
+    public List getValidProxies() {
+        return validProxies;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (this.validProxies == null) {
+            throw new IllegalArgumentException(
+                "A validProxies list must be set");
+        }
+    }
+
+    public void confirmProxyListTrusted(List proxyList)
+        throws ProxyUntrustedException {
+        if (proxyList == null) {
+            throw new IllegalArgumentException("proxyList cannot be null");
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Proxy list: " + proxyList.toString());
+        }
+
+        if (proxyList.size() == 0) {
+            // A Service Ticket (not a Proxy Ticket)
+            return;
+        }
+
+        if (!validProxies.contains(proxyList.get(0))) {
+            throw new ProxyUntrustedException("Nearest proxy '"
+                + proxyList.get(0) + "' is untrusted");
+        }
+    }
+}

+ 63 - 0
core/src/main/java/org/acegisecurity/providers/cas/proxy/RejectProxyTickets.java

@@ -0,0 +1,63 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import net.sf.acegisecurity.providers.cas.CasProxyDecider;
+import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.List;
+
+
+/**
+ * Accepts no proxied requests.
+ * 
+ * <P>
+ * This class should be used if only service tickets wish to be accepted (ie no
+ * proxy tickets at all).
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RejectProxyTickets implements CasProxyDecider {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(RejectProxyTickets.class);
+
+    //~ Methods ================================================================
+
+    public void confirmProxyListTrusted(List proxyList)
+        throws ProxyUntrustedException {
+        if (proxyList == null) {
+            throw new IllegalArgumentException("proxyList cannot be null");
+        }
+
+        if (proxyList.size() == 0) {
+            // A Service Ticket (not a Proxy Ticket)
+            return;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Proxies are unacceptable; proxy list provided: "
+                + proxyList.toString());
+        }
+
+        throw new ProxyUntrustedException("Proxy tickets are rejected");
+    }
+}

+ 6 - 0
core/src/main/java/org/acegisecurity/providers/cas/proxy/package.html

@@ -0,0 +1,6 @@
+<html>
+<body>
+Implementations that decide whether proxy lists of
+CAS authentications are trusted.
+</body>
+</html>

+ 96 - 0
core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidator.java

@@ -0,0 +1,96 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.ticketvalidator;
+
+import net.sf.acegisecurity.providers.cas.TicketValidator;
+import net.sf.acegisecurity.ui.cas.ServiceProperties;
+
+import org.springframework.beans.factory.InitializingBean;
+
+
+/**
+ * Convenience abstract base for <code>TicketValidator</code>s.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public abstract class AbstractTicketValidator implements TicketValidator,
+    InitializingBean {
+    //~ Instance fields ========================================================
+
+    private ServiceProperties serviceProperties;
+    private String casValidate;
+    private String trustStore;
+
+    //~ Methods ================================================================
+
+    public void setCasValidate(String casValidate) {
+        this.casValidate = casValidate;
+    }
+
+    /**
+     * Mandatory URL to CAS' proxy ticket valiation service.
+     * 
+     * <P>
+     * This is usually something like
+     * <code>https://www.mycompany.com/cas/proxyValidate</code>.
+     * </p>
+     *
+     * @return the CAS proxy ticket validation URL
+     */
+    public String getCasValidate() {
+        return casValidate;
+    }
+
+    public void setServiceProperties(ServiceProperties serviceProperties) {
+        this.serviceProperties = serviceProperties;
+    }
+
+    public ServiceProperties getServiceProperties() {
+        return serviceProperties;
+    }
+
+    public void setTrustStore(String trustStore) {
+        this.trustStore = trustStore;
+    }
+
+    /**
+     * Optional property which will be used to set the system property
+     * <code>javax.net.ssl.trustStore</code>.
+     *
+     * @return the <code>javax.net.ssl.trustStore</code> that will be set
+     *         during bean initialization, or <code>null</code> to leave the
+     *         system property unchanged
+     */
+    public String getTrustStore() {
+        return trustStore;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if ((this.casValidate == null) || "".equals(casValidate)) {
+            throw new IllegalArgumentException("A casValidate URL must be set");
+        }
+
+        if (serviceProperties == null) {
+            throw new IllegalArgumentException(
+                "serviceProperties must be specified");
+        }
+
+        if ((trustStore != null) && (!"".equals(trustStore))) {
+            System.setProperty("javax.net.ssl.trustStore", trustStore);
+        }
+    }
+}

+ 128 - 0
core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidator.java

@@ -0,0 +1,128 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.ticketvalidator;
+
+import edu.yale.its.tp.cas.client.ProxyTicketValidator;
+
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationServiceException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.providers.cas.TicketResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * Uses CAS' <code>ProxyTicketValidator</code> to validate a service ticket.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProxyTicketValidator extends AbstractTicketValidator {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(CasProxyTicketValidator.class);
+
+    //~ Instance fields ========================================================
+
+    private String proxyCallbackUrl;
+
+    //~ Methods ================================================================
+
+    public void setProxyCallbackUrl(String proxyCallbackUrl) {
+        this.proxyCallbackUrl = proxyCallbackUrl;
+    }
+
+    /**
+     * Optional callback URL to obtain a proxy-granting ticket from CAS.
+     * 
+     * <P>
+     * This callback URL belongs to the Acegi Security System for Spring
+     * secured application. We suggest you use CAS'
+     * <code>ProxyTicketReceptor</code> servlet to receive this callback and
+     * manage the proxy-granting ticket list. The callback URL is usually
+     * something like
+     * <code>https://www.mycompany.com/application/casProxy/receptor</code>.
+     * </p>
+     * 
+     * <P>
+     * If left <code>null</code>, the <code>CasAuthenticationToken</code> will
+     * not have a proxy granting ticket IOU and there will be no
+     * proxy-granting ticket callback. Accordingly, the Acegi Securty System
+     * for Spring secured application will be unable to obtain a proxy ticket
+     * to call another CAS-secured service on behalf of the user. This is not
+     * really an issue for most applications.
+     * </p>
+     *
+     * @return the proxy callback URL, or <code>null</code> if not used
+     */
+    public String getProxyCallbackUrl() {
+        return proxyCallbackUrl;
+    }
+
+    public TicketResponse confirmTicketValid(String serviceTicket)
+        throws AuthenticationException {
+        // Attempt to validate presented ticket using CAS' ProxyTicketValidator class
+        ProxyTicketValidator pv = new ProxyTicketValidator();
+
+        pv.setCasValidateUrl(super.getCasValidate());
+        pv.setServiceTicket(serviceTicket);
+        pv.setService(super.getServiceProperties().getService());
+
+        if (super.getServiceProperties().isSendRenew()) {
+            logger.warn(
+                "The current CAS ProxyTicketValidator does not support the 'renew' property. The ticket cannot be validated as having been issued by a 'renew' authentication. It is expected this will be corrected in a future version of CAS' ProxyTicketValidator.");
+        }
+
+        if ((this.proxyCallbackUrl != null)
+            && (!"".equals(this.proxyCallbackUrl))) {
+            pv.setProxyCallbackUrl(proxyCallbackUrl);
+        }
+
+        return validateNow(pv);
+    }
+
+    /**
+     * Perform the actual remote invocation. Protected to enable replacement
+     * during tests.
+     *
+     * @param pv the populated <code>ProxyTicketValidator</code>
+     *
+     * @return the <code>TicketResponse</code>
+     *
+     * @throws AuthenticationServiceException
+     *         if<code>ProxyTicketValidator</code> internally fails
+     * @throws BadCredentialsException DOCUMENT ME!
+     */
+    protected TicketResponse validateNow(ProxyTicketValidator pv)
+        throws AuthenticationServiceException, BadCredentialsException {
+        try {
+            pv.validate();
+        } catch (Exception internalProxyTicketValidatorProblem) {
+            throw new AuthenticationServiceException(internalProxyTicketValidatorProblem
+                .getMessage());
+        }
+
+        if (!pv.isAuthenticationSuccesful()) {
+            throw new BadCredentialsException(pv.getErrorCode() + ": "
+                + pv.getErrorMessage());
+        }
+
+        return new TicketResponse(pv.getUser(), pv.getProxyList(),
+            pv.getPgtIou());
+    }
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/providers/cas/ticketvalidator/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+Implementations that validate service tickets.
+</body>
+</html>

+ 111 - 0
core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilter.java

@@ -0,0 +1,111 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.ui.AbstractProcessingFilter;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * Processes a CAS service ticket.
+ * 
+ * <p>
+ * A service ticket consists of an opaque ticket string. It arrives at this
+ * filter by the user's browser successfully authenticating using CAS, and
+ * then receiving a HTTP redirect to a <code>service</code>. The opal ticket
+ * string is presented in the <code>ticket</code> request parameter. This
+ * filter monitors the <code>service</code> URL so it can receive the service
+ * ticket and process it. The CAS server knows which <code>service</code> URL
+ * to use via the {@link ServiceProperties#getService()} method.
+ * </p>
+ * 
+ * <P>
+ * Processing the service ticket involves creating a
+ * <code>UsernamePasswordAuthenticationToken</code> which uses {@link
+ * #CAS_STATEFUL_IDENTIFIER} for the <code>principal</code> and the opaque
+ * ticket string as the <code>credentials</code>.
+ * </p>
+ * 
+ * <P>
+ * The configured <code>AuthenticationManager</code> is expected to provide a
+ * provider that can recognise
+ * <code>UsernamePasswordAuthenticationToken</code>s containing this special
+ * <code>principal</code> name, and process them accordingly by validation
+ * with the CAS server.
+ * </p>
+ * 
+ * <P>
+ * <B>Do not use this class directly.</B> Instead configure
+ * <code>web.xml</code> to use the {@link
+ * net.sf.acegisecurity.util.FilterToBeanProxy}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProcessingFilter extends AbstractProcessingFilter {
+    //~ Static fields/initializers =============================================
+
+    /**
+     * Used to identify a CAS request for a stateful user agent, such as a web
+     * browser.
+     */
+    public static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
+
+    /**
+     * Used to identify a CAS request for a stateless user agent, such as a
+     * remoting protocol client (eg Hessian, Burlap, SOAP etc). Results in a
+     * more aggressive caching strategy being used, as the absence of a
+     * <code>HttpSession</code> will result in a new authentication attempt on
+     * every request.
+     */
+    public static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
+
+    //~ Methods ================================================================
+
+    /**
+     * This filter by default responds to
+     * <code>/j_acegi_cas_security_check</code>.
+     *
+     * @return the default
+     */
+    public String getDefaultFilterProcessesUrl() {
+        return "/j_acegi_cas_security_check";
+    }
+
+    public Authentication attemptAuthentication(HttpServletRequest request)
+        throws AuthenticationException {
+        String username = CAS_STATEFUL_IDENTIFIER;
+        String password = request.getParameter("ticket");
+
+         if (password == null) {
+            password = "";
+        }
+
+        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
+                password);
+
+        return this.getAuthenticationManager().authenticate(authRequest);
+    }
+
+    public void init(FilterConfig filterConfig) throws ServletException {}
+}

+ 102 - 0
core/src/main/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPoint.java

@@ -0,0 +1,102 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Used by the <code>SecurityEnforcementFilter</code> to commence
+ * authentication via the Yale Central Authentication Service (CAS).
+ * 
+ * <P>
+ * The user's browser will be redirected to the Yale CAS enterprise-wide login
+ * page. This page is specified by the <code>loginUrl</code> property. Once
+ * login is complete, the CAS login page will redirect to the page indicated
+ * by the <code>service</code> property. The <code>service</code> is a HTTP
+ * URL belonging to the current application. The <code>service</code> URL is
+ * monitored by the {@link CasProcessingFilter}, which will validate the CAS
+ * login was successful.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProcessingFilterEntryPoint implements AuthenticationEntryPoint,
+    InitializingBean {
+    //~ Instance fields ========================================================
+
+    private ServiceProperties serviceProperties;
+    private String loginUrl;
+
+    //~ Methods ================================================================
+
+    public void setLoginUrl(String loginUrl) {
+        this.loginUrl = loginUrl;
+    }
+
+    /**
+     * The enterprise-wide CAS login URL. Usually something like
+     * <code>https://www.mycompany.com/cas/login</code>.
+     *
+     * @return the enterprise-wide CAS login URL
+     */
+    public String getLoginUrl() {
+        return loginUrl;
+    }
+
+    public void setServiceProperties(ServiceProperties serviceProperties) {
+        this.serviceProperties = serviceProperties;
+    }
+
+    public ServiceProperties getServiceProperties() {
+        return serviceProperties;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if ((loginUrl == null) || "".equals(loginUrl)) {
+            throw new IllegalArgumentException("loginUrl must be specified");
+        }
+
+        if (serviceProperties == null) {
+            throw new IllegalArgumentException(
+                "serviceProperties must be specified");
+        }
+    }
+
+    public void commence(ServletRequest request, ServletResponse response)
+        throws IOException, ServletException {
+        String url;
+
+        if (serviceProperties.isSendRenew()) {
+            url = loginUrl + "?renew=true" + "&service="
+                + serviceProperties.getService();
+        } else {
+            url = loginUrl + "?service=" + serviceProperties.getService();
+        }
+
+        ((HttpServletResponse) response).sendRedirect(url);
+    }
+}

+ 86 - 0
core/src/main/java/org/acegisecurity/ui/cas/ServiceProperties.java

@@ -0,0 +1,86 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import org.springframework.beans.factory.InitializingBean;
+
+
+/**
+ * Stores properties related to this CAS service.
+ * 
+ * <P>
+ * Each web application capable of processing CAS tickets is known as a
+ * service. This class stores the properties that are relevant to the local
+ * CAS service, being the application that is being secured by the Acegi
+ * Security System for Spring.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ServiceProperties implements InitializingBean {
+    //~ Instance fields ========================================================
+
+    private String service;
+
+     private boolean sendRenew = false;
+
+    //~ Methods ================================================================
+
+    public void setSendRenew(boolean sendRenew) {
+        this.sendRenew = sendRenew;
+    }
+
+    /**
+     * Indicates whether the <code>renew</code> parameter should be sent to the
+     * CAS login URL and CAS validation URL.
+     * <P> If <code>true</code>, it will
+     * force CAS to authenticate the user again (even if the user has
+     * previously authenticated). During ticket validation it will require the
+     * ticket was generated as a consequence of an explicit login. High
+     * security applications would probably set this to <code>true</code>.
+     * Defaults to <code>false</code>, providing automated single sign on.
+     *
+     * @return whether to send the <code>renew</code> parameter to CAS
+     */
+    public boolean isSendRenew() {
+        return sendRenew;
+    }
+
+    public void setService(String service) {
+        this.service = service;
+    }
+
+    /**
+      * Represents the service the user is authenticating to. 
+      * 
+      * <B>This service is the callback URL
+     * belonging to the local Acegi Security System for Spring secured
+     * application. For example,
+     * <code>https://www.mycompany.com/application/j_acegi_cas_security_check</code>
+    * 
+     * @return the URL of the service the user is authenticating to
+     */
+    public String getService() {
+        return service;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if ((service == null) || "".equals(service)) {
+            throw new IllegalArgumentException("service must be specified");
+        }
+    }
+}

+ 6 - 0
core/src/main/java/org/acegisecurity/ui/cas/package.html

@@ -0,0 +1,6 @@
+<html>
+<body>
+Authenticates standard web browser users via 
+Yale Central Authentication Service (CAS).
+</body>
+</html>

+ 6 - 3
core/src/main/java/org/acegisecurity/userdetails/User.java

@@ -15,6 +15,8 @@
 
 package net.sf.acegisecurity.providers.dao;
 
+import java.io.Serializable;
+
 import net.sf.acegisecurity.GrantedAuthority;
 
 
@@ -24,7 +26,7 @@ import net.sf.acegisecurity.GrantedAuthority;
  * @author Ben Alex
  * @version $Id$
  */
-public class User {
+public class User implements Serializable {
     //~ Instance fields ========================================================
 
     private String password;
@@ -53,9 +55,10 @@ public class User {
      */
     public User(String username, String password, boolean enabled,
         GrantedAuthority[] authorities) throws IllegalArgumentException {
-        if ((username == null) || (password == null) || (authorities == null)) {
+        if (((username == null) || "".equals(username)) || (password == null)
+            || "".equals(password) || (authorities == null)) {
             throw new IllegalArgumentException(
-                "Cannot pass null values to constructor");
+                "Cannot pass null or empty values to constructor");
         }
 
         for (int i = 0; i < authorities.length; i++) {

+ 1 - 1
core/src/test/java/org/acegisecurity/MockHttpServletRequest.java

@@ -47,7 +47,7 @@ import javax.servlet.http.HttpSession;
 public class MockHttpServletRequest implements HttpServletRequest {
     //~ Instance fields ========================================================
 
-    private HttpSession session;
+    private HttpSession session = new MockHttpSession();
     private Map headersMap = new HashMap();
     private Map paramMap = new HashMap();
     private Principal principal;

+ 404 - 0
core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationProviderTests.java

@@ -0,0 +1,404 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.providers.cas.ticketvalidator.AbstractTicketValidator;
+import net.sf.acegisecurity.ui.cas.CasProcessingFilter;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link CasAuthenticationProvider}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasAuthenticationProviderTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasAuthenticationProviderTests() {
+        super();
+    }
+
+    public CasAuthenticationProviderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasAuthenticationProviderTests.class);
+    }
+
+    public void testAuthenticateStateful() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider(true));
+        cap.setKey("qwerty");
+
+        StatelessTicketCache cache = new MockStatelessTicketCache();
+        cap.setStatelessTicketCache(cache);
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATEFUL_IDENTIFIER,
+                "ST-123");
+
+        Authentication result = cap.authenticate(token);
+
+        // Confirm ST-123 was NOT added to the cache
+        assertTrue(cache.getByTicketId("ST-456") == null);
+
+        if (!(result instanceof CasAuthenticationToken)) {
+            fail("Should have returned a CasAuthenticationToken");
+        }
+
+        CasAuthenticationToken casResult = (CasAuthenticationToken) result;
+        assertEquals("marissa", casResult.getPrincipal());
+        assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt",
+            casResult.getProxyGrantingTicketIou());
+        assertEquals("https://localhost/portal/j_acegi_cas_security_check",
+            casResult.getProxyList().get(0));
+        assertEquals("ST-123", casResult.getCredentials());
+        assertEquals(new GrantedAuthorityImpl("ROLE_A"),
+            casResult.getAuthorities()[0]);
+        assertEquals(new GrantedAuthorityImpl("ROLE_B"),
+            casResult.getAuthorities()[1]);
+        assertEquals(cap.getKey().hashCode(), casResult.getKeyHash());
+
+        // Now confirm the CasAuthenticationToken is automatically re-accepted.
+        // To ensure TicketValidator not called again, set it to deliver an exception...
+        cap.setTicketValidator(new MockTicketValidator(false));
+
+        Authentication laterResult = cap.authenticate(result);
+        assertEquals(result, laterResult);
+    }
+
+    public void testAuthenticateStateless() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider(true));
+        cap.setKey("qwerty");
+
+        StatelessTicketCache cache = new MockStatelessTicketCache();
+        cap.setStatelessTicketCache(cache);
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATELESS_IDENTIFIER,
+                "ST-456");
+
+        Authentication result = cap.authenticate(token);
+
+        // Confirm ST-456 was added to the cache
+        assertTrue(cache.getByTicketId("ST-456") != null);
+
+        if (!(result instanceof CasAuthenticationToken)) {
+            fail("Should have returned a CasAuthenticationToken");
+        }
+
+        assertEquals("marissa", result.getPrincipal());
+        assertEquals("ST-456", result.getCredentials());
+
+        // Now try to authenticate again. To ensure TicketValidator not
+        // called again, set it to deliver an exception...
+        cap.setTicketValidator(new MockTicketValidator(false));
+
+        // Previously created UsernamePasswordAuthenticationToken is OK
+        Authentication newResult = cap.authenticate(token);
+        assertEquals("marissa", newResult.getPrincipal());
+        assertEquals("ST-456", newResult.getCredentials());
+    }
+
+    public void testDetectsAMissingTicketId() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider(true));
+        cap.setKey("qwerty");
+
+        StatelessTicketCache cache = new MockStatelessTicketCache();
+        cap.setStatelessTicketCache(cache);
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(CasProcessingFilter.CAS_STATEFUL_IDENTIFIER,
+                "");
+
+        try {
+            Authentication result = cap.authenticate(token);
+            fail("Should have thrown BadCredentialsException");
+        } catch (BadCredentialsException expected) {
+            assertEquals("Failed to provide a CAS service ticket to validate",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsAnInvalidKey() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider(true));
+        cap.setKey("qwerty");
+
+        StatelessTicketCache cache = new MockStatelessTicketCache();
+        cap.setStatelessTicketCache(cache);
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        CasAuthenticationToken token = new CasAuthenticationToken("WRONG_KEY",
+                "test", "credentials",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("XX")},
+                new Vector(), "IOU-xxx");
+
+        try {
+            Authentication result = cap.authenticate(token);
+            fail("Should have thrown BadCredentialsException");
+        } catch (BadCredentialsException expected) {
+            assertEquals("The presented CasAuthenticationToken does not contain the expected key",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingAuthoritiesPopulator()
+        throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+
+        try {
+            cap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A casAuthoritiesPopulator must be set",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingKey() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+
+        try {
+            cap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingProxyDecider() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+
+        try {
+            cap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A casProxyDecider must be set", expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingStatelessTicketCache()
+        throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setKey("qwerty");
+        cap.setTicketValidator(new MockTicketValidator(true));
+
+        try {
+            cap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A statelessTicketCache must be set",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingTicketValidator() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider(true));
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+
+        try {
+            cap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A ticketValidator must be set", expected.getMessage());
+        }
+    }
+
+    public void testGettersSetters() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        assertTrue(cap.getCasAuthoritiesPopulator() != null);
+        assertTrue(cap.getCasProxyDecider() != null);
+        assertEquals("qwerty", cap.getKey());
+        assertTrue(cap.getStatelessTicketCache() != null);
+        assertTrue(cap.getTicketValidator() != null);
+    }
+
+    public void testIgnoresClassesItDoesNotSupport() throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        TestingAuthenticationToken token = new TestingAuthenticationToken("user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        assertFalse(cap.supports(TestingAuthenticationToken.class));
+
+        // Try it anyway
+        assertEquals(null, cap.authenticate(token));
+    }
+
+    public void testIgnoresUsernamePasswordAuthenticationTokensWithoutCasIdentifiersAsPrincipal()
+        throws Exception {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        cap.setCasAuthoritiesPopulator(new MockAuthoritiesPopulator());
+        cap.setCasProxyDecider(new MockProxyDecider());
+        cap.setKey("qwerty");
+        cap.setStatelessTicketCache(new MockStatelessTicketCache());
+        cap.setTicketValidator(new MockTicketValidator(true));
+        cap.afterPropertiesSet();
+
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("some_normal_user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        assertEquals(null, cap.authenticate(token));
+    }
+
+    public void testSupports() {
+        CasAuthenticationProvider cap = new CasAuthenticationProvider();
+        assertTrue(cap.supports(UsernamePasswordAuthenticationToken.class));
+        assertTrue(cap.supports(CasAuthenticationToken.class));
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockAuthoritiesPopulator implements CasAuthoritiesPopulator {
+        public GrantedAuthority[] getAuthorities(String casUserId)
+            throws AuthenticationException {
+            return new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A"), new GrantedAuthorityImpl(
+                    "ROLE_B")};
+        }
+    }
+
+    private class MockProxyDecider implements CasProxyDecider {
+        private boolean acceptProxy;
+
+        public MockProxyDecider(boolean acceptProxy) {
+            this.acceptProxy = acceptProxy;
+        }
+
+        private MockProxyDecider() {
+            super();
+        }
+
+        public void confirmProxyListTrusted(List proxyList)
+            throws ProxyUntrustedException {
+            if (acceptProxy) {
+                return;
+            } else {
+                throw new ProxyUntrustedException("As requested from mock");
+            }
+        }
+    }
+
+    private class MockStatelessTicketCache implements StatelessTicketCache {
+        private Map cache = new HashMap();
+
+        public CasAuthenticationToken getByTicketId(String serviceTicket) {
+            return (CasAuthenticationToken) cache.get(serviceTicket);
+        }
+
+        public void putTicketInCache(CasAuthenticationToken token) {
+            cache.put(token.getCredentials().toString(), token);
+        }
+
+        public void removeTicketFromCache(CasAuthenticationToken token) {
+            throw new UnsupportedOperationException(
+                "mock method not implemented");
+        }
+
+        public void removeTicketFromCache(String serviceTicket) {
+            throw new UnsupportedOperationException(
+                "mock method not implemented");
+        }
+    }
+
+    private class MockTicketValidator extends AbstractTicketValidator {
+        private boolean returnTicket;
+
+        public MockTicketValidator(boolean returnTicket) {
+            this.returnTicket = returnTicket;
+        }
+
+        private MockTicketValidator() {
+            super();
+        }
+
+        public TicketResponse confirmTicketValid(String serviceTicket)
+            throws AuthenticationException {
+            if (returnTicket) {
+                List list = new Vector();
+                list.add("https://localhost/portal/j_acegi_cas_security_check");
+
+                return new TicketResponse("marissa", list,
+                    "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            }
+
+            throw new BadCredentialsException("As requested from mock");
+        }
+    }
+}

+ 283 - 0
core/src/test/java/org/acegisecurity/providers/cas/CasAuthenticationTokenTests.java

@@ -0,0 +1,283 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link CasAuthenticationToken}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasAuthenticationTokenTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasAuthenticationTokenTests() {
+        super();
+    }
+
+    public CasAuthenticationTokenTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasAuthenticationTokenTests.class);
+    }
+
+    public void testConstructorRejectsNulls() {
+        try {
+            new CasAuthenticationToken(null, "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", null, "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", "Test", null,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", "Test", "Password", null,
+                new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, null,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(), null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new CasAuthenticationToken("key", "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), null, new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testEqualsWhenEqual() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token1 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList1,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        List proxyList2 = new Vector();
+        proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token2 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList2,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        assertEquals(token1, token2);
+    }
+
+    public void testGetters() {
+        // Build the proxy list returned in the ticket from CAS
+        List proxyList = new Vector();
+        proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        assertEquals("key".hashCode(), token.getKeyHash());
+        assertEquals("Test", token.getPrincipal());
+        assertEquals("Password", token.getCredentials());
+        assertEquals("ROLE_ONE", token.getAuthorities()[0].getAuthority());
+        assertEquals("ROLE_TWO", token.getAuthorities()[1].getAuthority());
+        assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt",
+            token.getProxyGrantingTicketIou());
+        assertEquals(proxyList, token.getProxyList());
+    }
+
+    public void testNoArgConstructor() {
+        try {
+            new CasAuthenticationToken();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testNotEqualsDueToAbstractParentEqualsCheck() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token1 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList1,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        List proxyList2 = new Vector();
+        proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token2 = new CasAuthenticationToken("key",
+                "OTHER_VALUE", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList2,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        assertTrue(!token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToDifferentAuthenticationClass() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token1 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList1,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test",
+                "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+        token2.setAuthenticated(true);
+
+        assertTrue(!token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToProxyGrantingTicket() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token1 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList1,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        List proxyList2 = new Vector();
+        proxyList2.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token2 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList2, "PGTIOU-SOME_OTHER_VALUE");
+
+        assertTrue(!token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToProxyList() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token1 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList1,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        List proxyList2 = new Vector();
+        proxyList2.add(
+            "https://localhost/SOME_OTHER_PORTAL/j_acegi_cas_security_check");
+
+        CasAuthenticationToken token2 = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, proxyList2,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+
+        assertTrue(!token1.equals(token2));
+    }
+
+    public void testSetAuthenticatedIgnored() {
+        CasAuthenticationToken token = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        assertTrue(token.isAuthenticated());
+        token.setAuthenticated(false); // ignored
+        assertTrue(token.isAuthenticated());
+    }
+
+    public void testToString() {
+        CasAuthenticationToken token = new CasAuthenticationToken("key",
+                "Test", "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")}, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        String result = token.toString();
+        assertTrue(result.lastIndexOf("Proxy List:") != -1);
+        assertTrue(result.lastIndexOf("Proxy-Granting Ticket IOU:") != -1);
+        assertTrue(result.lastIndexOf("Credentials (Service/Proxy Ticket):") != -1);
+    }
+}

+ 102 - 0
core/src/test/java/org/acegisecurity/providers/cas/TicketResponseTests.java

@@ -0,0 +1,102 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link TicketResponse}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class TicketResponseTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public TicketResponseTests() {
+        super();
+    }
+
+    public TicketResponseTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(TicketResponseTests.class);
+    }
+
+    public void testConstructorAcceptsNullProxyGrantingTicketIOU() {
+        TicketResponse ticket = new TicketResponse("marissa", new Vector(), null);
+        assertEquals("", ticket.getProxyGrantingTicketIou());
+    }
+
+    public void testConstructorAcceptsNullProxyList() {
+        TicketResponse ticket = new TicketResponse("marissa", null,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        assertEquals(new Vector(), ticket.getProxyList());
+    }
+
+    public void testConstructorRejectsNullUser() {
+        try {
+            new TicketResponse(null, new Vector(),
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGetters() {
+        // Build the proxy list returned in the ticket from CAS
+        List proxyList = new Vector();
+        proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        TicketResponse ticket = new TicketResponse("marissa", proxyList,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        assertEquals("marissa", ticket.getUser());
+        assertEquals(proxyList, ticket.getProxyList());
+        assertEquals("PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt",
+            ticket.getProxyGrantingTicketIou());
+    }
+
+    public void testNoArgConstructor() {
+        try {
+            new TicketResponse();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testToString() {
+        TicketResponse ticket = new TicketResponse("marissa", null,
+                "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+        String result = ticket.toString();
+        assertTrue(result.lastIndexOf("Proxy List:") != -1);
+        assertTrue(result.lastIndexOf("Proxy-Granting Ticket IOU:") != -1);
+        assertTrue(result.lastIndexOf("User:") != -1);
+    }
+}

+ 89 - 0
core/src/test/java/org/acegisecurity/providers/cas/cache/EhCacheBasedTicketCacheTests.java

@@ -0,0 +1,89 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.cache;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.cas.CasAuthenticationToken;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link EhCacheBasedTicketCache}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class EhCacheBasedTicketCacheTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public EhCacheBasedTicketCacheTests() {
+        super();
+    }
+
+    public EhCacheBasedTicketCacheTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(EhCacheBasedTicketCacheTests.class);
+    }
+
+    public void testCacheOperation() throws Exception {
+        EhCacheBasedTicketCache cache = new EhCacheBasedTicketCache();
+        cache.afterPropertiesSet();
+
+        // Check it gets stored in the cache
+        cache.putTicketInCache(getToken());
+        assertEquals(getToken(),
+            cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ"));
+
+        // Check it gets removed from the cache
+        cache.removeTicketFromCache(getToken());
+        assertNull(cache.getByTicketId("ST-0-ER94xMJmn6pha35CQRoZ"));
+
+        // Check it doesn't return values for null or unknown service tickets
+        assertNull(cache.getByTicketId(null));
+        assertNull(cache.getByTicketId("UNKNOWN_SERVICE_TICKET"));
+    }
+
+    public void testGettersSetters() {
+        EhCacheBasedTicketCache cache = new EhCacheBasedTicketCache();
+        cache.setMinutesToIdle(5);
+        assertEquals(5, cache.getMinutesToIdle());
+    }
+
+    private CasAuthenticationToken getToken() {
+        List proxyList = new Vector();
+        proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        return new CasAuthenticationToken("key", "marissa",
+            "ST-0-ER94xMJmn6pha35CQRoZ",
+            new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                    "ROLE_TWO")}, proxyList,
+            "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+    }
+}

+ 148 - 0
core/src/test/java/org/acegisecurity/providers/cas/populator/DaoCasAuthoritiesPopulatorTests.java

@@ -0,0 +1,148 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.populator;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+import net.sf.acegisecurity.providers.dao.User;
+import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.DataRetrievalFailureException;
+
+
+/**
+ * Tests {@link DaoCasAuthoritiesPopulator}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class DaoCasAuthoritiesPopulatorTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public DaoCasAuthoritiesPopulatorTests() {
+        super();
+    }
+
+    public DaoCasAuthoritiesPopulatorTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(DaoCasAuthoritiesPopulatorTests.class);
+    }
+
+    public void testDetectsMissingAuthenticationDao() throws Exception {
+        DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator();
+
+        try {
+            populator.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("An authenticationDao must be set",
+                expected.getMessage());
+        }
+    }
+
+    public void testGetGrantedAuthoritiesForInvalidUsername()
+        throws Exception {
+        DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator();
+        populator.setAuthenticationDao(new MockAuthenticationDaoUserMarissa());
+        populator.afterPropertiesSet();
+
+        try {
+            populator.getAuthorities("scott");
+            fail("Should have thrown UsernameNotFoundException");
+        } catch (UsernameNotFoundException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGetGrantedAuthoritiesForValidUsername()
+        throws Exception {
+        DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator();
+        populator.setAuthenticationDao(new MockAuthenticationDaoUserMarissa());
+        populator.afterPropertiesSet();
+
+        GrantedAuthority[] results = populator.getAuthorities("marissa");
+        assertEquals(2, results.length);
+        assertEquals(new GrantedAuthorityImpl("ROLE_ONE"), results[0]);
+        assertEquals(new GrantedAuthorityImpl("ROLE_TWO"), results[1]);
+    }
+
+    public void testGetGrantedAuthoritiesWhenDaoThrowsException()
+        throws Exception {
+        DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator();
+        populator.setAuthenticationDao(new MockAuthenticationDaoSimulateBackendError());
+        populator.afterPropertiesSet();
+
+        try {
+            populator.getAuthorities("THE_DAO_WILL_FAIL");
+            fail("Should have thrown DataRetrievalFailureException");
+        } catch (DataRetrievalFailureException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGettersSetters() {
+        DaoCasAuthoritiesPopulator populator = new DaoCasAuthoritiesPopulator();
+        AuthenticationDao dao = new MockAuthenticationDaoUserMarissa();
+        populator.setAuthenticationDao(dao);
+        assertEquals(dao, populator.getAuthenticationDao());
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockAuthenticationDaoSimulateBackendError
+        implements AuthenticationDao {
+        public long getRefreshDuration() {
+            return 0;
+        }
+
+        public User loadUserByUsername(String username)
+            throws UsernameNotFoundException, DataAccessException {
+            throw new DataRetrievalFailureException(
+                "This mock simulator is designed to fail");
+        }
+    }
+
+    private class MockAuthenticationDaoUserMarissa implements AuthenticationDao {
+        public long getRefreshDuration() {
+            return 0;
+        }
+
+        public User loadUserByUsername(String username)
+            throws UsernameNotFoundException, DataAccessException {
+            if ("marissa".equals(username)) {
+                return new User("marissa", "koala", true,
+                    new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                            "ROLE_TWO")});
+            } else {
+                throw new UsernameNotFoundException("Could not find: "
+                    + username);
+            }
+        }
+    }
+}

+ 66 - 0
core/src/test/java/org/acegisecurity/providers/cas/proxy/AcceptAnyCasProxyTests.java

@@ -0,0 +1,66 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import junit.framework.TestCase;
+
+import java.util.Vector;
+
+
+/**
+ * Tests {@link AcceptAnyCasProxy}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AcceptAnyCasProxyTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AcceptAnyCasProxyTests() {
+        super();
+    }
+
+    public AcceptAnyCasProxyTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AcceptAnyCasProxyTests.class);
+    }
+
+    public void testDoesNotAcceptNull() {
+        AcceptAnyCasProxy proxyDecider = new AcceptAnyCasProxy();
+
+        try {
+            proxyDecider.confirmProxyListTrusted(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("proxyList cannot be null", expected.getMessage());
+        }
+    }
+
+    public void testNormalOperation() {
+        AcceptAnyCasProxy proxyDecider = new AcceptAnyCasProxy();
+        proxyDecider.confirmProxyListTrusted(new Vector());
+        assertTrue(true); // as no Exception thrown
+    }
+}

+ 141 - 0
core/src/test/java/org/acegisecurity/providers/cas/proxy/NamedCasProxyDeciderTests.java

@@ -0,0 +1,141 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link NamedCasProxyDecider}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NamedCasProxyDeciderTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public NamedCasProxyDeciderTests() {
+        super();
+    }
+
+    public NamedCasProxyDeciderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(NamedCasProxyDeciderTests.class);
+    }
+
+    public void testAcceptsIfNearestProxyIsAuthorized()
+        throws Exception {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+
+        // Build the ticket returned from CAS
+        List proxyList = new Vector();
+        proxyList.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        // Build the list of valid nearest proxies
+        List validProxies = new Vector();
+        validProxies.add("https://localhost/portal/j_acegi_cas_security_check");
+        validProxies.add(
+            "https://localhost/newPortal/j_acegi_cas_security_check");
+        proxyDecider.setValidProxies(validProxies);
+        proxyDecider.afterPropertiesSet();
+
+        proxyDecider.confirmProxyListTrusted(proxyList);
+        assertTrue(true);
+    }
+
+    public void testAcceptsIfNoProxiesInTicket() {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+        List proxyList = new Vector(); // no proxies in list
+
+        proxyDecider.confirmProxyListTrusted(proxyList);
+        assertTrue(true);
+    }
+
+    public void testDetectsMissingValidProxiesList() throws Exception {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+
+        try {
+            proxyDecider.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A validProxies list must be set",
+                expected.getMessage());
+        }
+    }
+
+    public void testDoesNotAcceptNull() {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+
+        try {
+            proxyDecider.confirmProxyListTrusted(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("proxyList cannot be null", expected.getMessage());
+        }
+    }
+
+    public void testGettersSetters() {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+
+        // Build the list of valid nearest proxies
+        List validProxies = new Vector();
+        validProxies.add("https://localhost/portal/j_acegi_cas_security_check");
+        validProxies.add(
+            "https://localhost/newPortal/j_acegi_cas_security_check");
+        proxyDecider.setValidProxies(validProxies);
+
+        assertEquals(validProxies, proxyDecider.getValidProxies());
+    }
+
+    public void testRejectsIfNearestProxyIsNotAuthorized()
+        throws Exception {
+        NamedCasProxyDecider proxyDecider = new NamedCasProxyDecider();
+
+        // Build the ticket returned from CAS
+        List proxyList = new Vector();
+        proxyList.add(
+            "https://localhost/untrustedWebApp/j_acegi_cas_security_check");
+
+        // Build the list of valid nearest proxies
+        List validProxies = new Vector();
+        validProxies.add("https://localhost/portal/j_acegi_cas_security_check");
+        validProxies.add(
+            "https://localhost/newPortal/j_acegi_cas_security_check");
+        proxyDecider.setValidProxies(validProxies);
+        proxyDecider.afterPropertiesSet();
+
+        try {
+            proxyDecider.confirmProxyListTrusted(proxyList);
+            fail("Should have thrown ProxyUntrustedException");
+        } catch (ProxyUntrustedException expected) {
+            assertTrue(true);
+        }
+    }
+}

+ 84 - 0
core/src/test/java/org/acegisecurity/providers/cas/proxy/RejectProxyTicketsTests.java

@@ -0,0 +1,84 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.proxy;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link RejectProxyTickets}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RejectProxyTicketsTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public RejectProxyTicketsTests() {
+        super();
+    }
+
+    public RejectProxyTicketsTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(RejectProxyTicketsTests.class);
+    }
+
+    public void testAcceptsIfNoProxiesInTicket() {
+        RejectProxyTickets proxyDecider = new RejectProxyTickets();
+        List proxyList = new Vector(); // no proxies in list
+
+        proxyDecider.confirmProxyListTrusted(proxyList);
+        assertTrue(true);
+    }
+
+    public void testDoesNotAcceptNull() {
+        RejectProxyTickets proxyDecider = new RejectProxyTickets();
+
+        try {
+            proxyDecider.confirmProxyListTrusted(null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("proxyList cannot be null", expected.getMessage());
+        }
+    }
+
+    public void testRejectsIfAnyProxyInList() {
+        RejectProxyTickets proxyDecider = new RejectProxyTickets();
+        List proxyList = new Vector();
+        proxyList.add("https://localhost/webApp/j_acegi_cas_security_check");
+
+        try {
+            proxyDecider.confirmProxyListTrusted(proxyList);
+            fail("Should have thrown ProxyUntrustedException");
+        } catch (ProxyUntrustedException expected) {
+            assertTrue(true);
+        }
+    }
+}

+ 143 - 0
core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/AbstractTicketValidatorTests.java

@@ -0,0 +1,143 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.ticketvalidator;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.providers.cas.TicketResponse;
+import net.sf.acegisecurity.ui.cas.ServiceProperties;
+
+import java.util.Vector;
+
+
+/**
+ * Tests {@link AbstractTicketValidator}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AbstractTicketValidatorTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AbstractTicketValidatorTests() {
+        super();
+    }
+
+    public AbstractTicketValidatorTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AbstractTicketValidatorTests.class);
+    }
+
+    public void testDetectsMissingCasValidate() throws Exception {
+        AbstractTicketValidator tv = new MockAbstractTicketValidator();
+        tv.setServiceProperties(new ServiceProperties());
+
+        try {
+            tv.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("A casValidate URL must be set", expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingServiceProperties() throws Exception {
+        AbstractTicketValidator tv = new MockAbstractTicketValidator();
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+
+        try {
+            tv.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("serviceProperties must be specified",
+                expected.getMessage());
+        }
+    }
+
+    public void testGetters() throws Exception {
+        AbstractTicketValidator tv = new MockAbstractTicketValidator();
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+        assertEquals("https://company.com/cas/proxyvalidate",
+            tv.getCasValidate());
+
+        tv.setServiceProperties(new ServiceProperties());
+        assertTrue(tv.getServiceProperties() != null);
+
+        tv.afterPropertiesSet();
+
+        tv.setTrustStore("/some/file/cacerts");
+        assertEquals("/some/file/cacerts", tv.getTrustStore());
+    }
+
+    public void testSystemPropertySetDuringAfterPropertiesSet()
+        throws Exception {
+        AbstractTicketValidator tv = new MockAbstractTicketValidator();
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+        assertEquals("https://company.com/cas/proxyvalidate",
+            tv.getCasValidate());
+
+        tv.setServiceProperties(new ServiceProperties());
+        assertTrue(tv.getServiceProperties() != null);
+
+        tv.setTrustStore("/some/file/cacerts");
+        assertEquals("/some/file/cacerts", tv.getTrustStore());
+
+        String before = System.getProperty("javax.net.ssl.trustStore");
+        tv.afterPropertiesSet();
+        assertEquals("/some/file/cacerts",
+            System.getProperty("javax.net.ssl.trustStore"));
+
+        if (before == null) {
+            System.setProperty("javax.net.ssl.trustStore", "");
+        } else {
+            System.setProperty("javax.net.ssl.trustStore", before);
+        }
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockAbstractTicketValidator extends AbstractTicketValidator {
+        private boolean returnTicket;
+
+        public MockAbstractTicketValidator(boolean returnTicket) {
+            this.returnTicket = returnTicket;
+        }
+
+        private MockAbstractTicketValidator() {
+            super();
+        }
+
+        public TicketResponse confirmTicketValid(String serviceTicket)
+            throws AuthenticationException {
+            if (returnTicket) {
+                return new TicketResponse("user", new Vector(),
+                    "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            }
+
+            throw new BadCredentialsException("As requested by mock");
+        }
+    }
+}

+ 138 - 0
core/src/test/java/org/acegisecurity/providers/cas/ticketvalidator/CasProxyTicketValidatorTests.java

@@ -0,0 +1,138 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.providers.cas.ticketvalidator;
+
+import edu.yale.its.tp.cas.client.ProxyTicketValidator;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.AuthenticationServiceException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.providers.cas.TicketResponse;
+import net.sf.acegisecurity.ui.cas.ServiceProperties;
+
+import java.util.Vector;
+
+
+/**
+ * Tests {@link CasProxyTicketValidator}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProxyTicketValidatorTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasProxyTicketValidatorTests() {
+        super();
+    }
+
+    public CasProxyTicketValidatorTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasProxyTicketValidatorTests.class);
+    }
+
+    public void testGetters() {
+        CasProxyTicketValidator tv = new CasProxyTicketValidator();
+        tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator");
+        assertEquals("http://my.com/webapp/casProxy/someValidator",
+            tv.getProxyCallbackUrl());
+    }
+
+    public void testNormalOperation() {
+        ServiceProperties sp = new ServiceProperties();
+        sp.setSendRenew(true);
+        sp.setService("https://my.com/webapp//j_acegi_cas_security_check");
+
+        CasProxyTicketValidator tv = new MockCasProxyTicketValidator(true, false);
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+        tv.setServiceProperties(sp);
+        tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator");
+
+        TicketResponse response = tv.confirmTicketValid(
+                "ST-0-ER94xMJmn6pha35CQRoZ");
+
+        assertEquals("user", response.getUser());
+    }
+
+    public void testProxyTicketValidatorInternalExceptionsGracefullyHandled() {
+        CasProxyTicketValidator tv = new MockCasProxyTicketValidator(false, true);
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+        tv.setServiceProperties(new ServiceProperties());
+        tv.setProxyCallbackUrl("http://my.com/webapp/casProxy/someValidator");
+
+        try {
+            tv.confirmTicketValid("ST-0-ER94xMJmn6pha35CQRoZ");
+            fail("Should have thrown AuthenticationServiceException");
+        } catch (AuthenticationServiceException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testValidationFailsOkAndOperationWithoutAProxyCallbackUrl() {
+        CasProxyTicketValidator tv = new MockCasProxyTicketValidator(false,
+                false);
+        tv.setCasValidate("https://company.com/cas/proxyvalidate");
+        tv.setServiceProperties(new ServiceProperties());
+
+        try {
+            tv.confirmTicketValid("ST-0-ER94xMJmn6pha35CQRoZ");
+            fail("Should have thrown BadCredentialsExpected");
+        } catch (BadCredentialsException expected) {
+            assertTrue(true);
+        }
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockCasProxyTicketValidator extends CasProxyTicketValidator {
+        private boolean returnTicket;
+        private boolean throwAuthenticationServiceException;
+
+        public MockCasProxyTicketValidator(boolean returnTicket,
+            boolean throwAuthenticationServiceException) {
+            this.returnTicket = returnTicket;
+            this.throwAuthenticationServiceException = throwAuthenticationServiceException;
+        }
+
+        private MockCasProxyTicketValidator() {
+            super();
+        }
+
+        protected TicketResponse validateNow(ProxyTicketValidator pv)
+            throws AuthenticationServiceException, BadCredentialsException {
+            if (returnTicket) {
+                return new TicketResponse("user", new Vector(),
+                    "PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt");
+            }
+
+            if (throwAuthenticationServiceException) {
+                throw new AuthenticationServiceException("As requested by mock");
+            }
+
+            throw new BadCredentialsException("As requested by mock");
+        }
+    }
+}

+ 126 - 0
core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterEntryPointTests.java

@@ -0,0 +1,126 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+
+
+/**
+ * Tests {@link CasProcessingFilterEntryPoint}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProcessingFilterEntryPointTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasProcessingFilterEntryPointTests() {
+        super();
+    }
+
+    public CasProcessingFilterEntryPointTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasProcessingFilterEntryPointTests.class);
+    }
+
+    public void testDetectsMissingLoginFormUrl() throws Exception {
+        CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint();
+        ep.setServiceProperties(new ServiceProperties());
+
+        try {
+            ep.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("loginUrl must be specified", expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingServiceProperties() throws Exception {
+        CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint();
+        ep.setLoginUrl("https://cas/login");
+
+        try {
+            ep.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("serviceProperties must be specified",
+                expected.getMessage());
+        }
+    }
+
+    public void testGettersSetters() {
+        CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint();
+        ep.setLoginUrl("https://cas/login");
+        assertEquals("https://cas/login", ep.getLoginUrl());
+
+        ep.setServiceProperties(new ServiceProperties());
+        assertTrue(ep.getServiceProperties() != null);
+    }
+
+    public void testNormalOperationWithRenewFalse() throws Exception {
+        ServiceProperties sp = new ServiceProperties();
+        sp.setSendRenew(false);
+        sp.setService(
+            "https://mycompany.com/bigWebApp/j_acegi_cas_security_check");
+
+        CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint();
+        ep.setLoginUrl("https://cas/login");
+        ep.setServiceProperties(sp);
+
+        MockHttpServletRequest request = new MockHttpServletRequest(
+                "/some_path");
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        ep.afterPropertiesSet();
+        ep.commence(request, response);
+        assertEquals("https://cas/login?service=https://mycompany.com/bigWebApp/j_acegi_cas_security_check",
+            response.getRedirect());
+    }
+
+    public void testNormalOperationWithRenewTrue() throws Exception {
+        ServiceProperties sp = new ServiceProperties();
+        sp.setSendRenew(true);
+        sp.setService(
+            "https://mycompany.com/bigWebApp/j_acegi_cas_security_check");
+
+        CasProcessingFilterEntryPoint ep = new CasProcessingFilterEntryPoint();
+        ep.setLoginUrl("https://cas/login");
+        ep.setServiceProperties(sp);
+
+        MockHttpServletRequest request = new MockHttpServletRequest(
+                "/some_path");
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        ep.afterPropertiesSet();
+        ep.commence(request, response);
+        assertEquals("https://cas/login?renew=true&service=https://mycompany.com/bigWebApp/j_acegi_cas_security_check",
+            response.getRedirect());
+    }
+}

+ 93 - 0
core/src/test/java/org/acegisecurity/ui/cas/CasProcessingFilterTests.java

@@ -0,0 +1,93 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.MockAuthenticationManager;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpSession;
+
+
+/**
+ * Tests {@link CasProcessingFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class CasProcessingFilterTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public CasProcessingFilterTests() {
+        super();
+    }
+
+    public CasProcessingFilterTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(CasProcessingFilterTests.class);
+    }
+
+    public void testGetters() {
+        CasProcessingFilter filter = new CasProcessingFilter();
+        assertEquals("/j_acegi_cas_security_check",
+            filter.getDefaultFilterProcessesUrl());
+    }
+
+    public void testNormalOperation() throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest(null,
+                new MockHttpSession());
+        request.setParameter("ticket", "ST-0-ER94xMJmn6pha35CQRoZ");
+
+        MockAuthenticationManager authMgr = new MockAuthenticationManager(true);
+
+        CasProcessingFilter filter = new CasProcessingFilter();
+        filter.setAuthenticationManager(authMgr);
+        filter.init(null);
+
+        Authentication result = filter.attemptAuthentication(request);
+        assertTrue(result != null);
+    }
+
+    public void testNullServiceTicketHandledGracefully()
+        throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest(null,
+                new MockHttpSession());
+
+        MockAuthenticationManager authMgr = new MockAuthenticationManager(false);
+
+        CasProcessingFilter filter = new CasProcessingFilter();
+        filter.setAuthenticationManager(authMgr);
+        filter.init(null);
+
+        try {
+            filter.attemptAuthentication(request);
+            fail("Should have thrown AuthenticationException");
+        } catch (AuthenticationException expected) {
+            assertTrue(true);
+        }
+    }
+}

+ 71 - 0
core/src/test/java/org/acegisecurity/ui/cas/ServicePropertiesTests.java

@@ -0,0 +1,71 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.ui.cas;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests {@link ServiceProperties}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ServicePropertiesTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public ServicePropertiesTests() {
+        super();
+    }
+
+    public ServicePropertiesTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(ServicePropertiesTests.class);
+    }
+
+    public void testDetectsMissingLoginFormUrl() throws Exception {
+        ServiceProperties sp = new ServiceProperties();
+
+        try {
+            sp.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertEquals("service must be specified", expected.getMessage());
+        }
+    }
+
+    public void testGettersSetters() throws Exception {
+        ServiceProperties sp = new ServiceProperties();
+        sp.setSendRenew(false);
+        assertFalse(sp.isSendRenew());
+        sp.setSendRenew(true);
+        assertTrue(sp.isSendRenew());
+
+        sp.setService("https://mycompany.com/service");
+        assertEquals("https://mycompany.com/service", sp.getService());
+        
+        sp.afterPropertiesSet();
+    }
+}

+ 606 - 13
docs/reference/src/index.xml

@@ -294,7 +294,7 @@
         handling each request. Handling involves a number of
         operations:</para>
 
-        <itemizedlist spacing="compact">
+        <orderedlist>
           <listitem>
             <para>Store the configuration attributes that are associated with
             each secure request.</para>
@@ -354,7 +354,7 @@
             <para>Return any result received from the
             <literal>SecurityInterceptorCallback</literal>.</para>
           </listitem>
-        </itemizedlist>
+        </orderedlist>
 
         <para>Whilst this may seem quite involved, don't worry. Developers
         interact with the security process by simply implementing basic
@@ -854,6 +854,13 @@
               <literal>AuthenticationProvider</literal> if you were not using
               container adapters.</para>
             </listitem>
+
+            <listitem>
+              <para><literal>CasAuthenticationProvider</literal> is able to
+              authenticate Yale Central Authentication Service (CAS) tickets.
+              This is discussed further in the CAS Single Sign On
+              section.</para>
+            </listitem>
           </itemizedlist></para>
       </sect2>
 
@@ -870,8 +877,26 @@
 
         <para><programlisting>&lt;bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"&gt;
   &lt;property name="authenticationDao"&gt;&lt;ref bean="inMemoryDaoImpl"/&gt;&lt;/property&gt;
+  &lt;property name="saltSource"&gt;&lt;ref bean="saltSource"/&gt;&lt;/property&gt;
+  &lt;property name="passwordEncoder"&gt;&lt;ref bean="passwordEncoder"/&gt;&lt;/property&gt;
 &lt;/bean&gt;</programlisting></para>
 
+        <para>The <literal>PasswordEncoder</literal> and
+        <literal>SaltSource</literal> are optional. A
+        <literal>PasswordEncoder</literal> provides encoding and decoding of
+        passwords obtained from the authentication repository. A
+        <literal>SaltSource</literal> enables the passwords to be populated
+        with a "salt", which enhances the security of the passwords in the
+        authentication repository. <literal>PasswordEncoder</literal>
+        implementations are provided with the Acegi Security System for Spring
+        covering MD5, SHA and cleartext encodings. Two
+        <literal>SaltSource</literal> implementations are also provided:
+        <literal>SystemWideSaltSource</literal> which encodes all passwords
+        with the same salt, and <literal>ReflectionSaltSource</literal>, which
+        inspects a given property of the returned User object to obtain the
+        salt. Please refer to the JavaDocs for further details on these
+        optional features.</para>
+
         <para>For a class to be able to provide the
         <literal>DaoAuthenticationProvider</literal> with access to an
         authentication repository, it must implement the
@@ -957,10 +982,13 @@
         <para>You can use different relational database management systems by
         modifying the <literal>DriverManagerDataSource</literal> shown above.
         Irrespective of the database used, a standard schema must be used as
-        indicated in <literal>dbinit.txt</literal>. Of particular note is the
-        database must return responses that treat the username as case
-        insensitive, in order to comply with the
-        <literal>AuthenticationDao</literal> contract.</para>
+        indicated in <literal>dbinit.txt</literal>.</para>
+
+        <para>If you default schema is unsuitable for your needs,
+        <literal>JdbcDaoImpl</literal> provides two properties that allow
+        customisation of the SQL statements. You may also subclass the
+        <literal>JdbcDaoImpl</literal> if further customisation is necessary.
+        Please refer to the JavaDocs for details.</para>
 
         <para>The Acegi Security System for Spring ships with a Hypersonic SQL
         instance that has the required authentication information and sample
@@ -1487,11 +1515,13 @@ public boolean supports(Class clazz);</programlisting></para>
         <literal>HttpSession</literal> object and filters to authenticate the
         user. Another approach is HTTP Basic Authentication, which allows
         clients to use HTTP headers to present authentication information to
-        the Acegi Security System for Spring. The final approach is via
-        Container Adapters, which allow supported web containers to perform
-        the authentication themselves. HTTP Session Authentication is
-        discussed below, whilst Container Adapters are discussed in a separate
-        section.</para>
+        the Acegi Security System for Spring. Alternatively, you can also use
+        Yale Central Authentication Service (CAS) for enterprise-wide single
+        sign on. The final approach is via Container Adapters, which allow
+        supported web containers to perform the authentication themselves.
+        HTTP Session and Basic Authentication is discussed below, whilst CAS
+        and Container Adapters are discussed in separate sections of this
+        document.</para>
       </sect2>
 
       <sect2 id="security-ui-http-session">
@@ -1538,7 +1568,7 @@ public boolean supports(Class clazz);</programlisting></para>
         <literal>authenticationFailureUrl</literal>. The
         <literal>AuthenticationException</literal> will be placed into the
         <literal>HttpSession</literal> attribute indicated by
-        <literal>AuthenticationProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY</literal>,
+        <literal>AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY</literal>,
         enabling a reason to be provided to the user on the error page.</para>
 
         <para>If authentication is successful, the resulting
@@ -1552,7 +1582,7 @@ public boolean supports(Class clazz);</programlisting></para>
         browser will need to be redirected to the target URL. The target URL
         is usually indicated by the <literal>HttpSession</literal> attribute
         specified by
-        <literal>AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY</literal>.
+        <literal>AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY</literal>.
         This attribute is automatically set by the
         <literal>SecurityEnforcementFilter</literal> when an
         <literal>AuthenticationException</literal> occurs, so that after login
@@ -2100,6 +2130,569 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
       </sect2>
     </sect1>
 
+    <sect1 id="security-cas">
+      <title>Yale Central Authentication Service (CAS) Single Sign On</title>
+
+      <sect2 id="security-cas-overview">
+        <title>Overview</title>
+
+        <para>Yale University produces an enterprise-wide single sign on
+        system known as CAS. Unlike other initiatives, Yale's Central
+        Authentication Service is open source, widely used, simple to
+        understand, platform independent, and supports proxy capabilities. The
+        Acegi Security System for Spring fully supports CAS, and provides an
+        easy migration path from single-application deployments of Acegi
+        Security through to multiple-application deployments secured by an
+        enterprise-wide CAS server.</para>
+
+        <para>You can learn more about CAS at
+        <literal>http://www.yale.edu/tp/auth/</literal>. You will need to
+        visit this URL to download the CAS Server files. Whilst the Acegi
+        Security System for Spring includes two CAS libraries in the
+        "-with-dependencies" ZIP file, you will still need the CAS Java Server
+        Pages and <literal>web.xml</literal> to customise and deploy your CAS
+        server.</para>
+      </sect2>
+
+      <sect2 id="security-cas-how-cas-works">
+        <title>How CAS Works</title>
+
+        <para>Whilst the CAS web site above contains two documents that detail
+        the architecture of CAS, we present the general overview again here
+        within the context of the Acegi Security System for Spring. The
+        following refers to CAS 2.0, being the version of CAS that Acegi
+        Security for Spring supports.</para>
+
+        <para>Somewhere in your enterprise you will need to setup a CAS
+        server. The CAS server is simply a standard WAR file, so there isn't
+        anything difficult about setting up your server. Inside the WAR file
+        you will customise the login and other single sign on pages displayed
+        to users. You will also need to specify in the web.xml a
+        <literal>PasswordHandler</literal>. The
+        <literal>PasswordHandler</literal> has a simple method that returns a
+        boolean as to whether a given username and password is valid. Your
+        <literal>PasswordHandler</literal> implementation will need to link
+        into some type of backend authentication repository, such as an LDAP
+        server or database. </para>
+
+        <para>If you're running an existing CAS server, you will have already
+        established a <literal>PasswordHandler</literal>. If you have not,
+        might prefer to use the Acegi Security System for Spring
+        <literal>CasPasswordHandler</literal> class. This class delegates
+        through to the standard Acegi Security
+        <literal>AuthenticationManager</literal>, enabling you to use a
+        security configuration you might already have in place. You do not
+        need to use the <literal>CasPasswordHandler</literal> class on your
+        CAS server unless you do not wish. The Acegi Security System for
+        Spring will function as a CAS client successfully irrespective of the
+        <literal>PasswordHandler</literal> you've chosen for your CAS
+        server.</para>
+
+        <para>Apart from the CAS server itself, the other key player is of
+        course the secure web applications deployed throughout your
+        enterprise. These web applications are known as "services". There are
+        two types of services: standard services and proxy services. A proxy
+        service is able to request resources from other services on behalf of
+        the user. This will be explained more fully later.</para>
+
+        <para>Services can be developed in a large variety of languages, due
+        to CAS 2.0's very light XML-based protocol. The Yale CAS home page
+        contains a clients archive which demonstrates CAS clients in Java,
+        Active Server Pages, Perl, Python and others. Naturally, Java support
+        is very strong given the CAS server is written in Java. You do not
+        need to use one of CAS' clients to interact with the CAS server from
+        Acegi Security System for Spring secured applications. This is handled
+        transparently for you.</para>
+
+        <para>The basic interaction between a web browser, CAS server and an
+        Acegi Security for System Spring secured service is as follows:</para>
+
+        <orderedlist>
+          <listitem>
+            <para>The web user is browsing the service's public pages. CAS or
+            Acegi Security is not involved.</para>
+          </listitem>
+
+          <listitem>
+            <para>The user eventually requests a page that is either secure or
+            one of the beans it uses is secure. Acegi Security's
+            SecurityEnforcementFilter will detect the
+            AuthenticationException.</para>
+          </listitem>
+
+          <listitem>
+            <para>Because the user has no <literal>Authentication</literal>
+            object in
+            <literal>HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY</literal>,
+            the SecurityEnforcementFilter will call the configured
+            <literal>AuthenticationEntryPoint</literal>. If using CAS, this
+            will be the <literal>CasProcessingFilterEntryPoint</literal>
+            class.</para>
+          </listitem>
+
+          <listitem>
+            <para>The CasProcessingFilterEntry point will redirect the user's
+            browser to the CAS server. It will also indicate a
+            <literal>service</literal> parameter, which is the callback URL
+            for the Acegi Security service. For example, the URL the browser
+            is redirected to might be
+            <literal>https://my.company.com/cas/login?service=https://server3.company.com/webapp/j_acegi_cas_security_check</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>After the user's browser redirects to CAS, they will be
+            prompted for their username and password. If the user presents a
+            session cookie which indicates they've previously logged on, they
+            will not be prompted to login again (there is an exception to this
+            procedure, which we'll cover later). CAS will use the
+            PasswordHandler discussed above to decide whether the username and
+            password is valid</para>
+          </listitem>
+
+          <listitem>
+            <para>Upon successful login, CAS will redirect the user's browser
+            back to the original service. It will also include a
+            <literal>ticket</literal> parameter, which is an opaque string
+            representing the "service ticket". Continuing our earlier example,
+            the URL the browser is redirected to might be
+            <literal>https://server3.company.com/webapp/j_acegi_cas_security_check?ticket=ST-0-ER94xMJmn6pha35CQRoZ</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>Back in the service web application, the
+            <literal>CasProcessingFilter</literal> is always listening for
+            requests to <literal>/j_acegi_cas_security_check</literal> (this
+            is configurable, but we'll use the defaults in this introduction).
+            The processing filter will construct a
+            <literal>UsernamePasswordAuthenticationToken</literal>
+            representing the service ticket. The principal will be equal to
+            <literal>CasProcessingFilter.CAS_STATEFUL_IDENTIFIER</literal>,
+            whilst the credentials will be the service ticket opaque value.
+            This authentication request will then be handed to the configured
+            <literal>AuthenticationManager</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>The AuthenticationManager implementation will be the
+            <literal>ProviderManager</literal>, which is in turn configured
+            with the <literal>CasAuthenticationProvider</literal>. The
+            <literal>CasAuthenticationProvider</literal> only responds to
+            <literal>UsernamePasswordAuthenticationToken</literal>s containing
+            the CAS-specific principal (such as
+            <literal>CasProcessingFilter.CAS_STATEFUL_IDENTIFIER</literal>)
+            and <literal>CasAuthenticationToken</literal>s (discussed
+            later).</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>CasAuthenticationProvider</literal> will validate
+            the service ticket using a <literal>TicketValidator</literal>
+            implementation. Acegi Security includes one implementation, the
+            <literal>CasProxyTicketValidator</literal>. This implementation
+            uses a CAS-supplied ticket validator. The
+            <literal>CasProxyTicketValidator</literal> makes a HTTPS request
+            to the CAS server in order to validate the service ticket. The
+            <literal>CasProxyTicketValidator</literal> may also include a
+            proxy callback parameter, which is included in this example:
+            <literal>https://my.company.com/cas/proxyValidate?service=https://server3.company.com/webapp/j_acegi_cas_security_check&amp;ticket=ST-0-ER94xMJmn6pha35CQRoZ&amp;pgtUrl=https://server3.company.com/webapp/casProxy/receptor</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>Back of the CAS server, the proxy validation request will be
+            received. If the presented service ticket matches the service URL
+            requested initially, CAS will provide an affirmative response in
+            XML indicating the username. If any proxy was involved in the
+            authentication (discussed below), the list of proxies is also
+            included in the XML response.</para>
+          </listitem>
+
+          <listitem>
+            <para>[OPTIONAL] If the request to the CAS validation service
+            included the <literal>pgtUrl</literal>, CAS will include a
+            <literal>pgtIou</literal> string in the XML response. This
+            <literal>pgtIou</literal> represents a proxy-granting ticket IOU.
+            The CAS server will then create its own HTTPS connection back to
+            the <literal>pgtUrl</literal>. This is to mutually authenticate
+            the CAS server and the claimed service. The HTTPS connection will
+            be used to send a proxy granting ticket to the original web
+            application. For example,
+            <literal>https://server3.company.com/webapp/casProxy/receptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&amp;pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH</literal>.
+            We suggest you use CAS' <literal>ProxyTicketReceptor</literal>
+            servlet to receive these proxy-granting tickets, if they are
+            required.</para>
+          </listitem>
+
+          <listitem>
+            <para>The <literal>CasProxyTicketValidator</literal> will parse
+            the XML received from the CAS server. It will return to the
+            <literal>CasAuthenticationProvider</literal> a
+            <literal>TicketResponse</literal>, which includes the username
+            (mandatory), proxy list (if any were involved), and proxy-granting
+            ticket IOU (if the proxy callback was requested).</para>
+          </listitem>
+
+          <listitem>
+            <para>Next <literal>CasAuthenticationProvider</literal> will call
+            a configured <literal>CasProxyDecider</literal>. The
+            <literal>CasProxyDecider</literal> indicates whether the proxy
+            list in the <literal>TicketResponse</literal> is acceptable to the
+            service. Several implementations are provided with the Acegi
+            Security System: <literal>RejectProxyTickets</literal>,
+            <literal>AcceptAnyCasProxy</literal> and
+            <literal>NamedCasProxyDecider</literal>. These names are largely
+            self-explanatory, except <literal>NamedCasProxyDecider</literal>
+            which allows a <literal>List</literal> of trusted proxies to be
+            provided.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>CasAuthenticationProvider</literal> will next
+            request a <literal>CasAuthoritiesPopulator</literal> to advise the
+            <literal>GrantedAuthority</literal> objects that apply to the user
+            contained in the <literal>TicketResponse</literal>. Acegi Security
+            includes a <literal>DaoCasAuthoritiesPopulator</literal> which
+            simply uses the <literal>AuthenticationDao</literal>
+            infrastructure to find the <literal>User</literal> and their
+            associated <literal>GrantedAuthority</literal>s. Note that the
+            password and enabled/disabled status of <literal>User</literal>s
+            returned by the <literal>AuthenticationDao</literal> are ignored,
+            as the CAS server is responsible for authentication decisions.
+            <literal>DaoCasAuthoritiesPopulator</literal> is only concerned
+            with retrieving the <literal>GrantedAuthority</literal>s.</para>
+          </listitem>
+
+          <listitem>
+            <para>If there were no problems,
+            <literal>CasAuthenticationProvider</literal> constructs a
+            <literal>CasAuthenticationToken</literal> including the details
+            contained in the <literal>TicketResponse</literal> and the
+            <literal>GrantedAuthority</literal>s. The
+            <literal>CasAuthenticationToken</literal> contains the hash of a
+            key, so that the <literal>CasAuthenticationProvider</literal>
+            knows it created it.</para>
+          </listitem>
+
+          <listitem>
+            <para>Control then returns to
+            <literal>CasProcessingFilter</literal>, which places the created
+            <literal>CasAuthenticationToken</literal> into the
+            <literal>HttpSession</literal> attribute named
+            <literal>HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>The user's browser is redirected to the original page that
+            caused the <literal>AuthenticationException</literal>.</para>
+          </listitem>
+
+          <listitem>
+            <para>As the <literal>Authentication</literal> object is now in
+            the well-known location, it is handled like any other
+            authentication approach. Usually the
+            <literal>AutoIntegrationFilter</literal> will be used to associate
+            the <literal>Authentication</literal> object with the
+            <literal>ContextHolder</literal> for the duration of each
+            request.</para>
+          </listitem>
+        </orderedlist>
+
+        <para>It's good that you're still here! It might sound involved, but
+        you can relax as the Acegi Security System for Spring classes hide
+        much of the complexity. Let's now look at how this is
+        configured.</para>
+      </sect2>
+
+      <sect2 id="security-cas-install-server">
+        <title>CAS Server Installation (Optional)</title>
+
+        <para>As mentioned above, the Acegi Security System for Spring
+        includes a <literal>PasswordHandler</literal> that bridges your
+        existing <literal>AuthenticationManager</literal> into CAS. You do not
+        need to use this <literal>PasswordHandler</literal> to use Acegi
+        Security on the client side (any CAS
+        <literal>PasswordHandler</literal> will do).</para>
+
+        <para>To install, you will need to download and extract the CAS server
+        archive. We used version 2.0.12 Beta 3. There will be a
+        <literal>/web</literal> directory in the root of the deployment. Copy
+        an <literal>applicationContext.xml</literal> containing your
+        <literal>AuthenticationManager</literal> as well as the
+        <literal>CasPasswordHandler</literal> into the
+        <literal>/web/WEB-INF</literal> directory. A sample
+        <literal>applicationContext.xml</literal> is included below:</para>
+
+        <programlisting>&lt;bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl"&gt;
+  &lt;property name="userMap"&gt;
+    &lt;value&gt;
+      marissa=koala,ROLES_IGNORED_BY_CAS
+      dianne=emu,ROLES_IGNORED_BY_CAS
+      scott=wombat,ROLES_IGNORED_BY_CAS
+      peter=opal,disabled,ROLES_IGNORED_BY_CAS
+    &lt;/value&gt;
+  &lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"&gt;
+  &lt;property name="authenticationDao"&gt;&lt;ref bean="inMemoryDaoImpl"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"&gt;
+  &lt;property name="providers"&gt;
+    &lt;list&gt;
+      &lt;ref bean="daoAuthenticationProvider"/&gt;
+    &lt;/list&gt;
+  &lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casPasswordHandler" class="net.sf.acegisecurity.adapters.cas.CasPasswordHandler"&gt;
+  &lt;property name="authenticationManager"&gt;&lt;ref bean="authenticationManager"/&gt;&lt;/property&gt;
+&lt;/bean&gt;</programlisting>
+
+        <para>Note the granted authorities are ignored by CAS. It has no way
+        of communciating the granted authorities to calling applications. CAS
+        is only concerned with username and passwords.</para>
+
+        <para>Next you will need to edit the existing
+        <literal>/web/WEB-INF/web.xml</literal> file. Add (or edit in the case
+        of the <literal>authHandler</literal> property) the following
+        lines:</para>
+
+        <para><programlisting>&lt;context-param&gt;
+  &lt;param-name&gt;edu.yale.its.tp.cas.authHandler&lt;/param-name&gt;
+  &lt;param-value&gt;net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy&lt;/param-value&gt;
+&lt;/context-param&gt;
+
+&lt;context-param&gt;
+  &lt;param-name&gt;contextConfigLocation&lt;/param-name&gt;
+  &lt;param-value&gt;/WEB-INF/applicationContext.xml&lt;/param-value&gt;
+&lt;/context-param&gt;
+
+&lt;listener&gt;
+  &lt;listener-class&gt;org.springframework.web.context.ContextLoaderListener&lt;/listener-class&gt;
+&lt;/listener&gt;</programlisting></para>
+
+        <para>Copy the <literal>spring.jar</literal> and
+        <literal>acegi-security.jar</literal> files into
+        <literal>/web/WEB-INF/lib</literal>. Now use the <literal>ant
+        dist</literal> task in the <literal>build.xml</literal> in the root of
+        the directory structure. This will create
+        <literal>/lib/cas.war</literal>, which is ready for deployment to your
+        servlet container.</para>
+      </sect2>
+
+      <sect2 id="security-cas-install-client">
+        <title>CAS Acegi Security System Client Installation</title>
+
+        <para>The web application side of CAS is made easy due to the Acegi
+        Security System for Spring. It is assumed you already know the basics
+        of using the Acegi Security System for Spring, so these are not
+        covered again below. Only the CAS-specific beans are mentioned.</para>
+
+        <para>You will need to add a <literal>ServiceProperties</literal> bean
+        to your application context. This represents your service:</para>
+
+        <para><programlisting>&lt;bean id="serviceProperties" class="net.sf.acegisecurity.ui.cas.ServiceProperties"&gt;
+  &lt;property name="service"&gt;&lt;value&gt;https://localhost:8443/contacts-cas/j_acegi_cas_security_check&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="sendRenew"&gt;&lt;value&gt;false&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;</programlisting></para>
+
+        <para>The <literal>service</literal> must equal a URL that will be
+        monitored by the <literal>CasProcessingFilter</literal>. The
+        <literal>sendRenew</literal> defaults to false, but should be set to
+        true if your application is particularly sensitive. What this
+        parameter does is tell the CAS login service that a single sign on
+        login is unacceptable. Instead, the user will need to re-enter their
+        username and password in order to gain access to the service.</para>
+
+        <para>The following beans should be configured to commence the CAS
+        authentication process:</para>
+
+        <para><programlisting>&lt;bean id="casProcessingFilter" class="net.sf.acegisecurity.ui.cas.CasProcessingFilter"&gt;
+  &lt;property name="authenticationManager"&gt;&lt;ref bean="authenticationManager"/&gt;&lt;/property&gt;
+  &lt;property name="authenticationFailureUrl"&gt;&lt;value&gt;/casfailed.jsp&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="defaultTargetUrl"&gt;&lt;value&gt;/&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="filterProcessesUrl"&gt;&lt;value&gt;/j_acegi_cas_security_check&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"&gt;
+  &lt;property name="filterSecurityInterceptor"&gt;&lt;ref bean="filterInvocationInterceptor"/&gt;&lt;/property&gt;
+  &lt;property name="authenticationEntryPoint"&gt;&lt;ref bean="casProcessingFilterEntryPoint"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.cas.CasProcessingFilterEntryPoint"&gt;
+  &lt;property name="loginUrl"&gt;&lt;value&gt;https://localhost:8443/cas/login&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="serviceProperties"&gt;&lt;ref bean="serviceProperties"/&gt;&lt;/property&gt;
+&lt;/bean&gt;</programlisting></para>
+
+        <para>You will also need to add the
+        <literal>CasProcessingFilter</literal> to web.xml:</para>
+
+        <para><programlisting>&lt;filter&gt;
+  &lt;filter-name&gt;Acegi CAS Processing Filter&lt;/filter-name&gt;
+  &lt;filter-class&gt;net.sf.acegisecurity.util.FilterToBeanProxy&lt;/filter-class&gt;
+  &lt;init-param&gt;
+    &lt;param-name&gt;targetClass&lt;/param-name&gt;
+    &lt;param-value&gt;net.sf.acegisecurity.ui.cas.CasProcessingFilter&lt;/param-value&gt;
+  &lt;/init-param&gt;
+&lt;/filter&gt;
+
+&lt;filter-mapping&gt;
+  &lt;filter-name&gt;Acegi CAS Processing Filter&lt;/filter-name&gt;
+  &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+&lt;/filter-mapping&gt;</programlisting></para>
+
+        <para>The <literal>CasProcessingFilter</literal> has very similar
+        properties to the <literal>AuthenticationProcessingFilter</literal>
+        (used for form-based logins). Each property is
+        self-explanatory.</para>
+
+        <para>For CAS to operate, the
+        <literal>SecurityEnforcementFilter</literal> must have its
+        <literal>authenticationEntryPoint</literal> property set to the
+        <literal>CasProcessingFilterEntryPoint</literal> bean. </para>
+
+        <para>The <literal>CasProcessingFilterEntryPoint</literal> must refer
+        to the <literal>ServiceProperties</literal> bean (discussed above) and
+        provide the URL to the enterprise's CAS login server. This is where
+        the user's browser will be redirected.</para>
+
+        <para>Next you need to add an <literal>AuthenticationManager</literal>
+        that uses <literal>CasAuthenticationProvider</literal> and its
+        collaborators:</para>
+
+        <para><programlisting>&lt;bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"&gt;
+  &lt;property name="providers"&gt;
+    &lt;list&gt;
+      &lt;ref bean="casAuthenticationProvider"/&gt;
+    &lt;/list&gt;
+  &lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casAuthenticationProvider" class="net.sf.acegisecurity.providers.cas.CasAuthenticationProvider"&gt;
+  &lt;property name="casAuthoritiesPopulator"&gt;&lt;ref bean="casAuthoritiesPopulator"/&gt;&lt;/property&gt;
+  &lt;property name="casProxyDecider"&gt;&lt;ref bean="casProxyDecider"/&gt;&lt;/property&gt;
+  &lt;property name="ticketValidator"&gt;&lt;ref bean="casProxyTicketValidator"/&gt;&lt;/property&gt;
+  &lt;property name="statelessTicketCache"&gt;&lt;ref bean="statelessTicketCache"/&gt;&lt;/property&gt;
+  &lt;property name="key"&gt;&lt;value&gt;my_password_for_this_auth_provider_only&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casProxyTicketValidator" class="net.sf.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator"&gt;
+  &lt;property name="casValidate"&gt;&lt;value&gt;https://localhost:8443/cas/proxyValidate&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="proxyCallbackUrl"&gt;&lt;value&gt;https://localhost:8443/contacts-cas/casProxy/receptor&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="serviceProperties"&gt;&lt;ref bean="serviceProperties"/&gt;&lt;/property&gt;
+  &lt;!-- &lt;property name="trustStore"&gt;&lt;value&gt;/some/path/to/your/lib/security/cacerts&lt;/value&gt;&lt;/property&gt; --&gt;
+&lt;/bean&gt;
+
+&lt;bean id="statelessTicketCache" class="net.sf.acegisecurity.providers.cas.cache.EhCacheBasedTicketCache"&gt;
+  &lt;property name="minutesToIdle"&gt;&lt;value&gt;20&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casAuthoritiesPopulator" class="net.sf.acegisecurity.providers.cas.populator.DaoCasAuthoritiesPopulator"&gt;
+  &lt;property name="authenticationDao"&gt;&lt;ref bean="inMemoryDaoImpl"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="casProxyDecider" class="net.sf.acegisecurity.providers.cas.proxy.RejectProxyTickets"/&gt;</programlisting></para>
+
+        <para>The beans are all reasonable self-explanatory if you refer back
+        to the "How CAS Works" section. Careful readers might notice one
+        surprise: the <literal>statelessTicketCache</literal> property of the
+        <literal>CasAuthenticationProvider</literal>. This is discussed in
+        detail in the "Advanced CAS Usage" section.</para>
+
+        <para>Note the <literal>CasProxyTicketValidator</literal> has a
+        remarked out <literal>trustStore</literal> property. This property
+        might be helpful if you experience HTTPS certificate issues. Also note
+        the <literal>proxyCallbackUrl</literal> is set so the service can
+        receive a proxy-granting ticket. As mentioned above, this is optional
+        and unnecessary if you do not require proxy-granting tickets. If you
+        do use this feature, you will need to configure a suitable servlet to
+        receive the proxy-granting tickets. We suggest you use CAS'
+        <literal>ProxyTicketReceptor</literal> by adding the following to your
+        web application's <literal>web.xml</literal>:</para>
+
+        <para><programlisting>&lt;servlet&gt;
+  &lt;servlet-name&gt;casproxy&lt;/servlet-name&gt;
+  &lt;servlet-class&gt;edu.yale.its.tp.cas.proxy.ProxyTicketReceptor&lt;/servlet-class&gt;
+&lt;/servlet&gt;
+
+&lt;servlet-mapping&gt;
+  &lt;servlet-name&gt;casproxy&lt;/servlet-name&gt;
+  &lt;url-pattern&gt;/casProxy/*&lt;/url-pattern&gt;
+&lt;/servlet-mapping&gt;</programlisting></para>
+
+        <para>This completes the configuration of CAS. If you haven't made any
+        mistakes, your web application should happily work within the
+        framework of CAS single sign on. No other parts of the Acegi Security
+        System for Spring need to be concerned about the fact CAS handled
+        authentication.</para>
+
+        <para>There is also a <literal>contacts-cas.war</literal> file in the
+        sample applications directory. This sample application uses the above
+        settings and can be deployed to see CAS in operation.</para>
+      </sect2>
+
+      <sect2 id="security-cas-advanced-usage">
+        <title>Advanced CAS Usage</title>
+
+        <para>[DRAFT - COMMENTS WELCOME]</para>
+
+        <para>The <literal>CasAuthenticationProvider</literal> distinguishes
+        between stateful and stateless clients. A stateful client is
+        considered any that originates via the
+        <literal>CasProcessingFilter</literal>. A stateless client is any that
+        presents an authentication request via the
+        <literal>UsernamePasswordAuthenticationToken</literal> with a
+        principal equal to
+        <literal>CasProcessingFilter.CAS_STATELESS_IDENTIFIER</literal>.</para>
+
+        <para>Stateless clients are likely to be via remoting protocols such
+        as Hessian and Burlap. The <literal>BasicProcessingFilter</literal> is
+        still used in this case, but the remoting protocol client is expected
+        to present a username equal to the static string above, and a password
+        equal to a CAS service ticket. Clients should acquire a CAS service
+        ticket directly from the CAS server.</para>
+
+        <para>Because remoting protocols have no way of presenting themselves
+        within the context of a <literal>HttpSession</literal>, it isn't
+        possible to rely on the <literal>HttpSession</literal>'s
+        <literal>HttpSessionIntegrationFilter.ACEGI_SECURITY_AUTHENTICATION_KEY</literal>
+        attribute to locate the CasAuthenticationToken. Furthermore, because
+        the CAS server invalidates a service ticket after it has been
+        validated by the TicketValidator, presenting the same service ticket
+        on subsequent requests will not work. It is similarly very difficult
+        to obtain a proxy-granting ticket for a remoting protocol client, as
+        they are often operational on client machines which do not have HTTPS
+        certificates that would be trusted by the CAS server.</para>
+
+        <para>One obvious option is to not use CAS at all for remoting
+        protocol clients. However, this would eliminate many of the desirable
+        features of CAS.</para>
+
+        <para>As a middle-ground, the CasAuthenticationProvider uses a
+        StatelessTicketCache. This is used solely for requests with a
+        principal equal to
+        <literal>CasProcessingFilter.CAS_STATELESS_IDENTIFIER</literal>. What
+        happens is the CasAuthenticationProvider will store the resulting
+        CasAuthenticationToken in the StatelessTicketCache, keyed on the
+        service ticket. Accordingly, remoting protocol clients can present the
+        same service ticket and the CasAuthenticationProvider will not need to
+        contact the CAS server for validation.</para>
+
+        <para>The other aspect of advanced CAS usage involves creating proxy
+        tickets from the proxy-granting ticket. As indicated above, we
+        recommend you use CAS' <literal>ProxyTicketReceptor</literal> to
+        receive these tickets. The <literal>ProxyTicketReceptor</literal>
+        provides a static method that enables you to obtain a proxy ticket by
+        presenting the proxy-granting IOU ticket. You can obtain the
+        proxy-granting IOU ticket by calling
+        <literal>CasAuthenticationToken.getProxyGrantingTicketIou()</literal>.</para>
+
+        <para>It is hoped you find CAS integration easy and useful with the
+        Acegi Security System for Spring classes. Welcome to enterprise-wide
+        single sign on!</para>
+      </sect2>
+    </sect1>
+
     <sect1 id="security-sample">
       <title>Contacts Sample Application</title>
 

+ 29 - 0
lib/cas/LICENSE

@@ -0,0 +1,29 @@
+   Copyright (c) 2000-2003 Yale University. All rights reserved.
+
+   THIS SOFTWARE IS PROVIDED "AS IS," AND ANY EXPRESS OR IMPLIED
+   WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE EXPRESSLY
+   DISCLAIMED. IN NO EVENT SHALL YALE UNIVERSITY OR ITS EMPLOYEES BE
+   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED, THE COSTS OF
+   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR
+   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+   SOFTWARE, EVEN IF ADVISED IN ADVANCE OF THE POSSIBILITY OF SUCH
+   DAMAGE.
+
+   Redistribution and use of this software in source or binary forms,
+   with or without modification, are permitted, provided that the
+   following conditions are met:
+
+   1. Any redistribution must include the above copyright notice and
+   disclaimer and this list of conditions in any related documentation
+   and, if feasible, in the redistributed software.
+
+   2. Any redistribution must include the acknowledgment, "This product
+   includes software developed by Yale University," in any related
+   documentation and, if feasible, in the redistributed software.
+
+   3. The names "Yale" and "Yale University" must not be used to endorse
+   or promote products derived from this software.

+ 2 - 0
lib/cas/version.txt

@@ -0,0 +1,2 @@
+CAS Server 2.0.12 beta 3
+CAS Client 2.0.10

+ 58 - 0
lib/ehcache/LICENSE.txt

@@ -0,0 +1,58 @@
+/*
+ * $Id$
+ *
+ * ====================================================================
+ *
+ * The Apache Software License, Version 1.1
+ *
+ * Copyright (c) 1999-2003 The Apache Software Foundation.  All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The end-user documentation included with the redistribution, if
+ *    any, must include the following acknowlegement:
+ *       "This product includes software developed by the
+ *        Apache Software Foundation (http://www.apache.org/)."
+ *    Alternately, this acknowlegement may appear in the software itself,
+ *    if and wherever such third-party acknowlegements normally appear.
+ *
+ * 4. The names "The Jakarta Project", "Commons", and "Apache Software
+ *    Foundation" must not be used to endorse or promote products derived
+ *    from this software without prior written permission. For written
+ *    permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache"
+ *    nor may "Apache" appear in their names without prior written
+ *    permission of the Apache Group.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */

+ 1 - 0
lib/ehcache/version.txt

@@ -0,0 +1 @@
+EHCACHE 0.7

+ 52 - 0
samples/contacts/build.xml

@@ -156,6 +156,58 @@
 			<lib dir="${dist.lib.dir}" includes="acegi-security.jar"/>
 		</war>
 
+		<!-- Make WAR suitable for deployment into containers using Yale CAS -->
+        <delete file="${dist.dir}/${name.cas}.war"/>
+        <mkdir dir="${tmp.dir}/${name.cas}"/>
+		<copy todir="${tmp.dir}/${name.cas}">
+			<fileset dir="${war.dir}">
+				<include name="**"/>
+			</fileset>
+			<fileset dir="${etc.dir}/cas">
+				<include name="casfailed.jsp"/>
+			</fileset>
+		</copy>
+		<copy todir="${tmp.dir}/${name.cas}/classes">
+			<fileset dir="${build.dir}">
+				<include name="**"/>
+			</fileset>
+		</copy>
+		<copy todir="${tmp.dir}/${name.cas}/WEB-INF">
+			<fileset dir="${etc.dir}/cas">
+				<include name="web.xml"/>
+				<include name="applicationContext.xml"/>
+			</fileset>
+		</copy>
+		<war warfile="${dist.dir}/${name.cas}.war" webxml="${tmp.dir}/${name.cas}/WEB-INF/web.xml">
+			<!-- Include the files which are not under WEB-INF -->
+			<fileset dir="${tmp.dir}/${name.cas}">
+				<exclude name="WEB-INF/**"/>
+				<exclude name="classes/**"/>
+			</fileset>
+
+			<!-- Bring in various XML configuration files -->
+			<webinf dir="${tmp.dir}/${name.cas}/WEB-INF">
+				<exclude name="web.xml"/>
+			</webinf>
+
+			<!-- Include the compiled classes -->
+			<classes dir="${tmp.dir}/${name.cas}/classes"/>
+
+			<!-- Include required libraries -->
+			<lib dir="${lib.dir}/jakarta-taglibs" includes="*.jar"/>
+			<lib dir="${lib.dir}/spring" includes="spring.jar"/>
+			<lib dir="${lib.dir}/aop-alliance" includes="aopalliance.jar"/>
+			<lib dir="${lib.dir}/regexp" includes="jakarta-oro.jar"/>
+			<lib dir="${lib.dir}/j2ee" includes="jstl.jar"/>
+			<lib dir="${lib.dir}/caucho" includes="*.jar"/>
+			<lib dir="${lib.dir}/jakarta-commons" includes="commons-codec.jar"/>
+			<lib dir="${lib.dir}/jakarta-commons" includes="commons-collections.jar"/>
+			<lib dir="${lib.dir}/cas" includes="*.jar"/>
+			<lib dir="${lib.dir}/ehcache" includes="ehcache.jar"/>
+			<lib dir="${dist.lib.dir}" includes="acegi-security-taglib.jar"/>
+			<lib dir="${dist.lib.dir}" includes="acegi-security.jar"/>
+		</war>
+
 		<!-- Make WAR suitable for deployment into containers WITH container adapters -->
         <delete file="${dist.dir}/${name.ca}.war"/>
         <mkdir dir="${tmp.dir}/${name.ca}"/>

+ 229 - 0
samples/contacts/etc/cas/applicationContext.xml

@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+
+<!--
+  - Application context loaded by ContextLoaderListener if NOT using container adapters
+  - $Id$
+  -->
+
+<beans>
+
+	<!-- =================== SECURITY SYSTEM DEFINITIONS ================== -->
+	
+	<!-- RunAsManager -->
+	<bean id="runAsManager" class="net.sf.acegisecurity.runas.RunAsManagerImpl">
+     	<property name="key"><value>my_run_as_password</value></property>
+ 	</bean>
+
+	<!-- ~~~~~~~~~~~~~~~~~~~~ AUTHENTICATION DEFINITIONS ~~~~~~~~~~~~~~~~~~ -->
+	
+	<bean id="runAsAuthenticationProvider" class="net.sf.acegisecurity.runas.RunAsImplAuthenticationProvider">
+     	<property name="key"><value>my_run_as_password</value></property>
+ 	</bean>
+
+	<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
+		<property name="providers">
+		  <list>
+		    <ref bean="runAsAuthenticationProvider"/>
+		    <ref bean="casAuthenticationProvider"/>
+		  </list>
+		</property>
+	</bean>
+
+	<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl">
+  		<property name="userMap">
+			<value>
+				marissa=PASSWORD_NOT_USED,ROLE_TELLER,ROLE_SUPERVISOR
+				dianne=PASSWORD_NOT_USED,ROLE_TELLER
+				scott=PASSWORD_NOT_USED,ROLE_TELLER
+				peter=PASSWORD_NOT_USED_AND_DISABLED_IGNORED,disabled,ROLE_TELLER
+			</value>
+		</property>
+	</bean>
+	
+	<bean id="basicProcessingFilter" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter">
+		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+		<property name="authenticationEntryPoint"><ref bean="basicProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="basicProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint">
+		<property name="realmName"><value>Contacts Realm</value></property>
+	</bean>
+
+	<bean id="casAuthenticationProvider" class="net.sf.acegisecurity.providers.cas.CasAuthenticationProvider">
+		<property name="casAuthoritiesPopulator"><ref bean="casAuthoritiesPopulator"/></property>
+		<property name="casProxyDecider"><ref bean="casProxyDecider"/></property>
+		<property name="ticketValidator"><ref bean="casProxyTicketValidator"/></property>
+		<property name="statelessTicketCache"><ref bean="statelessTicketCache"/></property>
+		<property name="key"><value>my_password_for_this_auth_provider_only</value></property>
+	</bean>
+
+	<bean id="casProxyTicketValidator" class="net.sf.acegisecurity.providers.cas.ticketvalidator.CasProxyTicketValidator">
+		<property name="casValidate"><value>https://localhost:8443/cas/proxyValidate</value></property>
+		<property name="proxyCallbackUrl"><value>https://localhost:8443/contacts-cas/casProxy/receptor</value></property>
+		<property name="serviceProperties"><ref bean="serviceProperties"/></property>
+        <!-- <property name="trustStore"><value>/some/path/to/your/lib/security/cacerts</value></property> -->
+	</bean>
+
+	<bean id="statelessTicketCache" class="net.sf.acegisecurity.providers.cas.cache.EhCacheBasedTicketCache">
+		<property name="minutesToIdle"><value>20</value></property>
+	</bean>
+
+	<bean id="casAuthoritiesPopulator" class="net.sf.acegisecurity.providers.cas.populator.DaoCasAuthoritiesPopulator">
+		<property name="authenticationDao"><ref bean="inMemoryDaoImpl"/></property>
+	</bean>
+
+	<bean id="casProxyDecider" class="net.sf.acegisecurity.providers.cas.proxy.RejectProxyTickets">
+	</bean>
+
+	<bean id="serviceProperties" class="net.sf.acegisecurity.ui.cas.ServiceProperties">
+		<property name="service"><value>https://localhost:8443/contacts-cas/j_acegi_cas_security_check</value></property>
+		<property name="sendRenew"><value>false</value></property>
+	</bean>
+
+	<!-- ~~~~~~~~~~~~~~~~~~~~ AUTHORIZATION DEFINITIONS ~~~~~~~~~~~~~~~~~~~ -->
+
+	<!-- An access decision voter that reads ROLE_* configuaration settings -->
+	<bean id="roleVoter" class="net.sf.acegisecurity.vote.RoleVoter"/>
+
+	<!-- An access decision voter that reads CONTACT_OWNED_BY_CURRENT_USER configuaration settings -->
+	<bean id="contactSecurityVoter" class="sample.contact.ContactSecurityVoter"/>
+
+	<!-- An access decision manager used by the business objects -->
+	<bean id="businessAccessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased">
+   		<property name="allowIfAllAbstainDecisions"><value>false</value></property>
+		<property name="decisionVoters">
+		  <list>
+		    <ref bean="roleVoter"/>
+		    <ref bean="contactSecurityVoter"/>
+		  </list>
+		</property>
+	</bean>
+
+	<!-- ===================== SECURITY DEFINITIONS ======================= -->
+	
+	<bean id="publicContactManagerSecurity" class="net.sf.acegisecurity.intercept.method.MethodSecurityInterceptor">
+    	<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+    	<property name="accessDecisionManager"><ref bean="businessAccessDecisionManager"/></property>
+    	<property name="runAsManager"><ref bean="runAsManager"/></property>
+ 		<property name="objectDefinitionSource">
+			<value>
+				sample.contact.ContactManager.delete=ROLE_SUPERVISOR,RUN_AS_SERVER
+				sample.contact.ContactManager.getAllByOwner=CONTACT_OWNED_BY_CURRENT_USER,RUN_AS_SERVER
+				sample.contact.ContactManager.save=CONTACT_OWNED_BY_CURRENT_USER,RUN_AS_SERVER
+				sample.contact.ContactManager.getById=ROLE_TELLER,RUN_AS_SERVER
+			</value>
+		</property>
+	</bean>
+
+	<!-- We expect all callers of the backend object to hold the role ROLE_RUN_AS_SERVER -->
+	<bean id="backendContactManagerSecurity" class="net.sf.acegisecurity.intercept.method.MethodSecurityInterceptor">
+    	<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+    	<property name="accessDecisionManager"><ref bean="businessAccessDecisionManager"/></property>
+    	<property name="runAsManager"><ref bean="runAsManager"/></property>
+ 		<property name="objectDefinitionSource">
+			<value>
+				sample.contact.ContactManager.delete=ROLE_RUN_AS_SERVER
+				sample.contact.ContactManager.getAllByOwner=ROLE_RUN_AS_SERVER
+				sample.contact.ContactManager.save=ROLE_RUN_AS_SERVER
+				sample.contact.ContactManager.getById=ROLE_RUN_AS_SERVER
+			</value>
+		</property>
+	</bean>
+
+	<!-- ======================= BUSINESS DEFINITIONS ===================== -->
+
+	<bean id="contactManager" class="org.springframework.aop.framework.ProxyFactoryBean">
+    	<property name="proxyInterfaces"><value>sample.contact.ContactManager</value></property>
+	    <property name="interceptorNames">
+      	<list>
+        	<value>publicContactManagerSecurity</value>
+ 	        <value>publicContactManagerTarget</value>
+    	</list>
+	    </property>
+  	</bean>
+
+	<bean id="publicContactManagerTarget" class="sample.contact.ContactManagerFacade">
+    	<property name="backend"><ref bean="backendContactManager"/></property>
+	</bean>
+
+	<bean id="backendContactManager" class="org.springframework.aop.framework.ProxyFactoryBean">
+    	<property name="proxyInterfaces"><value>sample.contact.ContactManager</value></property>
+	    <property name="interceptorNames">
+      	<list>
+        	<value>backendContactManagerSecurity</value>
+ 	        <value>backendContactManagerTarget</value>
+    	</list>
+	    </property>
+  	</bean>
+
+	<bean id="backendContactManagerTarget" class="sample.contact.ContactManagerBackend"/>
+
+	<!-- ===================== HTTP REQUEST SECURITY ==================== -->
+
+	<bean id="casProcessingFilter" class="net.sf.acegisecurity.ui.cas.CasProcessingFilter">
+		<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+		<property name="authenticationFailureUrl"><value>/casfailed.jsp</value></property>
+		<property name="defaultTargetUrl"><value>/</value></property>
+		<property name="filterProcessesUrl"><value>/j_acegi_cas_security_check</value></property>
+	</bean>
+
+	<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter">
+		<property name="filterSecurityInterceptor"><ref bean="filterInvocationInterceptor"/></property>
+		<property name="authenticationEntryPoint"><ref bean="casProcessingFilterEntryPoint"/></property>
+	</bean>
+
+	<bean id="casProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.cas.CasProcessingFilterEntryPoint">
+		<property name="loginUrl"><value>https://localhost:8443/cas/login</value></property>
+		<property name="serviceProperties"><ref bean="serviceProperties"/></property>
+	</bean>
+
+	<bean id="httpRequestAccessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased">
+   		<property name="allowIfAllAbstainDecisions"><value>false</value></property>
+		<property name="decisionVoters">
+		  <list>
+		    <ref bean="roleVoter"/>
+		  </list>
+		</property>
+	</bean>
+
+	<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
+	     The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
+	     Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
+	<bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor">
+    	<property name="authenticationManager"><ref bean="authenticationManager"/></property>
+    	<property name="accessDecisionManager"><ref bean="httpRequestAccessDecisionManager"/></property>
+    	<property name="runAsManager"><ref bean="runAsManager"/></property>
+ 		<property name="objectDefinitionSource">
+			<value>
+			    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
+				\A/secure/super.*\Z=ROLE_WE_DONT_HAVE
+				\A/secure/.*\Z=ROLE_SUPERVISOR,ROLE_TELLER
+			</value>
+		</property>
+	</bean>
+	
+	<!-- BASIC Regular Expression Syntax (for beginners):
+	     
+	     \A means the start of the string (ie the beginning of the URL)
+	     \Z means the end of the string (ie the end of the URL)
+	     .  means any single character
+	     *  means null or any number of repetitions of the last expression (so .* means zero or more characters)
+	     
+	     Some examples:
+	     
+	     Expression:   \A/my/directory/.*\Z
+	     Would match:    /my/directory/
+	                     /my/directory/hello.html
+	     
+	     Expression:   \A/.*\Z
+	     Would match:    /hello.html
+	                     /
+	     
+	     Expression:   \A/.*/secret.html\Z
+	     Would match:    /some/directory/secret.html
+	                     /another/secret.html
+	     Not match:      /anothersecret.html (missing required /)
+	-->
+
+</beans>

+ 20 - 0
samples/contacts/etc/cas/casfailed.jsp

@@ -0,0 +1,20 @@
+<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core' %>
+<%@ page import="net.sf.acegisecurity.ui.AbstractProcessingFilter" %>
+<%@ page import="net.sf.acegisecurity.AuthenticationException" %>
+<%-- This page will be copied into WAR's root directory if using CAS --%>
+
+<html>
+  <head>
+    <title>Login to CAS failed!</title>
+  </head>
+
+  <body>
+    <h1>Login to CAS failed!</h1>
+
+      <font color="red">
+        Your CAS credentials were rejected.<BR><BR>
+        Reason: <%= ((AuthenticationException) session.getAttribute(AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY)).getMessage() %>
+      </font>
+
+  </body>
+</html>

+ 160 - 0
samples/contacts/etc/cas/web.xml

@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'>
+
+<!--
+  - Contacts web application
+  - $Id$
+  - File will be copied into WAR's WEB-INF directory if NOT using container adapter
+  -->
+
+<web-app>
+
+    <display-name>Contacts Sample Application</display-name>
+    
+	<description>
+    	Example of an application secured using Acegi Security System for Spring.
+    </description>
+
+	<!-- Required for CAS ProxyTicketReceptor servlet. This is the
+	     URL to CAS' "proxy" actuator, where a PGT and TargetService can
+	     be presented to obtain a new proxy ticket. THIS CAN BE
+	     REMOVED IF THE APPLICATION DOESN'T NEED TO ACT AS A PROXY -->
+    <context-param>
+        <param-name>edu.yale.its.tp.cas.proxyUrl</param-name>
+        <param-value>http://localhost:8433/cas/proxy</param-value>
+    </context-param>
+
+	<!--
+	  - Location of the XML file that defines the root application context
+	  - Applied by ContextLoaderListener.
+	  -->
+	<context-param>
+		<param-name>contextConfigLocation</param-name>
+		<param-value>/WEB-INF/applicationContext.xml</param-value>
+	</context-param>
+
+    <filter>
+        <filter-name>Acegi CAS Processing Filter</filter-name>
+        <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
+        <init-param>
+            <param-name>targetClass</param-name>
+            <param-value>net.sf.acegisecurity.ui.cas.CasProcessingFilter</param-value>
+        </init-param>
+    </filter>
+
+    <filter>
+        <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
+        <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
+        <init-param>
+            <param-name>targetClass</param-name>
+            <param-value>net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter</param-value>
+        </init-param>
+    </filter>
+
+    <filter>
+        <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name>
+        <filter-class>net.sf.acegisecurity.ui.AutoIntegrationFilter</filter-class>
+    </filter>
+
+    <filter>
+        <filter-name>Acegi HTTP Request Security Filter</filter-name>
+        <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
+        <init-param>
+            <param-name>targetClass</param-name>
+            <param-value>net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter</param-value>
+        </init-param>
+    </filter>
+
+    <filter-mapping>
+      <filter-name>Acegi CAS Processing Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+    
+    <filter-mapping>
+      <filter-name>Acegi HTTP Request Security Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+	<!--
+	  - Loads the root application context of this web app at startup,
+	  - by default from "/WEB-INF/applicationContext.xml".
+	  - Use WebApplicationContextUtils.getWebApplicationContext(servletContext)
+	  - to access it anywhere in the web application, outside of the framework.
+    -->
+	<listener>
+		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
+	</listener>
+
+  <!--
+    - Servlet that dispatches request to registered handlers (Controller implementations).
+    - Has its own application context, by default defined in "{servlet-name}-servlet.xml",
+    - i.e. "contacts-servlet.xml".
+    -
+    - A web app can contain any number of such servlets.
+    - Note that this web app does not have a shared root application context,
+    - therefore the DispatcherServlet contexts do not have a common parent.
+    -->
+	<servlet>
+		<servlet-name>contacts</servlet-name>
+		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+		<load-on-startup>1</load-on-startup>
+	</servlet>
+
+	<servlet>
+		<servlet-name>caucho</servlet-name>
+		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+		<load-on-startup>2</load-on-startup>
+	</servlet>
+	
+	<!-- CAS servlet which receives a proxy-granting ticket from the CAS
+	     server. THIS CAN BE REMOVED IF THE APPLICATION DOESN'T NEED TO 
+	     ACT AS A PROXY -->
+	<servlet>
+		<servlet-name>casproxy</servlet-name>
+		<servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class>
+		<load-on-startup>3</load-on-startup>
+	</servlet>
+
+  <!--
+    - Maps the contacts dispatcher to /*.
+    -
+   -->
+	<servlet-mapping>
+    	<servlet-name>contacts</servlet-name>
+    	<url-pattern>*.htm</url-pattern>
+ 	</servlet-mapping>
+  
+  <!--
+	- Dispatcher servlet mapping for HTTP remoting via the Caucho protocols,
+	- i.e. Hessian and Burlap (see caucho-servlet.xml for the controllers).
+   -->
+	<servlet-mapping>
+		<servlet-name>caucho</servlet-name>
+		<url-pattern>/caucho/*</url-pattern>
+	</servlet-mapping>
+
+	<servlet-mapping>
+		<servlet-name>casproxy</servlet-name>
+		<url-pattern>/casProxy/*</url-pattern>
+	</servlet-mapping>
+
+ 	<welcome-file-list>
+		<welcome-file>index.jsp</welcome-file>
+	</welcome-file-list>
+
+  	<taglib>
+      <taglib-uri>/spring</taglib-uri>
+      <taglib-location>/WEB-INF/spring.tld</taglib-location>
+  	</taglib>
+  
+</web-app>

+ 15 - 0
samples/contacts/etc/ssl/acegisecurity.txt

@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIIC1DCCAj0CBECA2vQwDQYJKoZIhvcNAQEEBQAwgbAxEDAOBgNVBAYTB1Vua25vd24xEDAOBgNV
+BAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xOTA3BgNVBAoTMFRFU1QgQ0VSVElGSUNBVEUg
+T05MWS4gRE8gTk9UIFVTRSBJTiBQUk9EVUNUSU9OLjEpMCcGA1UECxMgQWNlZ2kgU2VjdXJpdHkg
+U3lzdGVtIGZvciBTcHJpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0wNDA0MTcwNzIxMjRaFw0z
+MTA5MDIwNzIxMjRaMIGwMRAwDgYDVQQGEwdVbmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYD
+VQQHEwdVbmtub3duMTkwNwYDVQQKEzBURVNUIENFUlRJRklDQVRFIE9OTFkuIERPIE5PVCBVU0Ug
+SU4gUFJPRFVDVElPTi4xKTAnBgNVBAsTIEFjZWdpIFNlY3VyaXR5IFN5c3RlbSBmb3IgU3ByaW5n
+MRIwEAYDVQQDEwlsb2NhbGhvc3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANwJCiDthTNC
+SJZ87CYkhWWDBciaFRvuDldzgEGwEUF5gNczd8Er66Pvh+Ir350hjE4LsDfi5iQNOuhbRR37LvW5
+7CrKG3W+vq7K3Zr9JEqhP/U2ocPLQQF4/NbBKStRacGGY1O3koTqp9W8gE0vSlC3/KhoOoPWHkGh
+NZXOxuwLAgMBAAEwDQYJKoZIhvcNAQEEBQADgYEAdTlsziREdIR00/+tufCq/ACHSo2nJr1yRzIi
+cVOXJBws0f+M3TUSIaODFv/54bZnWtjlWGa55uhc425+LrkOtqus7cMoNnyte/C6g/gcnArkKVhL
+C68LGqARe9DK1ycquA4KmgiKyhi9a54kKA6BC4bmmEI98HpB6uxxvOB+ChE=
+-----END CERTIFICATE-----

+ 82 - 0
samples/contacts/etc/ssl/howto.txt

@@ -0,0 +1,82 @@
+$Id$
+
+CAS requires HTTPS be used for all operations, with the certificate used
+having been signed by a certificate in the cacerts files shipped with Java.
+
+If you're using a HTTPS certificate signed by a well known authority
+(like Verisign), you can safely ignore the procedure below (although you
+might find the troubleshooting section at the end helpful).
+
+The following demonstrates how to create a self-signed certificate and add
+it to the cacerts file. If you just want to use the certificate we have
+already created and shipped with the Acegi Security System for Spring, you
+can skip directly to step 3.
+
+
+1. keytool -keystore keystore -alias acegisecurity -genkey -keyalg RSA -validity 9999 -storepass password -keypass password
+
+What is your first and last name?
+  [Unknown]:  localhost
+What is the name of your organizational unit?
+  [Unknown]:  Acegi Security System for Spring
+What is the name of your organization?
+  [Unknown]:  TEST CERTIFICATE ONLY. DO NOT USE IN PRODUCTION.
+What is the name of your City or Locality?
+  [Unknown]:
+What is the name of your State or Province?
+  [Unknown]:
+What is the two-letter country code for this unit?
+  [Unknown]:
+Is CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ONLY. D
+O NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown correct?
+  [no]:  yes
+
+
+2. keytool -export -v -rfc -alias acegisecurity -file acegisecurity.txt -keystore keystore -storepass password
+
+3. copy acegisecurity.txt %JAVA_HOME%\lib\security
+   
+4. copy keystore %YOUR_WEB_CONTAINER_LOCATION%
+
+   NOTE: You will need to configure your web container as appropriate.
+   We recommend you test the certificate works by visiting
+   https://localhost:8443. When prompted by your browser, select to
+   install the certificate.
+
+5. cd %JAVA_HOME%\lib\security
+
+6. keytool -import -v -file acegisecurity.txt -keypass password -keystore cacerts -storepass changeit -alias acegisecurity
+
+Owner: CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ONL
+Y. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown
+Issuer: CN=localhost, OU=Acegi Security System for Spring, O=TEST CERTIFICATE ON
+LY. DO NOT USE IN PRODUCTION., L=Unknown, ST=Unknown, C=Unknown
+Serial number: 4080daf4
+Valid from: Sat Apr 17 07:21:24 GMT 2004 until: Tue Sep 02 07:21:24 GMT 2031
+Certificate fingerprints:
+         MD5:  B4:AC:A8:24:34:99:F1:A9:F8:1D:A5:6C:BF:0A:34:FA
+         SHA1: F1:E6:B1:3A:01:39:2D:CF:06:FA:82:AB:86:0D:77:9D:06:93:D6:B0
+Trust this certificate? [no]:  yes
+Certificate was added to keystore
+[Saving cacerts]
+
+
+7. Finished. You can now run the sample application as if you purchased a
+   properly signed certificate. For production applications, of course you should
+   use an appropriately signed certificate so your web visitors will trust it
+   (such as issued by Thawte, Verisign etc).
+
+TROUBLESHOOTING
+
+* A "sun.security.validator.ValidatorException: No trusted certificate 
+  found" indicates the cacerts is not being used or it did not correctly
+  import the certificate. To rule out your web container replacing or in
+  some way modifying the trust manager, set the
+  CasAuthenticationProvider.trustStore property to the full file system
+  location to your cacerts file.
+
+* If your web container is ignoring your cacerts file, double-check it
+  is stored in $JAVA_HOME\lib\security\cacerts. $JAVA_HOME might be
+  pointing to the SDK, not JRE. In that case, copy
+  $JAVA_HOME\jre\lib\security\cacerts to $JAVA_HOME\lib\security\cacerts
+

BIN
samples/contacts/etc/ssl/keystore


+ 1 - 0
samples/contacts/project.properties

@@ -3,6 +3,7 @@
 
 name.filter=contacts
 name.ca=contacts-container-adapter
+name.cas=contacts-cas
 src.dir=src
 war.dir=war
 lib.dir=${basedir}/../../lib