Browse Source

Initial CAS support.

Ben Alex 21 years ago
parent
commit
fa9b872570
60 changed files with 5561 additions and 17 deletions
  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/regexp/jakarta-oro.jar"/>
 	<classpathentry kind="lib" path="lib/jakarta-commons/commons-codec.jar"/>
 	<classpathentry kind="lib" path="lib/jakarta-commons/commons-codec.jar"/>
 	<classpathentry kind="lib" path="lib/hsqldb/hsqldb.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"/>
 	<classpathentry kind="output" path="target/eclipseclasses"/>
 </classpath>
 </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;
 package net.sf.acegisecurity.providers.dao;
 
 
+import java.io.Serializable;
+
 import net.sf.acegisecurity.GrantedAuthority;
 import net.sf.acegisecurity.GrantedAuthority;
 
 
 
 
@@ -24,7 +26,7 @@ import net.sf.acegisecurity.GrantedAuthority;
  * @author Ben Alex
  * @author Ben Alex
  * @version $Id$
  * @version $Id$
  */
  */
-public class User {
+public class User implements Serializable {
     //~ Instance fields ========================================================
     //~ Instance fields ========================================================
 
 
     private String password;
     private String password;
@@ -53,9 +55,10 @@ public class User {
      */
      */
     public User(String username, String password, boolean enabled,
     public User(String username, String password, boolean enabled,
         GrantedAuthority[] authorities) throws IllegalArgumentException {
         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(
             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++) {
         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 {
 public class MockHttpServletRequest implements HttpServletRequest {
     //~ Instance fields ========================================================
     //~ Instance fields ========================================================
 
 
-    private HttpSession session;
+    private HttpSession session = new MockHttpSession();
     private Map headersMap = new HashMap();
     private Map headersMap = new HashMap();
     private Map paramMap = new HashMap();
     private Map paramMap = new HashMap();
     private Principal principal;
     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
         handling each request. Handling involves a number of
         operations:</para>
         operations:</para>
 
 
-        <itemizedlist spacing="compact">
+        <orderedlist>
           <listitem>
           <listitem>
             <para>Store the configuration attributes that are associated with
             <para>Store the configuration attributes that are associated with
             each secure request.</para>
             each secure request.</para>
@@ -354,7 +354,7 @@
             <para>Return any result received from the
             <para>Return any result received from the
             <literal>SecurityInterceptorCallback</literal>.</para>
             <literal>SecurityInterceptorCallback</literal>.</para>
           </listitem>
           </listitem>
-        </itemizedlist>
+        </orderedlist>
 
 
         <para>Whilst this may seem quite involved, don't worry. Developers
         <para>Whilst this may seem quite involved, don't worry. Developers
         interact with the security process by simply implementing basic
         interact with the security process by simply implementing basic
@@ -854,6 +854,13 @@
               <literal>AuthenticationProvider</literal> if you were not using
               <literal>AuthenticationProvider</literal> if you were not using
               container adapters.</para>
               container adapters.</para>
             </listitem>
             </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>
           </itemizedlist></para>
       </sect2>
       </sect2>
 
 
@@ -870,8 +877,26 @@
 
 
         <para><programlisting>&lt;bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"&gt;
         <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="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>
 &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
         <para>For a class to be able to provide the
         <literal>DaoAuthenticationProvider</literal> with access to an
         <literal>DaoAuthenticationProvider</literal> with access to an
         authentication repository, it must implement the
         authentication repository, it must implement the
@@ -957,10 +982,13 @@
         <para>You can use different relational database management systems by
         <para>You can use different relational database management systems by
         modifying the <literal>DriverManagerDataSource</literal> shown above.
         modifying the <literal>DriverManagerDataSource</literal> shown above.
         Irrespective of the database used, a standard schema must be used as
         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
         <para>The Acegi Security System for Spring ships with a Hypersonic SQL
         instance that has the required authentication information and sample
         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
         <literal>HttpSession</literal> object and filters to authenticate the
         user. Another approach is HTTP Basic Authentication, which allows
         user. Another approach is HTTP Basic Authentication, which allows
         clients to use HTTP headers to present authentication information to
         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>
 
 
       <sect2 id="security-ui-http-session">
       <sect2 id="security-ui-http-session">
@@ -1538,7 +1568,7 @@ public boolean supports(Class clazz);</programlisting></para>
         <literal>authenticationFailureUrl</literal>. The
         <literal>authenticationFailureUrl</literal>. The
         <literal>AuthenticationException</literal> will be placed into the
         <literal>AuthenticationException</literal> will be placed into the
         <literal>HttpSession</literal> attribute indicated by
         <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>
         enabling a reason to be provided to the user on the error page.</para>
 
 
         <para>If authentication is successful, the resulting
         <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
         browser will need to be redirected to the target URL. The target URL
         is usually indicated by the <literal>HttpSession</literal> attribute
         is usually indicated by the <literal>HttpSession</literal> attribute
         specified by
         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
         This attribute is automatically set by the
         <literal>SecurityEnforcementFilter</literal> when an
         <literal>SecurityEnforcementFilter</literal> when an
         <literal>AuthenticationException</literal> occurs, so that after login
         <literal>AuthenticationException</literal> occurs, so that after login
@@ -2100,6 +2130,569 @@ $CATALINA_HOME/bin/startup.sh</programlisting></para>
       </sect2>
       </sect2>
     </sect1>
     </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">
     <sect1 id="security-sample">
       <title>Contacts Sample Application</title>
       <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"/>
 			<lib dir="${dist.lib.dir}" includes="acegi-security.jar"/>
 		</war>
 		</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 -->
 		<!-- Make WAR suitable for deployment into containers WITH container adapters -->
         <delete file="${dist.dir}/${name.ca}.war"/>
         <delete file="${dist.dir}/${name.ca}.war"/>
         <mkdir dir="${tmp.dir}/${name.ca}"/>
         <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.filter=contacts
 name.ca=contacts-container-adapter
 name.ca=contacts-container-adapter
+name.cas=contacts-cas
 src.dir=src
 src.dir=src
 war.dir=war
 war.dir=war
 lib.dir=${basedir}/../../lib
 lib.dir=${basedir}/../../lib