Explorar o código

Added remember-me services.

Ben Alex %!s(int64=20) %!d(string=hai) anos
pai
achega
f1e071b0f1
Modificáronse 28 ficheiros con 2098 adicións e 21 borrados
  1. 2 1
      core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java
  2. 86 0
      core/src/main/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationProvider.java
  3. 139 0
      core/src/main/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationToken.java
  4. 5 0
      core/src/main/java/org/acegisecurity/providers/rememberme/package.html
  5. 22 2
      core/src/main/java/org/acegisecurity/ui/AbstractProcessingFilter.java
  6. 47 0
      core/src/main/java/org/acegisecurity/ui/rememberme/NullRememberMeServices.java
  7. 131 0
      core/src/main/java/org/acegisecurity/ui/rememberme/RememberMeProcessingFilter.java
  8. 115 0
      core/src/main/java/org/acegisecurity/ui/rememberme/RememberMeServices.java
  9. 352 0
      core/src/main/java/org/acegisecurity/ui/rememberme/TokenBasedRememberMeServices.java
  10. 5 0
      core/src/main/java/org/acegisecurity/ui/rememberme/package.html
  11. 15 1
      core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java
  12. 11 1
      core/src/test/java/org/acegisecurity/MockHttpServletRequest.java
  13. 7 2
      core/src/test/java/org/acegisecurity/MockHttpServletResponse.java
  14. 122 0
      core/src/test/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationProviderTests.java
  15. 190 0
      core/src/test/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationTokenTests.java
  16. 6 0
      core/src/test/java/org/acegisecurity/ui/AbstractProcessingFilterTests.java
  17. 51 0
      core/src/test/java/org/acegisecurity/ui/rememberme/NullRememberMeServicesTests.java
  18. 223 0
      core/src/test/java/org/acegisecurity/ui/rememberme/RememberMeProcessingFilterTests.java
  19. 412 0
      core/src/test/java/org/acegisecurity/ui/rememberme/TokenBasedRememberMeServicesTests.java
  20. 114 1
      doc/docbook/acegi.xml
  21. 9 7
      doc/xdocs/changes.xml
  22. 1 0
      samples/contacts/src/main/resources/log4j.properties
  23. 5 0
      samples/contacts/src/main/webapp/common/WEB-INF/jsp/hello.jsp
  24. 1 1
      samples/contacts/src/main/webapp/common/WEB-INF/jsp/index.jsp
  25. 7 1
      samples/contacts/src/main/webapp/common/logoff.jsp
  26. 17 2
      samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml
  27. 2 2
      samples/contacts/src/main/webapp/filter/WEB-INF/web.xml
  28. 1 0
      samples/contacts/src/main/webapp/filter/acegilogin.jsp

+ 2 - 1
core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java

@@ -16,6 +16,7 @@
 package net.sf.acegisecurity;
 
 import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
+import net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
 
 
 /**
@@ -39,7 +40,7 @@ public class AuthenticationTrustResolverImpl
     //~ Instance fields ========================================================
 
     private Class anonymousClass = AnonymousAuthenticationToken.class;
-    private Class rememberMeClass;
+    private Class rememberMeClass = RememberMeAuthenticationToken.class;
 
     //~ Methods ================================================================
 

+ 86 - 0
core/src/main/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationProvider.java

@@ -0,0 +1,86 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.providers.AuthenticationProvider;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.Assert;
+
+
+/**
+ * An {@link AuthenticationProvider} implementation that validates {@link
+ * net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken}s.
+ * 
+ * <p>
+ * To be successfully validated, the  {@link{@link
+ * net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken#getKeyHash()}
+ * must match this class' {@link #getKey()}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RememberMeAuthenticationProvider implements AuthenticationProvider,
+    InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(RememberMeAuthenticationProvider.class);
+
+    //~ Instance fields ========================================================
+
+    private String key;
+
+    //~ Methods ================================================================
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.hasLength(key);
+    }
+
+    public Authentication authenticate(Authentication authentication)
+        throws AuthenticationException {
+        if (!supports(authentication.getClass())) {
+            return null;
+        }
+
+        if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication)
+            .getKeyHash()) {
+            throw new BadCredentialsException(
+                "The presented RememberMeAuthenticationToken does not contain the expected key");
+        }
+
+        return authentication;
+    }
+
+    public boolean supports(Class authentication) {
+        return (RememberMeAuthenticationToken.class.isAssignableFrom(authentication));
+    }
+}

+ 139 - 0
core/src/main/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationToken.java

@@ -0,0 +1,139 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.providers.AbstractAuthenticationToken;
+
+import java.io.Serializable;
+
+
+/**
+ * Represents a remembered <code>Authentication</code>.
+ * 
+ * <p>
+ * A remembered <code>Authentication</code> must provide a fully valid
+ * <code>Authentication</code>, including the <code>GrantedAuthority</code>[]s
+ * that apply.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RememberMeAuthenticationToken extends AbstractAuthenticationToken
+    implements Serializable {
+    //~ Instance fields ========================================================
+
+    private Object principal;
+    private GrantedAuthority[] authorities;
+    private int keyHash;
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructor.
+     *
+     * @param key to identify if this object made by an authorised client
+     * @param principal the principal (typically a <code>UserDetails</code>)
+     * @param authorities the authorities granted to the principal
+     *
+     * @throws IllegalArgumentException if a <code>null</code> was passed
+     */
+    public RememberMeAuthenticationToken(String key, Object principal,
+        GrantedAuthority[] authorities) {
+        if ((key == null) || ("".equals(key)) || (principal == null)
+            || "".equals(principal) || (authorities == null)
+            || (authorities.length == 0)) {
+            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.authorities = authorities;
+    }
+
+    protected RememberMeAuthenticationToken() {
+        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;
+    }
+
+    /**
+     * Always returns an empty <code>String</code>
+     *
+     * @return an empty String
+     */
+    public Object getCredentials() {
+        return "";
+    }
+
+    public int getKeyHash() {
+        return this.keyHash;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+
+        if (obj instanceof RememberMeAuthenticationToken) {
+            RememberMeAuthenticationToken test = (RememberMeAuthenticationToken) obj;
+
+            if (this.getKeyHash() != test.getKeyHash()) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+}

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

@@ -0,0 +1,5 @@
+<html>
+<body>
+Authentication provider that processes <code>RememberMeAuthenticationToken</code>s.
+</body>
+</html>

+ 22 - 2
core/src/main/java/org/acegisecurity/ui/AbstractProcessingFilter.java

@@ -26,12 +26,16 @@ import net.sf.acegisecurity.context.ContextHolder;
 import net.sf.acegisecurity.context.security.SecureContext;
 import net.sf.acegisecurity.context.security.SecureContextUtils;
 import net.sf.acegisecurity.providers.cas.ProxyUntrustedException;
+import net.sf.acegisecurity.ui.rememberme.NullRememberMeServices;
+import net.sf.acegisecurity.ui.rememberme.RememberMeServices;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
 import org.springframework.beans.factory.InitializingBean;
 
+import org.springframework.util.Assert;
+
 import java.io.IOException;
 
 import javax.servlet.Filter;
@@ -106,6 +110,7 @@ public abstract class AbstractProcessingFilter implements Filter,
     //~ Instance fields ========================================================
 
     private AuthenticationManager authenticationManager;
+    private RememberMeServices rememberMeServices = new NullRememberMeServices();
 
     /**
      * Where to redirect the browser if authentication fails due to incorrect
@@ -194,6 +199,14 @@ public abstract class AbstractProcessingFilter implements Filter,
      */
     public abstract String getDefaultFilterProcessesUrl();
 
+    public void setRememberMeServices(RememberMeServices rememberMeServices) {
+        this.rememberMeServices = rememberMeServices;
+    }
+
+    public RememberMeServices getRememberMeServices() {
+        return rememberMeServices;
+    }
+
     /**
      * Performs actual authentication.
      *
@@ -306,6 +319,8 @@ public abstract class AbstractProcessingFilter implements Filter,
             throw new IllegalArgumentException(
                 "authenticationManager must be specified");
         }
+
+        Assert.notNull(this.rememberMeServices);
     }
 
     /**
@@ -370,7 +385,8 @@ public abstract class AbstractProcessingFilter implements Filter,
         HttpServletResponse response) throws IOException {}
 
     protected void onSuccessfulAuthentication(HttpServletRequest request,
-        HttpServletResponse response) throws IOException {}
+        HttpServletResponse response, Authentication authResult)
+        throws IOException {}
 
     protected void onUnsuccessfulAuthentication(HttpServletRequest request,
         HttpServletResponse response) throws IOException {}
@@ -429,7 +445,9 @@ public abstract class AbstractProcessingFilter implements Filter,
                 + targetUrl);
         }
 
-        onSuccessfulAuthentication(request, response);
+        onSuccessfulAuthentication(request, response, authResult);
+
+        rememberMeServices.loginSuccess(request, response, authResult);
 
         response.sendRedirect(response.encodeRedirectURL(targetUrl));
     }
@@ -481,6 +499,8 @@ public abstract class AbstractProcessingFilter implements Filter,
 
         onUnsuccessfulAuthentication(request, response);
 
+        rememberMeServices.loginFail(request, response);
+
         response.sendRedirect(response.encodeRedirectURL(request.getContextPath()
                 + failureUrl));
     }

+ 47 - 0
core/src/main/java/org/acegisecurity/ui/rememberme/NullRememberMeServices.java

@@ -0,0 +1,47 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import net.sf.acegisecurity.Authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Implementation of {@link NullRememberMeServices} that does nothing.
+ * 
+ * <p>
+ * Used as a default by several framework classes.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NullRememberMeServices implements RememberMeServices {
+    //~ Methods ================================================================
+
+    public Authentication autoLogin(HttpServletRequest request,
+        HttpServletResponse response) {
+        return null;
+    }
+
+    public void loginFail(HttpServletRequest request,
+        HttpServletResponse response) {}
+
+    public void loginSuccess(HttpServletRequest request,
+        HttpServletResponse response, Authentication successfulAuthentication) {}
+}

+ 131 - 0
core/src/main/java/org/acegisecurity/ui/rememberme/RememberMeProcessingFilter.java

@@ -0,0 +1,131 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+
+/**
+ * Detects if there is no <code>Authentication</code> object in the
+ * <code>ContextHolder</code>, and populates it with a remember-me
+ * authentication token if a {@link
+ * net.sf.acegisecurity.ui.rememberme.RememberMeServices} implementation so
+ * requests.
+ * 
+ * <p>
+ * Concrete <code>RememberMeServices</code> implementations will have their
+ * {@link
+ * net.sf.acegisecurity.ui.rememberme.RememberMeServices#autoLogin(HttpServletRequest,
+ * HttpServletResponse)} method called by this filter. The
+ * <code>Authentication</code> or <code>null</code> returned by that method
+ * will be placed into the <code>ContextHolder</code>.
+ * </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 RememberMeProcessingFilter implements Filter, InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(RememberMeProcessingFilter.class);
+
+    //~ Instance fields ========================================================
+
+    private RememberMeServices rememberMeServices = new NullRememberMeServices();
+
+    //~ Methods ================================================================
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(rememberMeServices);
+    }
+
+    /**
+     * Does nothing - we reply on IoC lifecycle services instead.
+     */
+    public void destroy() {}
+
+    public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain chain) throws IOException, ServletException {
+        if (!(request instanceof HttpServletRequest)) {
+            throw new ServletException("Can only process HttpServletRequest");
+        }
+
+        if (!(response instanceof HttpServletResponse)) {
+            throw new ServletException("Can only process HttpServletResponse");
+        }
+
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        SecureContext sc = SecureContextUtils.getSecureContext();
+
+        if (sc.getAuthentication() == null) {
+            sc.setAuthentication(rememberMeServices.autoLogin(httpRequest,
+                    httpResponse));
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Replaced ContextHolder with remember-me token: '"
+                    + sc.getAuthentication() + "'");
+            }
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug(
+                    "ContextHolder not replaced with remember-me token, as ContextHolder already contained: '"
+                    + sc.getAuthentication() + "'");
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    /**
+     * Does nothing - we reply on IoC lifecycle services instead.
+     *
+     * @param arg0 not used
+     *
+     * @throws ServletException not thrown
+     */
+    public void init(FilterConfig arg0) throws ServletException {}
+	public RememberMeServices getRememberMeServices() {
+		return rememberMeServices;
+	}
+	public void setRememberMeServices(RememberMeServices rememberMeServices) {
+		this.rememberMeServices = rememberMeServices;
+	}
+}

+ 115 - 0
core/src/main/java/org/acegisecurity/ui/rememberme/RememberMeServices.java

@@ -0,0 +1,115 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import net.sf.acegisecurity.Authentication;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Implement by a class that is capable of providing a remember-me service.
+ * 
+ * <P>
+ * Acegi Security filters (namely {@link
+ * net.sf.acegisecurity.ui.AbstractProcessingFilter} and {@link
+ * net.sf.acegisecurity.ui.rememberme.RememberMeProcessingFilter} will call
+ * the methods provided by an implementation of this interface.
+ * </p>
+ * 
+ * <P>
+ * Implementations may implement any type of remember-me capability they wish.
+ * Rolling cookies (as per <a
+ * href="http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice">http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice</a>)
+ * can be used, as can simple implementations that don't require a persistent
+ * store. Implementations also determine the validity period of a remember-me
+ * cookie.  This interface has been designed to accommodate any of these
+ * remember-me models.
+ * </p>
+ * 
+ * <p>
+ * This interface does not define how remember-me services should offer a
+ * "cancel all remember-me tokens" type capability, as this will be
+ * implementation specific and requires no hooks into Acegi Security.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface RememberMeServices {
+    //~ Methods ================================================================
+
+    /**
+     * This method will be called whenever the <code>ContextHolder</code> does
+     * not contain an <code>Authentication</code> and the Acegi Security
+     * system wishes to provide an implementation with an opportunity to
+     * authenticate the request using remember-me capabilities. Acegi Security
+     * makes no attempt whatsoever to determine whether the browser has
+     * requested remember-me services or presented a vaild cookie. Such
+     * determinations are left to the implementation. If a browser has
+     * presented an unauthorised cookie for whatever reason, it should be
+     * silently ignored and invalidated using the
+     * <code>HttpServletResponse</code> object.
+     * 
+     * <p>
+     * The returned <code>Authentication</code> must be acceptable to  {@link
+     * net.sf.acegisecurity.AuthenticationManager} or {@link
+     * net.sf.acegisecurity.providers.AuthenticationProvider} defined by the
+     * web application. It is recommended {@link
+     * net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken}
+     * be used in most cases, as it has a corresponding authentication
+     * provider.
+     * </p>
+     *
+     * @param request to look for a remember-me token within
+     * @param response to change, cancel or modify the remember-me token
+     *
+     * @return a valid authentication object, or <code>null</code> if the
+     *         request should not be authenticated
+     */
+    public Authentication autoLogin(HttpServletRequest request,
+        HttpServletResponse response);
+
+    /**
+     * Called whenever an interactive authentication attempt was made, but the
+     * credentials supplied by the user were missing or otherwise invalid.
+     * Implementations should invalidate any and all remember-me tokens
+     * indicated in the <code>HttpServletRequest</code>.
+     *
+     * @param request that contained an invalid authentication request
+     * @param response to change, cancel or modify the remember-me token
+     */
+    public void loginFail(HttpServletRequest request,
+        HttpServletResponse response);
+
+    /**
+     * Called whenever an interactive authentication attempt is successful. An
+     * implementation may automatically set a remember-me token in the
+     * <code>HttpServletResponse</code>, although this is not recommended.
+     * Instead, implementations should typically look for a request parameter
+     * that indicates the browser has presented an explicit request for
+     * authentication to be remembered, such as the presence of a HTTP POST
+     * parameter.
+     *
+     * @param request that contained the valid authentication request
+     * @param response to change, cancel or modify the remember-me token
+     * @param successfulAuthentication representing the successfully
+     *        authenticated principal
+     */
+    public void loginSuccess(HttpServletRequest request,
+        HttpServletResponse response, Authentication successfulAuthentication);
+}

+ 352 - 0
core/src/main/java/org/acegisecurity/ui/rememberme/TokenBasedRememberMeServices.java

@@ -0,0 +1,352 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
+import net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import org.springframework.web.bind.RequestUtils;
+
+import java.util.Date;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Identifies previously remembered users by a Base-64 encoded cookie.
+ * 
+ * <p>
+ * This implementation does not rely on an external database, so is attractive
+ * for simple applications. The cookie will be valid for a specific period
+ * from the date of the last {@link #loginSuccess(HttpServletRequest,
+ * HttpServletResponse, Authentication)}. As per the interface contract, this
+ * method will only be called when the principal completes a successful
+ * interactive authentication. As such the time period commences from the last
+ * authentication attempt where they furnished credentials - not the time
+ * period they last logged in via remember-me. The implementation will only
+ * send a remember-me token if the parameter defined by {@link
+ * #setParameter(String)} is present.
+ * </p>
+ * 
+ * <p>
+ * An {@link net.sf.acegisecurity.providers.dao.AuthenticationDao} is required
+ * by this implementation, so that it can construct a valid
+ * <code>Authentication</code> from the returned {@link
+ * net.sf.acegisecurity.UserDetails}. This is also necessary so that the
+ * user's password is available and can be checked as part of the encoded
+ * cookie.
+ * </p>
+ * 
+ * <p>
+ * The cookie encoded by this implementation adopts the following form:
+ * </p>
+ * 
+ * <p>
+ * <code> username + ":" + expiryTime + ":" + Md5Hex(username + ":" +
+ * expiryTime + ":" + password + ":" + key) </code>.
+ * </p>
+ * 
+ * <p>
+ * As such, if the user changes their password any remember-me token will be
+ * invalidated. Equally, the system administrator may invalidate every
+ * remember-me token on issue by changing the key. This provides some
+ * reasonable approaches to recovering from a remember-me token being left on
+ * a public machine (eg kiosk system, Internet cafe etc). Most importantly, at
+ * no time is the user's password ever sent to the user agent, providing an
+ * important security safeguard. Unfortunately the username is necessary in
+ * this implementation (as we do not want to rely on a database for
+ * remember-me services) and as such high security applications should be
+ * aware of this occasionally undesired disclosure of a valid username.
+ * </p>
+ * 
+ * <p>
+ * This is a basic remember-me implementation which is suitable for many
+ * applications. However, we recommend a database-based implementation if you
+ * require a more secure remember-me approach.
+ * </p>
+ * 
+ * <p>
+ * By default the tokens will be valid for 14 days from the last successful
+ * authentication attempt. This can be changed using {@link
+ * #setTokenValiditySeconds(int)}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class TokenBasedRememberMeServices implements RememberMeServices,
+    InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    public static final String ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY = "ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE";
+    public static final String DEFAULT_PARAMETER = "_acegi_security_remember_me";
+    protected static final Log logger = LogFactory.getLog(TokenBasedRememberMeServices.class);
+
+    //~ Instance fields ========================================================
+
+    private AuthenticationDao authenticationDao;
+    private String key;
+    private String parameter = DEFAULT_PARAMETER;
+    private int tokenValiditySeconds = 1209600; // 14 days
+
+    //~ Methods ================================================================
+
+    public void setAuthenticationDao(AuthenticationDao authenticationDao) {
+        this.authenticationDao = authenticationDao;
+    }
+
+    public AuthenticationDao getAuthenticationDao() {
+        return authenticationDao;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setParameter(String parameter) {
+        this.parameter = parameter;
+    }
+
+    public String getParameter() {
+        return parameter;
+    }
+
+    public void setTokenValiditySeconds(int tokenValiditySeconds) {
+        this.tokenValiditySeconds = tokenValiditySeconds;
+    }
+
+    public int getTokenValiditySeconds() {
+        return tokenValiditySeconds;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.hasLength(key);
+        Assert.hasLength(parameter);
+        Assert.notNull(authenticationDao);
+    }
+
+    public Authentication autoLogin(HttpServletRequest request,
+        HttpServletResponse response) {
+        Cookie[] cookies = request.getCookies();
+
+        if ((cookies == null) || (cookies.length == 0)) {
+            return null;
+        }
+
+        for (int i = 0; i < cookies.length; i++) {
+            if (ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY.equals(
+                    cookies[i].getName())) {
+                String cookieValue = cookies[i].getValue();
+
+                if (Base64.isArrayByteBase64(cookieValue.getBytes())) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Remember-me cookie detected");
+                    }
+
+                    // Decode token from Base64
+                    // format of token is:  
+                    //     username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
+                    String cookieAsPlainText = new String(Base64.decodeBase64(
+                                cookieValue.getBytes()));
+                    String[] cookieTokens = StringUtils
+                        .delimitedListToStringArray(cookieAsPlainText, ":");
+
+                    if (cookieTokens.length == 3) {
+                        long tokenExpiryTime;
+
+                        try {
+                            tokenExpiryTime = new Long(cookieTokens[1])
+                                .longValue();
+                        } catch (NumberFormatException nfe) {
+                            cancelCookie(request, response,
+                                "Cookie token[1] did not contain a valid number (contained '"
+                                + cookieTokens[1] + "')");
+
+                            return null;
+                        }
+
+                        // Check it has not expired
+                        if (tokenExpiryTime < System.currentTimeMillis()) {
+                            cancelCookie(request, response,
+                                "Cookie token[1] has expired (expired on '"
+                                + new Date(tokenExpiryTime)
+                                + "'; current time is '" + new Date() + "')");
+
+                            return null;
+                        }
+
+                        // Check the user exists
+                        // Defer lookup until after expiry time checked, to possibly avoid expensive lookup
+                        UserDetails userDetails;
+
+                        try {
+                            userDetails = this.authenticationDao
+                                .loadUserByUsername(cookieTokens[0]);
+                        } catch (UsernameNotFoundException notFound) {
+                            cancelCookie(request, response,
+                                "Cookie token[0] contained username '"
+                                + cookieTokens[0] + "' but was not found");
+
+                            return null;
+                        }
+
+                        // Check signature of token matches remaining details
+                        // Must do this after user lookup, as we need the DAO-derived password
+                        // If efficiency was a major issue, just add in a UserCache implementation,
+                        // but recall this method is usually only called one per HttpSession
+                        // (as if the token is valid, it will cause ContextHolder population, whilst
+                        // if invalid, will cause the cookie to be cancelled)
+                        String expectedTokenSignature = DigestUtils.md5Hex(userDetails
+                                .getUsername() + ":" + tokenExpiryTime + ":"
+                                + userDetails.getPassword() + ":" + this.key);
+
+                        if (!expectedTokenSignature.equals(cookieTokens[2])) {
+                            cancelCookie(request, response,
+                                "Cookie token[2] contained signature '"
+                                + cookieTokens[2] + "' but expected '"
+                                + expectedTokenSignature + "'");
+
+                            return null;
+                        }
+
+                        // By this stage we have a valid token
+                        if (logger.isDebugEnabled()) {
+                            logger.debug("Remember-me cookie accepted");
+                        }
+
+                        return new RememberMeAuthenticationToken(this.key,
+                            userDetails, userDetails.getAuthorities());
+                    } else {
+                        cancelCookie(request, response,
+                            "Cookie token did not contain 3 tokens; decoded value was '"
+                            + cookieAsPlainText + "'");
+
+                        return null;
+                    }
+                } else {
+                    cancelCookie(request, response,
+                        "Cookie token was not Base64 encoded; value was '"
+                        + cookieValue + "'");
+
+                    return null;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    public void loginFail(HttpServletRequest request,
+        HttpServletResponse response) {
+        cancelCookie(request, response,
+            "Interactive authentication attempt was unsuccessful");
+    }
+
+    public void loginSuccess(HttpServletRequest request,
+        HttpServletResponse response, Authentication successfulAuthentication) {
+        // Exit if the principal hasn't asked to be remembered
+        if (!RequestUtils.getBooleanParameter(request, parameter, false)) {
+            if (logger.isDebugEnabled()) {
+                logger.debug(
+                    "Did not send remember-me cookie (principal did not set parameter '"
+                    + this.parameter + "')");
+            }
+
+            return;
+        }
+
+        // Determine username and password, ensuring empty strings
+        Assert.notNull(successfulAuthentication.getPrincipal());
+        Assert.notNull(successfulAuthentication.getCredentials());
+
+        String username;
+        String password;
+
+        if (successfulAuthentication.getPrincipal() instanceof UserDetails) {
+            username = ((UserDetails) successfulAuthentication.getPrincipal())
+                .getUsername();
+            password = ((UserDetails) successfulAuthentication.getPrincipal())
+                .getPassword();
+        } else {
+            username = successfulAuthentication.getPrincipal().toString();
+            password = successfulAuthentication.getCredentials().toString();
+        }
+
+        Assert.hasLength(username);
+        Assert.hasLength(password);
+
+        long expiryTime = System.currentTimeMillis()
+            + (tokenValiditySeconds * 1000);
+
+        // construct token to put in cookie; format is:
+        //     username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
+        String signatureValue = new String(DigestUtils.md5Hex(username + ":"
+                    + expiryTime + ":" + password + ":" + key));
+        String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
+        String tokenValueBase64 = new String(Base64.encodeBase64(
+                    tokenValue.getBytes()));
+        response.addCookie(makeValidCookie(expiryTime, tokenValueBase64));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Added remember-me cookie for user '" + username
+                + "', expiry: '" + new Date(expiryTime) + "'");
+        }
+    }
+
+    protected Cookie makeCancelCookie() {
+        Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                null);
+        cookie.setMaxAge(0);
+
+        return cookie;
+    }
+
+    protected Cookie makeValidCookie(long expiryTime, String tokenValueBase64) {
+        Cookie cookie = new Cookie(ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                tokenValueBase64);
+        cookie.setMaxAge(60 * 60 * 24 * 365 * 5); // 5 years
+
+        return cookie;
+    }
+
+    private void cancelCookie(HttpServletRequest request,
+        HttpServletResponse response, String reasonForLog) {
+        if ((reasonForLog != null) && logger.isDebugEnabled()) {
+            logger.debug("Cancelling cookie for reason: " + reasonForLog);
+        }
+
+        response.addCookie(makeCancelCookie());
+    }
+}

+ 5 - 0
core/src/main/java/org/acegisecurity/ui/rememberme/package.html

@@ -0,0 +1,5 @@
+<html>
+<body>
+Support for remembering a user between different web sessions.
+</body>
+</html>

+ 15 - 1
core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java

@@ -19,6 +19,7 @@ import junit.framework.TestCase;
 
 import net.sf.acegisecurity.providers.TestingAuthenticationToken;
 import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
+import net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationToken;
 
 
 /**
@@ -54,6 +55,16 @@ public class AuthenticationTrustResolverImplTests extends TestCase {
                     new GrantedAuthority[] {new GrantedAuthorityImpl("ignored")})));
     }
 
+    public void testCorrectOperationIsRememberMe() {
+        AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
+        assertTrue(trustResolver.isRememberMe(
+                new RememberMeAuthenticationToken("ignored", "ignored",
+                    new GrantedAuthority[] {new GrantedAuthorityImpl("ignored")})));
+        assertFalse(trustResolver.isAnonymous(
+                new TestingAuthenticationToken("ignored", "ignored",
+                    new GrantedAuthority[] {new GrantedAuthorityImpl("ignored")})));
+    }
+
     public void testGettersSetters() {
         AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
 
@@ -62,6 +73,9 @@ public class AuthenticationTrustResolverImplTests extends TestCase {
         trustResolver.setAnonymousClass(String.class);
         assertEquals(String.class, trustResolver.getAnonymousClass());
 
-        assertNull(trustResolver.getRememberMeClass());
+        assertEquals(RememberMeAuthenticationToken.class,
+            trustResolver.getRememberMeClass());
+        trustResolver.setRememberMeClass(String.class);
+        assertEquals(String.class, trustResolver.getRememberMeClass());
     }
 }

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

@@ -54,6 +54,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
     private Map attribMap = new HashMap();
     private Map headersMap = new HashMap();
     private Map paramMap = new HashMap();
+    private Map cookiesMap = new HashMap();
     private Principal principal;
     private String contextPath = "";
     private String pathInfo; // null for no extra path
@@ -75,6 +76,15 @@ public class MockHttpServletRequest implements HttpServletRequest {
         this.queryString = queryString;
     }
 
+    public MockHttpServletRequest(Map headers, HttpSession session, String queryString, Cookie[] cookies) {
+    	this.queryString = queryString;
+        this.headersMap = headers;
+        this.session = session;
+        for (int i = 0; i < cookies.length; i++) {
+        	cookiesMap.put(cookies[i].getName(), cookies[i]);
+        }
+    }
+
     public MockHttpServletRequest(Map headers, Principal principal,
         HttpSession session) {
         this.headersMap = headers;
@@ -129,7 +139,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
     }
 
     public Cookie[] getCookies() {
-        throw new UnsupportedOperationException("mock method not implemented");
+        return (Cookie[]) cookiesMap.values().toArray(new Cookie[] {});
     }
 
     public long getDateHeader(String arg0) {

+ 7 - 2
core/src/test/java/org/acegisecurity/MockHttpServletResponse.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 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.
@@ -37,6 +37,7 @@ import javax.servlet.http.HttpServletResponse;
 public class MockHttpServletResponse implements HttpServletResponse {
     //~ Instance fields ========================================================
 
+    private Map cookiesMap = new HashMap();
     private Map headersMap = new HashMap();
     private String errorMessage;
     private String redirect;
@@ -72,6 +73,10 @@ public class MockHttpServletResponse implements HttpServletResponse {
         throw new UnsupportedOperationException("mock method not implemented");
     }
 
+    public Cookie getCookieByName(String name) {
+        return (Cookie) cookiesMap.get(name);
+    }
+
     public void setDateHeader(String arg0, long arg1) {
         throw new UnsupportedOperationException("mock method not implemented");
     }
@@ -131,7 +136,7 @@ public class MockHttpServletResponse implements HttpServletResponse {
     }
 
     public void addCookie(Cookie arg0) {
-        throw new UnsupportedOperationException("mock method not implemented");
+        cookiesMap.put(arg0.getName(), arg0);
     }
 
     public void addDateHeader(String arg0, long arg1) {

+ 122 - 0
core/src/test/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationProviderTests.java

@@ -0,0 +1,122 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+
+
+/**
+ * Tests {@link RememberMeAuthenticationProvider}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RememberMeAuthenticationProviderTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public RememberMeAuthenticationProviderTests() {
+        super();
+    }
+
+    public RememberMeAuthenticationProviderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(RememberMeAuthenticationProviderTests.class);
+    }
+
+    public void testDetectsAnInvalidKey() throws Exception {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("WRONG_KEY",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        try {
+            Authentication result = aap.authenticate(token);
+            fail("Should have thrown BadCredentialsException");
+        } catch (BadCredentialsException expected) {
+            assertEquals("The presented RememberMeAuthenticationToken does not contain the expected key",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingKey() throws Exception {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+
+        try {
+            aap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGettersSetters() throws Exception {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+        aap.setKey("qwerty");
+        aap.afterPropertiesSet();
+        assertEquals("qwerty", aap.getKey());
+    }
+
+    public void testIgnoresClassesItDoesNotSupport() throws Exception {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        TestingAuthenticationToken token = new TestingAuthenticationToken("user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        assertFalse(aap.supports(TestingAuthenticationToken.class));
+
+        // Try it anyway
+        assertNull(aap.authenticate(token));
+    }
+
+    public void testNormalOperation() throws Exception {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("qwerty",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        Authentication result = aap.authenticate(token);
+
+        assertEquals(result, token);
+    }
+
+    public void testSupports() {
+        RememberMeAuthenticationProvider aap = new RememberMeAuthenticationProvider();
+        assertTrue(aap.supports(RememberMeAuthenticationToken.class));
+        assertFalse(aap.supports(TestingAuthenticationToken.class));
+    }
+}

+ 190 - 0
core/src/test/java/org/acegisecurity/providers/rememberme/RememberMeAuthenticationTokenTests.java

@@ -0,0 +1,190 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+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 RememberMeAuthenticationToken}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RememberMeAuthenticationTokenTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public RememberMeAuthenticationTokenTests() {
+        super();
+    }
+
+    public RememberMeAuthenticationTokenTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(RememberMeAuthenticationTokenTests.class);
+    }
+
+    public void testConstructorRejectsNulls() {
+        try {
+            new RememberMeAuthenticationToken(null, "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new RememberMeAuthenticationToken("key", null,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new RememberMeAuthenticationToken("key", "Test", null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new RememberMeAuthenticationToken("key", "Test",
+                new GrantedAuthority[] {null});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new RememberMeAuthenticationToken("key", "Test",
+                new GrantedAuthority[] {});
+            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");
+
+        RememberMeAuthenticationToken token1 = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        RememberMeAuthenticationToken token2 = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertEquals(token1, token2);
+    }
+
+    public void testGetters() {
+        RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertEquals("key".hashCode(), token.getKeyHash());
+        assertEquals("Test", token.getPrincipal());
+        assertEquals("", token.getCredentials());
+        assertEquals("ROLE_ONE", token.getAuthorities()[0].getAuthority());
+        assertEquals("ROLE_TWO", token.getAuthorities()[1].getAuthority());
+        assertTrue(token.isAuthenticated());
+    }
+
+    public void testNoArgConstructor() {
+        try {
+            new RememberMeAuthenticationToken();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testNotEqualsDueToAbstractParentEqualsCheck() {
+        RememberMeAuthenticationToken token1 = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        RememberMeAuthenticationToken token2 = new RememberMeAuthenticationToken("key",
+                "DIFFERENT_PRINCIPAL",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToDifferentAuthenticationClass() {
+        RememberMeAuthenticationToken token1 = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test",
+                "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+        token2.setAuthenticated(true);
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToKey() {
+        RememberMeAuthenticationToken token1 = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        RememberMeAuthenticationToken token2 = new RememberMeAuthenticationToken("DIFFERENT_KEY",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testSetAuthenticatedIgnored() {
+        RememberMeAuthenticationToken token = new RememberMeAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+        assertTrue(token.isAuthenticated());
+        token.setAuthenticated(false); // ignored
+        assertTrue(token.isAuthenticated());
+    }
+}

+ 6 - 0
core/src/test/java/org/acegisecurity/ui/AbstractProcessingFilterTests.java

@@ -30,6 +30,7 @@ import net.sf.acegisecurity.context.ContextHolder;
 import net.sf.acegisecurity.context.security.SecureContextImpl;
 import net.sf.acegisecurity.context.security.SecureContextUtils;
 import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+import net.sf.acegisecurity.ui.rememberme.TokenBasedRememberMeServices;
 
 import java.io.IOException;
 
@@ -150,6 +151,11 @@ public class AbstractProcessingFilterTests extends TestCase {
 
     public void testGettersSetters() {
         AbstractProcessingFilter filter = new MockAbstractProcessingFilter();
+        assertNotNull(filter.getRememberMeServices());
+        filter.setRememberMeServices(new TokenBasedRememberMeServices());
+        assertEquals(TokenBasedRememberMeServices.class,
+            filter.getRememberMeServices().getClass());
+
         filter.setAuthenticationFailureUrl("/x");
         assertEquals("/x", filter.getAuthenticationFailureUrl());
 

+ 51 - 0
core/src/test/java/org/acegisecurity/ui/rememberme/NullRememberMeServicesTests.java

@@ -0,0 +1,51 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Tests {@link net.sf.acegisecurity.ui.rememberme.NullRememberMeServices}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class NullRememberMeServicesTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public NullRememberMeServicesTests() {
+        super();
+    }
+
+    public NullRememberMeServicesTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(NullRememberMeServicesTests.class);
+    }
+
+    public void testAlwaysReturnsNull() {
+    	NullRememberMeServices services = new NullRememberMeServices();
+    	assertNull(services.autoLogin(null,null));
+    	services.loginFail(null,null);
+    	services.loginSuccess(null,null,null);
+    	assertTrue(true);
+    }
+}

+ 223 - 0
core/src/test/java/org/acegisecurity/ui/rememberme/RememberMeProcessingFilterTests.java

@@ -0,0 +1,223 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.MockFilterConfig;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+import net.sf.acegisecurity.context.ContextHolder;
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextImpl;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+
+
+/**
+ * Tests {@link RememberMeProcessingFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RememberMeProcessingFilterTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public RememberMeProcessingFilterTests() {
+        super();
+    }
+
+    public RememberMeProcessingFilterTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(RememberMeProcessingFilterTests.class);
+    }
+    
+    public void testDoFilterWithNonHttpServletRequestDetected()
+    throws Exception {
+        RememberMeProcessingFilter filter = new RememberMeProcessingFilter();
+
+    try {
+        filter.doFilter(null, new MockHttpServletResponse(),
+            new MockFilterChain());
+        fail("Should have thrown ServletException");
+    } catch (ServletException expected) {
+        assertEquals("Can only process HttpServletRequest",
+            expected.getMessage());
+    }
+}
+
+    public void testDoFilterWithNonHttpServletResponseDetected()
+    throws Exception {
+        RememberMeProcessingFilter filter = new RememberMeProcessingFilter();
+
+    try {
+        filter.doFilter(new MockHttpServletRequest("dc"), null,
+            new MockFilterChain());
+        fail("Should have thrown ServletException");
+    } catch (ServletException expected) {
+        assertEquals("Can only process HttpServletResponse",
+            expected.getMessage());
+    }
+}
+
+    public void testDetectsRememberMeServicesProperty() throws Exception {
+        RememberMeProcessingFilter filter = new RememberMeProcessingFilter();
+        // check default is NullRememberMeServices
+        assertEquals(NullRememberMeServices.class, filter.getRememberMeServices().getClass());
+        
+        // check getter/setter
+        filter.setRememberMeServices(new TokenBasedRememberMeServices());
+        assertEquals(TokenBasedRememberMeServices.class, filter.getRememberMeServices().getClass());
+
+        // check detects if made null
+        filter.setRememberMeServices(null);
+        try {
+            filter.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testOperationWhenAuthenticationExistsInContextHolder()
+        throws Exception {
+        // Put an Authentication object into the ContextHolder
+        SecureContext sc = SecureContextUtils.getSecureContext();
+        Authentication originalAuth = new TestingAuthenticationToken("user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        sc.setAuthentication(originalAuth);
+        ContextHolder.setContext(sc);
+
+        // Setup our filter correctly
+    	Authentication remembered = new TestingAuthenticationToken("remembered",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_REMEMBERED")});
+        RememberMeProcessingFilter filter = new RememberMeProcessingFilter();
+        filter.setRememberMeServices(new MockRememberMeServices(remembered));
+        filter.afterPropertiesSet();
+
+        // Test
+        executeFilterInContainerSimulator(new MockFilterConfig(), filter,
+            new MockHttpServletRequest("x"), new MockHttpServletResponse(),
+            new MockFilterChain(true));
+
+        // Ensure filter didn't change our original object
+        assertEquals(originalAuth,
+            SecureContextUtils.getSecureContext().getAuthentication());
+    }
+
+    public void testOperationWhenNoAuthenticationInContextHolder()
+        throws Exception {
+    	Authentication remembered = new TestingAuthenticationToken("remembered",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_REMEMBERED")});
+        RememberMeProcessingFilter filter = new RememberMeProcessingFilter();
+        filter.setRememberMeServices(new MockRememberMeServices(remembered));
+        filter.afterPropertiesSet();
+
+        executeFilterInContainerSimulator(new MockFilterConfig(), filter,
+            new MockHttpServletRequest("x"), new MockHttpServletResponse(),
+            new MockFilterChain(true));
+
+        Authentication auth = SecureContextUtils.getSecureContext()
+                                                .getAuthentication();
+        
+        // Ensure filter setup with our remembered authentication object
+        assertEquals(remembered,
+                SecureContextUtils.getSecureContext().getAuthentication());
+    }
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        ContextHolder.setContext(new SecureContextImpl());
+    }
+
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        ContextHolder.setContext(null);
+    }
+
+    private void executeFilterInContainerSimulator(FilterConfig filterConfig,
+        Filter filter, ServletRequest request, ServletResponse response,
+        FilterChain filterChain) throws ServletException, IOException {
+        filter.init(filterConfig);
+        filter.doFilter(request, response, filterChain);
+        filter.destroy();
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockFilterChain implements FilterChain {
+        private boolean expectToProceed;
+
+        public MockFilterChain(boolean expectToProceed) {
+            this.expectToProceed = expectToProceed;
+        }
+
+        private MockFilterChain() {
+            super();
+        }
+
+        public void doFilter(ServletRequest request, ServletResponse response)
+            throws IOException, ServletException {
+            if (expectToProceed) {
+                assertTrue(true);
+            } else {
+                fail("Did not expect filter chain to proceed");
+            }
+        }
+    }
+    
+    private class MockRememberMeServices implements RememberMeServices
+	{
+    	private Authentication authToReturn;
+    	
+    	public MockRememberMeServices(Authentication authToReturn) {
+    		this.authToReturn = authToReturn;
+    	}
+    	
+		public Authentication autoLogin(HttpServletRequest request,
+				HttpServletResponse response) {
+			return authToReturn;
+		}
+		public void loginFail(HttpServletRequest request,
+				HttpServletResponse response) {
+		}
+		public void loginSuccess(HttpServletRequest request,
+				HttpServletResponse response,
+				Authentication successfulAuthentication) {
+		}
+}
+}

+ 412 - 0
core/src/test/java/org/acegisecurity/ui/rememberme/TokenBasedRememberMeServicesTests.java

@@ -0,0 +1,412 @@
+/* Copyright 2004, 2005 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.rememberme;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.AuthenticationDao;
+import net.sf.acegisecurity.providers.dao.User;
+import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import org.springframework.dao.DataAccessException;
+
+import org.springframework.util.StringUtils;
+
+import java.util.Date;
+
+import javax.servlet.http.Cookie;
+
+
+/**
+ * Tests {@link
+ * net.sf.acegisecurity.ui.rememberme.TokenBasedRememberMeServices}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class TokenBasedRememberMeServicesTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public TokenBasedRememberMeServicesTests() {
+        super();
+    }
+
+    public TokenBasedRememberMeServicesTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(TokenBasedRememberMeServicesTests.class);
+    }
+
+    public void testAutoLoginIfDoesNotPresentAnyCookies()
+        throws Exception {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(null, true));
+        services.afterPropertiesSet();
+
+        MockHttpServletRequest request = new MockHttpServletRequest("dc");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNull(returnedCookie); // shouldn't try to invalidate our cookie
+    }
+
+    public void testAutoLoginIfDoesNotPresentRequiredCookie()
+        throws Exception {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(null, true));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie("unrelated_cookie", "foobar");
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNull(returnedCookie); // shouldn't try to invalidate our cookie
+    }
+
+    public void testAutoLoginIfExpired() throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                generateCorrectCookieContentForToken(System.currentTimeMillis()
+                    - 1000000, "someone", "password", "key"));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginIfMissingThreeTokensInCookieValue()
+        throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                new String(Base64.encodeBase64("x".getBytes())));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginIfNotBase64Encoded() throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                "NOT_BASE_64_ENCODED");
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginIfSignatureBlocksDoesNotMatchExpectedValue()
+        throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                generateCorrectCookieContentForToken(System.currentTimeMillis()
+                    + 1000000, "someone", "password", "WRONG_KEY"));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginIfTokenDoesNotContainANumberInCookieValue()
+        throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                new String(Base64.encodeBase64(
+                        "username:NOT_A_NUMBER:signature".getBytes())));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginIfUserNotFound() throws Exception {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(null, true));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                generateCorrectCookieContentForToken(System.currentTimeMillis()
+                    + 1000000, "someone", "password", "key"));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNull(result);
+
+        Cookie returnedCookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(returnedCookie);
+        assertEquals(0, returnedCookie.getMaxAge());
+    }
+
+    public void testAutoLoginWithValidToken() throws Exception {
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setKey("key");
+        services.setAuthenticationDao(new MockAuthenticationDao(user, false));
+        services.afterPropertiesSet();
+
+        Cookie cookie = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY,
+                generateCorrectCookieContentForToken(System.currentTimeMillis()
+                    + 1000000, "someone", "password", "key"));
+        MockHttpServletRequest request = new MockHttpServletRequest(null, null,
+                "null", new Cookie[] {cookie});
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        Authentication result = services.autoLogin(request, response);
+
+        assertNotNull(result);
+
+        UserDetails resultingUserDetails = (UserDetails) result.getPrincipal();
+
+        assertEquals(user, resultingUserDetails);
+    }
+
+    public void testGettersSetters() {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        services.setAuthenticationDao(new MockAuthenticationDao(null, false));
+        assertTrue(services.getAuthenticationDao() != null);
+
+        services.setKey("d");
+        assertEquals("d", services.getKey());
+
+        assertEquals(TokenBasedRememberMeServices.DEFAULT_PARAMETER,
+            services.getParameter());
+        services.setParameter("some_param");
+        assertEquals("some_param", services.getParameter());
+
+        services.setTokenValiditySeconds(12);
+        assertEquals(12, services.getTokenValiditySeconds());
+    }
+
+    public void testLoginFail() {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        MockHttpServletRequest request = new MockHttpServletRequest("fv");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        services.loginFail(request, response);
+
+        Cookie cookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(cookie);
+        assertEquals(0, cookie.getMaxAge());
+    }
+
+    public void testLoginSuccessIgnoredIfParameterNotSetOrFalse() {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        MockHttpServletRequest request = new MockHttpServletRequest("d");
+        request.setParameter(TokenBasedRememberMeServices.DEFAULT_PARAMETER,
+            "false");
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        services.loginSuccess(request, response,
+            new TestingAuthenticationToken("someone", "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")}));
+
+        Cookie cookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNull(cookie);
+    }
+
+    public void testLoginSuccessNormalWithNonUserDetailsBasedPrincipal() {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        MockHttpServletRequest request = new MockHttpServletRequest("d");
+        request.setParameter(TokenBasedRememberMeServices.DEFAULT_PARAMETER,
+            "true");
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        services.loginSuccess(request, response,
+            new TestingAuthenticationToken("someone", "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")}));
+
+        Cookie cookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(cookie);
+        assertEquals(60 * 60 * 24 * 365 * 5, cookie.getMaxAge()); // 5 years
+        assertTrue(Base64.isArrayByteBase64(cookie.getValue().getBytes()));
+        assertTrue(new Date().before(
+                new Date(determineExpiryTimeFromBased64EncodedToken(
+                        cookie.getValue()))));
+    }
+
+    public void testLoginSuccessNormalWithUserDetailsBasedPrincipal() {
+        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices();
+        MockHttpServletRequest request = new MockHttpServletRequest("d");
+        request.setParameter(TokenBasedRememberMeServices.DEFAULT_PARAMETER,
+            "true");
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        UserDetails user = new User("someone", "password", true, true, true,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")});
+        services.loginSuccess(request, response,
+            new TestingAuthenticationToken(user, "ignored",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ABC")}));
+
+        Cookie cookie = response.getCookieByName(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY);
+        assertNotNull(cookie);
+        assertEquals(60 * 60 * 24 * 365 * 5, cookie.getMaxAge()); // 5 years
+        assertTrue(Base64.isArrayByteBase64(cookie.getValue().getBytes()));
+        assertTrue(new Date().before(
+                new Date(determineExpiryTimeFromBased64EncodedToken(
+                        cookie.getValue()))));
+    }
+
+    private long determineExpiryTimeFromBased64EncodedToken(String validToken) {
+        String cookieAsPlainText = new String(Base64.decodeBase64(
+                    validToken.getBytes()));
+        String[] cookieTokens = StringUtils.delimitedListToStringArray(cookieAsPlainText,
+                ":");
+
+        if (cookieTokens.length == 3) {
+            try {
+                return new Long(cookieTokens[1]).longValue();
+            } catch (NumberFormatException nfe) {}
+        }
+
+        return -1;
+    }
+
+    private String generateCorrectCookieContentForToken(long expiryTime,
+        String username, String password, String key) {
+        // format is:
+        //     username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)
+        String signatureValue = new String(DigestUtils.md5Hex(username + ":"
+                    + expiryTime + ":" + password + ":" + key));
+        String tokenValue = username + ":" + expiryTime + ":" + signatureValue;
+        String tokenValueBase64 = new String(Base64.encodeBase64(
+                    tokenValue.getBytes()));
+
+        return tokenValueBase64;
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockAuthenticationDao implements AuthenticationDao {
+        private UserDetails toReturn;
+        private boolean throwException;
+
+        public MockAuthenticationDao(UserDetails toReturn,
+            boolean throwException) {
+            this.toReturn = toReturn;
+            this.throwException = throwException;
+        }
+
+        public UserDetails loadUserByUsername(String username)
+            throws UsernameNotFoundException, DataAccessException {
+            if (throwException) {
+                throw new UsernameNotFoundException("as requested by mock");
+            }
+
+            return toReturn;
+        }
+    }
+}

+ 114 - 1
doc/docbook/acegi.xml

@@ -2645,7 +2645,7 @@ key:              A private key to prevent modification of the nonce token
       /index.jsp=ROLE_ANONYMOUS,ROLE_USER
       /hello.htm=ROLE_ANONYMOUS,ROLE_USER
       /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
-      /acegilogin.jsp=ROLE_ANONYMOUS,ROLE_USER
+      /acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER
       /**=ROLE_USER
     &lt;/value&gt;
   &lt;/property&gt;
@@ -2669,6 +2669,110 @@ key:              A private key to prevent modification of the nonce token
         authentication mechanism.</para>
       </sect2>
 
+      <sect2 id="security-ui-remember-me">
+        <title>Remember-Me Authentication</title>
+
+        <para>Remember-me authentication refers to web sites being able to
+        remember the identity of a principal between sessions. This is
+        typically accomplished by sending a cookie to the browser, with the
+        cookie being detected during future sessions and causing automated
+        login to take place. Acegi Security provides the necessary hooks so
+        that such operations can take place, along with providing a concrete
+        implementation that uses hashing to preserve the security of
+        cookie-based tokens. </para>
+
+        <para>Remember-me authentication is not used with digest or basic
+        authentication, given they are often not used with
+        <literal>HttpSession</literal>s. Remember-me is used with
+        <literal>AuthenticationProcessingFilter</literal>, and is implemented
+        via hooks in the <literal>AbstractProcessingFilter</literal>
+        superclass. The hooks will invoke a concrete
+        <literal>RememberMeServices</literal> at the appropriate times. The
+        interface looks like this:</para>
+
+        <para><programlisting>public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
+public void loginFail(HttpServletRequest request, HttpServletResponse response);
+public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);</programlisting></para>
+
+        <para>Please refer to JavaDocs for a fuller discussion on what the
+        methods do, although note at this stage
+        <literal>AbstractProcessingFilter</literal> only calls the
+        <literal>loginFail()</literal> and <literal>loginSuccess()</literal>
+        methods. The <literal>autoLogin()</literal> method is called by
+        <literal>RememberMeProcessingFilter</literal> whenever the
+        <literal>ContextHolder</literal> does not contain an
+        <literal>Authentication</literal>. This interface therefore provides
+        the underlaying remember-me implementation with sufficient
+        notification of authentication-related events, and delegates to the
+        implementation whenever a candidate web request might contain a cookie
+        and wish to be remembered.</para>
+
+        <para>This design allows any number of remember-me implementation
+        strategies. In the interests of simplicity and avoiding the need for
+        DAO implementations that specify write and create methods, Acegi
+        Security's only concrete implementation,
+        <literal>TokenBasedRememberMeServices</literal>, uses hashing to
+        achieve a useful remember-me strategy. In essence a cookie is sent to
+        the browser upon successful interactive authentication, with that
+        cookie being composed as follows:</para>
+
+        <para><programlisting>base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key))
+
+username:         As identifiable to TokenBasedRememberMeServices.getAuthenticationDao()
+password:         That matches the relevant UserDetails retrieved from TokenBasedRememberMeServices.getAuthenticationDao()
+expirationTime:   The date and time when the remember-me token expires, expressed in milliseconds
+key:              A private key to prevent modification of the remember-me token
+</programlisting></para>
+
+        <para>As such the remember-me token is valid only for the period
+        specified, and provided that the username, password and key does not
+        change. Notably, this has a potential security issue issue in that a
+        captured remember-me token will be usable from any user agent until
+        such time as the token expires. This is the same issue as with digest
+        authentication. If a principal is aware a token has been captured,
+        they can easily change their password and immediately invalidate all
+        remember-me tokens on issue. However, if more significant security is
+        needed a rolling token approach should be used (this would require a
+        database) or remember-me services should simply not be used.</para>
+
+        <para><literal>TokenBasedRememberMeServices</literal> generates a
+        <literal>RememberMeAuthenticationToken</literal>, which is processed
+        by <literal>RememberMeAuthenticationProvider</literal>. A
+        <literal>key</literal> is shared between this authentication provider
+        and the <literal>TokenBasedRememberMeServices</literal>. In addition,
+        <literal>TokenBasedRememberMeServices</literal> requires an
+        <literal>AuthenticationDao</literal> from which it can retrieve the
+        username and password for signature comparison purposes, and generate
+        the <literal>RememberMeAuthenticationToken</literal> to contain the
+        correct <literal>GrantedAuthority</literal>[]s. Some sort of logout
+        command should be provided by the application (typically via a JSP)
+        that invalidates the cookie upon user request. See the Contacts Sample
+        application's <literal>logout.jsp</literal> for an example.</para>
+
+        <para>The beans required in an application context to enable
+        remember-me services are as follows:</para>
+
+        <para><programlisting>&lt;bean id="rememberMeProcessingFilter" class="net.sf.acegisecurity.ui.rememberme.RememberMeProcessingFilter"&gt;
+  &lt;property name="rememberMeServices"&gt;&lt;ref local="rememberMeServices"/&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="rememberMeServices" class="net.sf.acegisecurity.ui.rememberme.TokenBasedRememberMeServices"&gt;
+  &lt;property name="authenticationDao"&gt;&lt;ref local="jdbcDaoImpl"/&gt;&lt;/property&gt;
+  &lt;property name="key"&gt;&lt;value&gt;springRocks&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;
+   
+&lt;bean id="rememberMeAuthenticationProvider" class="net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider"&gt;
+  &lt;property name="key"&gt;&lt;value&gt;springRocks&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;</programlisting>Don't forget to add your
+        <literal>RememberMeServices</literal> implementation to your
+        <literal>AuthenticationProcessingFilter.setRememberMeServices()</literal>
+        property, include the <literal>RememberMeProcessingFilter</literal> in
+        your <literal>AuthenticationManager.setProviders()</literal> list, and
+        add a call to <literal>RememberMeProcessingFilter</literal> into your
+        <literal>FilterChainProxy</literal> (typically immediately after your
+        <literal>AuthenticationProcessingFilter</literal>).</para>
+      </sect2>
+
       <sect2 id="security-ui-well-known">
         <title>Well-Known Locations</title>
 
@@ -4482,6 +4586,15 @@ INSERT INTO acl_permission VALUES (null, 6, 'scott', 1);</programlisting></para>
             container</para>
           </listitem>
 
+          <listitem>
+            <para><literal>RememberMeProcessingFilter</literal>, so that if no
+            earlier authentication processing mechanism updated the
+            <literal>ContextHolder</literal>, and the request presents a
+            cookie that enables remember-me services to take place, a suitable
+            remembered <literal><literal>Authentication</literal></literal>
+            object will be put there</para>
+          </listitem>
+
           <listitem>
             <para><literal>AnonymousProcessingFilter</literal>, so that if no
             earlier authentication processing mechanism updated the

+ 9 - 7
doc/xdocs/changes.xml

@@ -27,27 +27,29 @@
   <body>
     <release version="0.8.0" date="CVS">
       <action dev="benalex" type="add">Added Digest Authentication support (RFC 2617 and RFC 2069)</action>
+      <action dev="benalex" type="add">Added pluggable remember-me services</action>
+      <action dev="benalex" type="add">Added pluggable mechnism to prevent concurrent login sessions</action>
+      <action dev="benalex" type="add">FilterChainProxy added to significantly simplify web.xml configuration of Acegi Security</action>
+      <action dev="benalex" type="add">AuthenticationProcessingFilter now provides hook for extra credentials (eg postcodes)</action>
+      <action dev="benalex" type="add">New WebAuthenticationDetails class now used by processing filters for Authentication.setDetails()</action>
+      <action dev="benalex" type="add">Additional debug-level logging</action>
+      <action dev="benalex" type="add">Improved Tapestry support in AbstractProcessingFilter</action>
       <action dev="benalex" type="update">Made ConfigAttributeDefinition and ConfigAttribute Serializable</action>
       <action dev="benalex" type="update">User now accepts blank passwords (null passwords still rejected)</action>
       <action dev="benalex" type="update">FilterToBeanProxy now searches hierarchical bean factories</action>
-      <action dev="benalex" type="add">Improved Tapestry support in AbstractProcessingFilter</action>
       <action dev="benalex" type="update">User now accepted blank passwords (null passwords still rejected)</action>
       <action dev="benalex" type="update">ContextHolderAwareRequestWrapper now provides a getUserPrincipal() method</action>
       <action dev="benalex" type="update">HttpSessionIntegrationFilter no longer creates a HttpSession unnecessarily</action>
       <action dev="benalex" type="update">FilterSecurityInterceptor now only executes once per request (improves performance with SiteMesh)</action>
-      <action dev="benalex" type="fix">Log4j now included in generated WAR artifacts (fixes issue with Log4j listener)</action>
       <action dev="raykrueger" type="update">JaasAuthenticatinProvider now uses System.property "java.security.auth.login.config"</action>
       <action dev="raykrueger" type="update">JaasAuthenticationCallbackHandler Authentication is passed to handle method setAuthentication removed</action>
       <action dev="raykrueger" type="update">Added AuthenticationException to the AutenticationEntryPoint.commence method signature</action>
       <action dev="raykrueger" type="update">Added AccessDeniedException to the SecurityEncorcementFilter.sendAccessDeniedError method signature</action>
-      <action dev="benalex" type="fix">Correct issue with JdbcDaoImpl default SQL query not using consistent case sensitivity</action>
-      <action dev="benalex" type="add">FilterChainProxy added to significantly simplify web.xml configuration of Acegi Security</action>
       <action dev="benalex" type="update">FilterToBeanProxy now addresses lifecycle mismatch (IoC container vs servlet container) issue</action>
-      <action dev="benalex" type="add">Additional debug-level logging</action>
-      <action dev="benalex" type="add">AuthenticationProcessingFilter now provides hook for extra credentials (eg postcodes)</action>
-      <action dev="benalex" type="add">New WebAuthenticationDetails class now used by processing filters for Authentication.setDetails()</action>
       <action dev="benalex" type="update">Significantly refactor "well-known location model" to authentication processing mechanism and HttpSessionContextIntegrationFilter model</action>
+      <action dev="benalex" type="fix">Correct issue with JdbcDaoImpl default SQL query not using consistent case sensitivity</action>
       <action dev="benalex" type="fix">Improve Linux and non-Sun JDK (specifically IBM JDK) compatibility</action>
+      <action dev="benalex" type="fix">Log4j now included in generated WAR artifacts (fixes issue with Log4j listener)</action>
       <action dev="benalex" type="fix">Correct NullPointerException in FilterInvocationDefinitionSource implementations</action>
     </release>
     <release version="0.7.0" date="2005-01-16">

+ 1 - 0
samples/contacts/src/main/resources/log4j.properties

@@ -13,6 +13,7 @@ log4j.rootLogger=WARN, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.acl.basic=DEBUG, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.taglibs.authz=DEBUG, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.ui.basicauth=DEBUG, stdout, fileout
+#log4j.logger.net.sf.acegisecurity.ui.rememberme=DEBUG, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.ui=DEBUG, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.afterinvocation=DEBUG, stdout, fileout
 #log4j.logger.net.sf.acegisecurity.ui.rmi=DEBUG, stdout, fileout

+ 5 - 0
samples/contacts/src/main/webapp/common/WEB-INF/jsp/hello.jsp

@@ -25,6 +25,11 @@
    the application context using standard Acegi Security classes. *</li>
 <li><b>Database-sourced security data</b>. All of the user, role and ACL
    information is obtained from an in-memory JDBC-compliant database.</li>
+<li><b>Integrated form-based and BASIC authentication</b>. Any BASIC
+   authentication header is detected and used for authentication. Normal
+   interactive form-based authentication is used by default.</li>
+<li><b>Remember-me services</b>. Acegi Security's pluggable remember-me
+   strategy is demonstrated, with a corresponding checkbox on the login form.</li>
 </ul>
 
 * As the application provides an "ACL Administration" use case, those

+ 1 - 1
samples/contacts/src/main/webapp/common/WEB-INF/jsp/index.jsp

@@ -27,6 +27,6 @@
   </tr>
 </c:forEach>
 </table>
-<p><a href="<c:url value="add.htm"/>">Add</a>   <p><a href="<c:url value="../logoff.jsp"/>">Logoff</a>
+<p><a href="<c:url value="add.htm"/>">Add</a>   <p><a href="<c:url value="../logoff.jsp"/>">Logoff</a> (also clears any remember-me cookie)
 </body>
 </html>

+ 7 - 1
samples/contacts/src/main/webapp/common/logoff.jsp

@@ -1,3 +1,9 @@
-<%session.invalidate();
+<%@ page import="javax.servlet.http.Cookie" %>
+<%@ page import="net.sf.acegisecurity.ui.rememberme.TokenBasedRememberMeServices" %>
+<%
+session.invalidate();
+Cookie terminate = new Cookie(TokenBasedRememberMeServices.ACEGI_SECURITY_HASHED_REMEMBER_ME_COOKIE_KEY, null);
+terminate.setMaxAge(0);
+response.addCookie(terminate);
 response.sendRedirect("index.jsp");
 %>

+ 17 - 2
samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml

@@ -21,7 +21,7 @@
          <value>
 		    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
 		    PATTERN_TYPE_APACHE_ANT
-            /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,anonymousProcessingFilter,securityEnforcementFilter
+            /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,rememberMeProcessingFilter,anonymousProcessingFilter,securityEnforcementFilter
          </value>
       </property>
     </bean>
@@ -33,6 +33,7 @@
          <list>
             <ref local="daoAuthenticationProvider"/>
             <ref local="anonymousAuthenticationProvider"/>
+             <ref local="rememberMeAuthenticationProvider"/>
          </list>
       </property>
    </bean>
@@ -89,6 +90,19 @@
       <property name="context"><value>net.sf.acegisecurity.context.security.SecureContextImpl</value></property>
    </bean>
 
+   <bean id="rememberMeProcessingFilter" class="net.sf.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
+      <property name="rememberMeServices"><ref local="rememberMeServices"/></property>
+   </bean>
+
+   <bean id="rememberMeServices" class="net.sf.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
+      <property name="authenticationDao"><ref local="jdbcDaoImpl"/></property>
+      <property name="key"><value>springRocks</value></property>
+   </bean>
+   
+   <bean id="rememberMeAuthenticationProvider" class="net.sf.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
+      <property name="key"><value>springRocks</value></property>
+   </bean>
+
    <!-- ===================== HTTP CHANNEL REQUIREMENTS ==================== -->
 
    <!-- You will need to uncomment the "Acegi Channel Processing Filter"
@@ -131,6 +145,7 @@
       <property name="authenticationFailureUrl"><value>/acegilogin.jsp?login_error=1</value></property>
       <property name="defaultTargetUrl"><value>/</value></property>
       <property name="filterProcessesUrl"><value>/j_acegi_security_check</value></property>
+      <property name="rememberMeServices"><ref local="rememberMeServices"/></property>
    </bean>
 
    <bean id="authenticationProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
@@ -160,7 +175,7 @@
 			    /index.jsp=ROLE_ANONYMOUS,ROLE_USER
 			    /hello.htm=ROLE_ANONYMOUS,ROLE_USER
 			    /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
-			    /acegilogin.jsp=ROLE_ANONYMOUS,ROLE_USER
+			    /acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER
 				/**=ROLE_USER
          </value>
       </property>

+ 2 - 2
samples/contacts/src/main/webapp/filter/WEB-INF/web.xml

@@ -62,11 +62,11 @@
         The HttpSessionEventPublisher will publish
         HttpSessionCreatedEvent and HttpSessionDestroyedEvent
         to the WebApplicationContext
-    -->
+   
     <listener>
         <listener-class>net.sf.acegisecurity.ui.session.HttpSessionEventPublisher</listener-class>
     </listener>
-
+ -->
   <!--
 	- Provides core MVC application controller. See contacts-servlet.xml.
     -->

+ 1 - 0
samples/contacts/src/main/webapp/filter/acegilogin.jsp

@@ -33,6 +33,7 @@
       <table>
         <tr><td>User:</td><td><input type='text' name='j_username' <c:if test="${not empty param.login_error}">value='<%= session.getAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_LAST_USERNAME_KEY) %>'</c:if>></td></tr>
         <tr><td>Password:</td><td><input type='password' name='j_password'></td></tr>
+        <tr><td><input type="checkbox" name="_acegi_security_remember_me"></td><td>Don't ask for my password for two weeks</td></tr>
 
         <tr><td colspan='2'><input name="submit" type="submit"></td></tr>
         <tr><td colspan='2'><input name="reset" type="reset"></td></tr>