Browse Source

Initial commit for captcha adapter

Marc-Antoine Garrigue 20 years ago
parent
commit
3287439421

+ 361 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaChannelProcessor.java

@@ -0,0 +1,361 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.acegisecurity.captcha;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import javax.servlet.ServletException;
+
+import net.sf.acegisecurity.ConfigAttribute;
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+import net.sf.acegisecurity.securechannel.ChannelEntryPoint;
+import net.sf.acegisecurity.securechannel.ChannelProcessor;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+/**
+ * <p>
+ * Ensures the user has enougth human privileges by review of the
+ * {@link net.sf.acegisecurity.captcha.CaptchaSecurityContext}.
+ * </p>
+ * 
+ * <P>
+ * The class takes 3 required attributes :
+ * <ul>
+ * <li>maxRequestsBeforeFirstTest : used by
+ * {@link #getRequiresHumanAfterMaxRequestsKeyword()} and
+ * {@link #getRequiresHumanAfterMaxMillisKeyword()}<br>
+ * default value = 0 (ie first request).</li>
+ * <li>maxRequestsBeforeReTest : used by
+ * {@link #getRequiresHumanAfterMaxMillisKeyword()}<br>
+ * default value = -1 (ie disabled, once in a {@link CaptchaSecurityContext}'s
+ * life).</li>
+ * <li>maxMillisBeforeReTest: used by
+ * {@link #getRequiresHumanAfterMaxMillisKeyword()} <br>
+ * default value = -1 (ie disabled, once in a {@link CaptchaSecurityContext}'s
+ * life).</li>
+ * </ul>
+ * The class responds to two case-sensitive keywords :
+ * <ul>
+ * <li>{@link #getRequiresHumanAfterMaxRequestsKeyword()} <br>
+ * default value = <code>REQUIRES_HUMAN_AFTER_MAX_REQUESTS</code> <br>
+ * if detected, checks if :
+ * <ul>
+ * <ul>
+ * <li><code>{@link CaptchaSecurityContext#isHuman()} == true</code> </li>
+ * <li><b>or</b></li>
+ * <li><code>{@link CaptchaSecurityContext#getHumanRestrictedResourcesRequestsCount()} < maxRequestsBeforeFirstTest</code></b></li>
+ * </ul>
+ * <li><b>and</b></li>
+ * <ul>
+ * <li><code>{@link CaptchaSecurityContext#getHumanRestrictedResourcesRequestsCount()} < maxRequestsBeforeReTest </code></li>
+ * <li><b>or</b></li>
+ * <li><code>maxRequestsBeforeReTest < 0 </code></b></li>
+ * </ul>
+ * </ul>
+ * </li>
+ * 
+ * 
+ * 
+ * 
+ * 
+ * 
+ * 
+ * <li>{@link #getRequiresHumanUntilMaxRequestsKeyword()} <br>
+ * default value = <code>REQUIRES_HUMAN_AFTER_MAX_MILLIS</code> <br>
+ * if detected, checks if :
+ * 
+ * <ul>
+ * <ul>
+ * <li><code>{@link CaptchaSecurityContext#isHuman()} == true</code> </li>
+ * <li><b>or</b></li>
+ * <li><code>{@link CaptchaSecurityContext#getHumanRestrictedResourcesRequestsCount()} < =maxRequestsBeforeFirstTest</code></b></li>
+ * </ul>
+ * <li><b>and</b></li>
+ * <ul>
+ * <li><code>System.currentTimeMillis()-{@link CaptchaSecurityContext#getLastPassedCaptchaDateInMillis()} <= maxMillisBeforeReTest </code></li>
+ * <li><b>or</b></li>
+ * <li><code>maxMillisBeforeReTest < 0 </code></b></li>
+ * </ul>
+ * </ul>
+ * </li>
+ * </ul>
+ * </p>
+ * 
+ * <p>
+ * <u>Examples : to ensure an url is accessed only by human that pass a captcha
+ * (assuming you are using the
+ * {@link net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter})
+ * </u><br>
+ * <ul>
+ * <li>Once in a session and at first request : use the <br>
+ * REQUIRES_HUMAN_AFTER_MAX_REQUESTS keyword <br>
+ * with a maxRequestsBeforeFirstTest=0<br>
+ * and a maxRequestsBeforeReTest=-1<br>
+ * </li>
+ * <br>
+ * &nbsp;
+ * <li>Once in a session and only after 3 requests : use the <br>
+ * REQUIRES_HUMAN_AFTER_MAX_REQUESTS keyword <br>
+ * with a maxRequestsBeforeFirstTest=3</li>
+ * and a maxRequestsBeforeReTest=-1<br>
+ * <br>
+ * &nbsp;
+ * <li>Every request and only after 5 requests : use the <br>
+ * REQUIRES_HUMAN_AFTER_MAX_REQUESTS <br>
+ * with a maxRequestsBeforeReTest=0<br>
+ * and a maxRequestsBeforeFirstTest=5</li>
+ * <br>
+ * &nbsp;
+ * <li>Every 3 requests and every minute : use the <br>
+ * REQUIRES_HUMAN_AFTER_MAX_MILLIS keywords <br>
+ * with a maxMillisBeforeReTest=6000 <br>
+ * and a maxRequestsBeforeFirstTest=3</li>
+ * <br>
+ * &nbsp;
+ * <li>Every 20 requests and every hour and only after 100 requests : use the
+ * <br>
+ * REQUIRES_HUMAN_AFTER_MAX_REQUESTS <br>
+ * and the REQUIRES_HUMAN_AFTER_MAX_MILLIS <br>
+ * and the REQUIRES_HUMAN_AFTER_MAX_REQUESTS keywords <br>
+ * with a maxRequestsBeforeReTest=20 <br>
+ * and a maxMillisBeforeReTest=3600000 <br>
+ * and amaxRequestsBeforeFirstTest=1000</li>
+ * 
+ * </ul>
+ * 
+ * 
+ * @author marc antoine Garrigue
+ * @version $Id$
+ */
+public class CaptchaChannelProcessor implements ChannelProcessor,
+		InitializingBean {
+	// ~ Static fields/initializers
+	// =============================================
+
+	private static final Log logger = LogFactory
+			.getLog(CaptchaChannelProcessor.class);
+
+	private String requiresHumanAfterMaxRequestsKeyword = "REQUIRES_HUMAN_AFTER_MAX_REQUESTS";
+
+	private String requiresHumanAfterMaxMillisKeyword = "REQUIRES_HUMAN_AFTER_MAX_MILLIS";
+
+	private ChannelEntryPoint entryPoint;
+
+	private int maxRequestsBeforeReTest = -1;
+
+	private int maxRequestsBeforeFirstTest = 0;
+
+	private long maxMillisBeforeReTest = -1;
+
+	public String getRequiresHumanAfterMaxMillisKeyword() {
+		return requiresHumanAfterMaxMillisKeyword;
+	}
+
+	public void setRequiresHumanAfterMaxMillisKeyword(
+			String requiresHumanAfterMaxMillis) {
+		this.requiresHumanAfterMaxMillisKeyword = requiresHumanAfterMaxMillis;
+
+	}
+
+	public void setRequiresHumanAfterMaxRequestsKeyword(
+			String requiresHumanAfterMaxRequestsKeyword) {
+		this.requiresHumanAfterMaxRequestsKeyword = requiresHumanAfterMaxRequestsKeyword;
+	}
+
+	public ChannelEntryPoint getEntryPoint() {
+		return entryPoint;
+	}
+
+	public void setEntryPoint(ChannelEntryPoint entryPoint) {
+		this.entryPoint = entryPoint;
+	}
+
+	public int getMaxRequestsBeforeReTest() {
+		return maxRequestsBeforeReTest;
+	}
+
+	public void setMaxRequestsBeforeReTest(int maxRequestsBeforeReTest) {
+		this.maxRequestsBeforeReTest = maxRequestsBeforeReTest;
+	}
+
+	public String getRequiresHumanAfterMaxRequestsKeyword() {
+		return requiresHumanAfterMaxRequestsKeyword;
+	}
+
+	public int getMaxRequestsBeforeFirstTest() {
+		return maxRequestsBeforeFirstTest;
+	}
+
+	public void setMaxRequestsBeforeFirstTest(int maxRequestsBeforeFirstTest) {
+		this.maxRequestsBeforeFirstTest = maxRequestsBeforeFirstTest;
+	}
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(entryPoint, "entryPoint required");
+	}
+
+	public long getMaxMillisBeforeReTest() {
+		return maxMillisBeforeReTest;
+	}
+
+	public void setMaxMillisBeforeReTest(long maxMillisBeforeReTest) {
+		this.maxMillisBeforeReTest = maxMillisBeforeReTest;
+	}
+
+	public void decide(FilterInvocation invocation,
+			ConfigAttributeDefinition config) throws IOException,
+			ServletException {
+		if ((invocation == null) || (config == null)) {
+			throw new IllegalArgumentException("Nulls cannot be provided");
+		}
+		CaptchaSecurityContext context = (CaptchaSecurityContext) SecurityContextHolder
+				.getContext();
+
+		Iterator iter = config.getConfigAttributes();
+		boolean shouldRedirect = true;
+
+		while (iter.hasNext()) {
+			ConfigAttribute attribute = (ConfigAttribute) iter.next();
+
+			if (supports(attribute)) {
+				logger.debug("supports this attribute : " + attribute);
+				if (isContextValidForAttribute(context, attribute)) {
+					shouldRedirect = false;
+				} else {
+					// reset if already passed a constraint
+
+					shouldRedirect = true;
+					// break at first unsatisfy contraint
+					break;
+				}
+
+			}
+		}
+		if (shouldRedirect) {
+			logger
+					.debug("context is not allowed to access ressource, redirect to captcha entry point");
+			redirectToEntryPoint(invocation);
+		} else {
+			// if we reach this point, we forward the request so
+			// increment it
+			logger
+					.debug("context is allowed to access ressource, increment rectricted ressource requests count ");
+			context.incrementHumanRestrictedRessoucesRequestsCount();
+
+		}
+	}
+
+	private boolean isContextValidForAttribute(CaptchaSecurityContext context,
+			ConfigAttribute attribute) {
+		boolean valid = false;
+		if ((attribute != null) || (attribute.getAttribute() != null)) {
+
+			// test the REQUIRES_HUMAN_AFTER_MAX_REQUESTS keyword
+			if (attribute.getAttribute().equals(
+					getRequiresHumanAfterMaxRequestsKeyword())) {
+				if (isContextValidConcerningHumanOrFirstTest(context)
+						&& isContextValidConcerningReTest(context)) {
+					valid = true;
+				}
+			}
+
+			// test the REQUIRES_HUMAN_AFTER_MAX_MILLIS keyword
+			if (attribute.getAttribute().equals(
+					getRequiresHumanAfterMaxMillisKeyword())) {
+				if (isContextValidConcerningHumanOrFirstTest(context)
+						&& isContextValidConcerningMaxMillis(context)) {
+					valid = true;
+				}
+			}
+
+		}
+		return valid;
+	}
+
+	private boolean isContextValidConcerningHumanOrFirstTest(
+			CaptchaSecurityContext context) {
+		if (context.isHuman()
+				|| context.getHumanRestrictedResourcesRequestsCount() < maxRequestsBeforeFirstTest) {
+			logger
+					.debug("context is valid concerning humanity or request count < maxRequestsBeforeFirstTest");
+
+			return true;
+		} else {
+			logger
+					.debug("context is not valid concerning humanity and request count > maxRequestsBeforeFirstTest");
+			return false;
+		}
+	}
+
+	private boolean isContextValidConcerningReTest(
+			CaptchaSecurityContext context) {
+		if (context.getHumanRestrictedResourcesRequestsCount() < maxRequestsBeforeReTest
+				|| maxRequestsBeforeReTest < 0) {
+			logger.debug("context is valid concerning reTest");
+
+			return true;
+		} else {
+			logger.debug("context is not valid concerning reTest");
+
+			return false;
+		}
+	}
+
+	private boolean isContextValidConcerningMaxMillis(
+			CaptchaSecurityContext context) {
+		if (System.currentTimeMillis()
+				- context.getLastPassedCaptchaDateInMillis() < maxMillisBeforeReTest
+				|| maxMillisBeforeReTest < 0) {
+			logger.debug("context is valid concerning maxMillis");
+
+			return true;
+		} else {
+			logger.debug("context is not valid concerning maxMillis");
+
+			return false;
+		}
+	}
+
+	private void redirectToEntryPoint(FilterInvocation invocation)
+			throws IOException, ServletException {
+		logger
+				.debug("security constraints not repected : redirecting to entry point");
+		entryPoint.commence(invocation.getRequest(), invocation.getResponse());
+		return;
+	}
+
+	public boolean supports(ConfigAttribute attribute) {
+		if ((attribute != null)
+				&& (attribute.getAttribute() != null)
+				&& (attribute.getAttribute().equals(
+						getRequiresHumanAfterMaxRequestsKeyword()) || attribute
+						.getAttribute().equals(
+								getRequiresHumanAfterMaxMillisKeyword())
+
+				)) {
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+}

+ 287 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaEntryPoint.java

@@ -0,0 +1,287 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.acegisecurity.captcha;
+
+import java.io.IOException;
+import java.util.Enumeration;
+
+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.securechannel.ChannelEntryPoint;
+import net.sf.acegisecurity.util.PortMapper;
+import net.sf.acegisecurity.util.PortMapperImpl;
+import net.sf.acegisecurity.util.PortResolver;
+import net.sf.acegisecurity.util.PortResolverImpl;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+/**
+ * The captcha entry point : redirect to the captcha test page. <br/>
+ * 
+ * This entry point can force the use of SSL : see {@link #getForceHttps()}<br/>
+ * 
+ * This entry point allows internal OR external redirect : see
+ * {@link #setOutsideWebApp(boolean)}<br/>/ Original request can be added to
+ * the redirect path using a special parameter : see
+ * {@link #getOriginalRequestParameterName()} and
+ * {@link #setIncludeOriginalRequest()} <br/> <br/> Default values :<br/>
+ * forceHttps = false<br/> includesOriginalRequest = false<br/>
+ * originalRequestParameterName= "originalRequest"<br/> isOutsideWebApp=false<br/>
+ * 
+ * @author marc antoine Garrigue
+ * @version $Id$
+ */
+public class CaptchaEntryPoint implements ChannelEntryPoint, InitializingBean {
+	// ~ Static fields/initializers
+	// =============================================
+
+	private static final Log logger = LogFactory
+			.getLog(CaptchaEntryPoint.class);
+
+	// ~ Instance fields
+	// ========================================================
+
+	private PortMapper portMapper = new PortMapperImpl();
+
+	private PortResolver portResolver = new PortResolverImpl();
+
+	private String captchaFormUrl;
+
+	private boolean forceHttps = false;
+
+	private String originalRequestParameterName = "originalRequest";
+
+	private boolean isOutsideWebApp = false;
+
+	private boolean includeOriginalRequest = false;
+
+	// ~ Methods
+	// ================================================================
+
+	/**
+	 * Set to true to force captcha form access to be via https. If this value
+	 * is ture (the default is false), and the incoming request for the
+	 * protected resource which triggered the interceptor was not already
+	 * <code>https</code>, then
+	 * 
+	 * @param forceHttps
+	 */
+	public void setForceHttps(boolean forceHttps) {
+		this.forceHttps = forceHttps;
+	}
+
+	public boolean getForceHttps() {
+		return forceHttps;
+	}
+
+	/**
+	 * The URL where the <code>CaptchaProcessingFilter</code> login page can
+	 * be found. Should be relative to the web-app context path, and include a
+	 * leading <code>/</code>
+	 * 
+	 * @param captchaFormUrl
+	 */
+	public void setCaptchaFormUrl(String loginFormUrl) {
+		this.captchaFormUrl = loginFormUrl;
+	}
+
+	/**
+	 * @return the captcha test page to redirect to.
+	 */
+	public String getCaptchaFormUrl() {
+		return captchaFormUrl;
+	}
+
+	public void setPortMapper(PortMapper portMapper) {
+		this.portMapper = portMapper;
+	}
+
+	public PortMapper getPortMapper() {
+		return portMapper;
+	}
+
+	public void setPortResolver(PortResolver portResolver) {
+		this.portResolver = portResolver;
+	}
+
+	public PortResolver getPortResolver() {
+		return portResolver;
+	}
+
+	public boolean isOutsideWebApp() {
+		return isOutsideWebApp;
+	}
+
+	/**
+	 * if set to true, the {@link #commence(ServletRequest, ServletResponse)}
+	 * method uses the {@link #getCaptchaFormUrl()} as a complete URL, else it
+	 * as a 'inside WebApp' path.
+	 * 
+	 * @param isOutsideWebApp
+	 */
+	public void setOutsideWebApp(boolean isOutsideWebApp) {
+		this.isOutsideWebApp = isOutsideWebApp;
+	}
+
+	public String getOriginalRequestParameterName() {
+		return originalRequestParameterName;
+	}
+
+	/**
+	 * sets the parameter under which the original request url will be appended
+	 * to the redirect url (only if {@link #isIncludeOriginalRequest()}==true).
+	 * 
+	 * @param originalRequestParameterName
+	 */
+	public void setOriginalRequestParameterName(
+			String originalRequestParameterName) {
+		this.originalRequestParameterName = originalRequestParameterName;
+	}
+
+	public boolean isIncludeOriginalRequest() {
+		return includeOriginalRequest;
+	}
+
+	/**
+	 * If set to true, the original request url will be appended to the redirect
+	 * url using the {@link #getOriginalRequestParameterName()}.
+	 * 
+	 * @param includeOriginalRequest
+	 */
+	public void setIncludeOriginalRequest(boolean includeOriginalRequest) {
+		this.includeOriginalRequest = includeOriginalRequest;
+	}
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.hasLength(captchaFormUrl, "captchaFormUrl must be specified");
+		Assert.notNull(portMapper, "portMapper must be specified");
+		Assert.notNull(portResolver, "portResolver must be specified");
+	}
+
+	public void commence(ServletRequest request, ServletResponse response)
+			throws IOException, ServletException {
+		StringBuffer redirectUrl = new StringBuffer();
+		HttpServletRequest req = (HttpServletRequest) request;
+
+		if (isOutsideWebApp) {
+			redirectUrl = redirectUrl.append(captchaFormUrl);
+		} else {
+			buildInternalRedirect(redirectUrl, req);
+		}
+
+		if (includeOriginalRequest) {
+			includeOriginalRequest(redirectUrl, req);
+		}
+		// add post parameter? TODO?
+		if (logger.isDebugEnabled()) {
+			logger.debug("Redirecting to: " + redirectUrl);
+		}
+
+		((HttpServletResponse) response)
+				.sendRedirect(((HttpServletResponse) response)
+						.encodeRedirectURL(redirectUrl.toString()));
+	}
+
+	private void includeOriginalRequest(StringBuffer redirectUrl,
+			HttpServletRequest req) {
+		// add original request to the url
+		if (redirectUrl.indexOf("?") >= 0) {
+			redirectUrl.append("&");
+		} else {
+			redirectUrl.append("?");
+		}
+		redirectUrl.append(originalRequestParameterName);
+		redirectUrl.append("=");
+		redirectUrl.append(req.getRequestURL().toString());
+		// append query params
+		Enumeration parameters = req.getParameterNames();
+		if (parameters != null && parameters.hasMoreElements()) {
+			redirectUrl.append("?");
+			while (parameters.hasMoreElements()) {
+				String name = parameters.nextElement().toString();
+				String value = req.getParameter(name);
+				redirectUrl.append(name);
+				redirectUrl.append("=");
+				redirectUrl.append(value);
+				if (parameters.hasMoreElements()) {
+					redirectUrl.append("&");
+				}
+			}
+		}
+
+	}
+
+	private void buildInternalRedirect(StringBuffer redirectUrl,
+			HttpServletRequest req) {
+		// construct it
+		StringBuffer simpleRedirect = new StringBuffer();
+
+		String scheme = req.getScheme();
+		String serverName = req.getServerName();
+		int serverPort = portResolver.getServerPort(req);
+		String contextPath = req.getContextPath();
+		boolean includePort = true;
+		if ("http".equals(scheme.toLowerCase()) && (serverPort == 80)) {
+			includePort = false;
+		}
+		if ("https".equals(scheme.toLowerCase()) && (serverPort == 443)) {
+			includePort = false;
+		}
+
+		simpleRedirect.append(scheme);
+		simpleRedirect.append("://");
+		simpleRedirect.append(serverName);
+		if (includePort) {
+			simpleRedirect.append(":");
+			simpleRedirect.append(serverPort);
+		}
+		simpleRedirect.append(contextPath);
+		simpleRedirect.append(captchaFormUrl);
+
+		if (forceHttps && req.getScheme().equals("http")) {
+			Integer httpPort = new Integer(portResolver.getServerPort(req));
+			Integer httpsPort = (Integer) portMapper.lookupHttpsPort(httpPort);
+
+			if (httpsPort != null) {
+				if (httpsPort.intValue() == 443) {
+					includePort = false;
+				} else {
+					includePort = true;
+				}
+
+				redirectUrl.append("https://");
+				redirectUrl.append(serverName);
+				if (includePort) {
+					redirectUrl.append(":");
+					redirectUrl.append(httpsPort);
+				}
+				redirectUrl.append(contextPath);
+				redirectUrl.append(captchaFormUrl);
+			} else {
+				redirectUrl.append(simpleRedirect);
+			}
+		} else {
+			redirectUrl.append(simpleRedirect);
+		}
+	}
+
+}

+ 55 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaSecurityContext.java

@@ -0,0 +1,55 @@
+/* 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.captcha;
+
+import net.sf.acegisecurity.context.SecurityContext;
+
+/**
+ * Interface that add humanity concerns to the SecurityContext
+ * 
+ * @author marc antoine garrigue
+ */
+public interface CaptchaSecurityContext extends SecurityContext {
+
+	/**
+	 * @return true if the current user has already passed a captcha.
+	 */
+	boolean isHuman();
+
+	/**
+	 * set human attribute, should called after captcha validation.
+	 * 
+	 * @param human
+	 */
+	void setHuman();
+
+	/**
+	 * 
+	 * @return number of human restricted resources requests since the last
+	 *         passed captcha.
+	 */
+	int getHumanRestrictedResourcesRequestsCount();
+
+	/**
+	 * @return the date of the last passed Captcha in millis, 0 if the user
+	 *         never passed captcha.
+	 */
+	long getLastPassedCaptchaDateInMillis();
+
+	/**
+	 * Method to increment the human Restricted Resrouces Requests Count;
+	 */
+	void incrementHumanRestrictedRessoucesRequestsCount();
+}

+ 85 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaSecurityContextImpl.java

@@ -0,0 +1,85 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.acegisecurity.captcha;
+
+import net.sf.acegisecurity.context.SecurityContextImpl;
+
+/**
+ * @author mag
+ * 
+ */
+public class CaptchaSecurityContextImpl extends SecurityContextImpl implements
+		CaptchaSecurityContext {
+
+	private boolean human;
+
+	private long lastPassedCaptchaDate;
+
+	private int humanRestrictedResourcesRequestsCount;
+
+	/**
+	 * 
+	 */
+	public CaptchaSecurityContextImpl() {
+		super();
+		human = false;
+		lastPassedCaptchaDate = 0;
+		humanRestrictedResourcesRequestsCount = 0;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.sf.acegisecurity.context.CaptchaSecurityContext#isHuman()
+	 */
+	public boolean isHuman() {
+		return human;
+	}
+
+	/**
+	 * reset the lastPassedCaptchaDate and count.
+	 */
+	public void setHuman() {
+		this.human = true;
+		this.lastPassedCaptchaDate = System.currentTimeMillis();
+		this.humanRestrictedResourcesRequestsCount = 0;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.sf.acegisecurity.context.CaptchaSecurityContext#getHumanRestrictedResourcesRequestsCount()
+	 */
+	public int getHumanRestrictedResourcesRequestsCount() {
+		return humanRestrictedResourcesRequestsCount;
+	}
+
+	/*
+	 * (non-Javadoc)
+	 * 
+	 * @see net.sf.acegisecurity.context.CaptchaSecurityContext#getLastPassedCaptchaDateInMillis()
+	 */
+	public long getLastPassedCaptchaDateInMillis() {
+
+		return lastPassedCaptchaDate;
+	}
+
+	/**
+	 * Method to increment the human Restricted Resrouces Requests Count;
+	 */
+	public void incrementHumanRestrictedRessoucesRequestsCount() {
+		humanRestrictedResourcesRequestsCount++;
+	};
+}

+ 17 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaServiceProxy.java

@@ -0,0 +1,17 @@
+package net.sf.acegisecurity.captcha;
+
+import javax.servlet.ServletRequest;
+
+/**
+ * Provide a common interface for captcha validation.
+ * 
+ * @author marc antoine Garrigue
+ * @version $Id$
+ */
+public interface CaptchaServiceProxy {
+
+	/**
+	 * @return true if the request is validated by the back end captcha service.
+	 */
+	boolean validateRequest(ServletRequest request);
+}

+ 126 - 0
core/src/main/java/org/acegisecurity/captcha/CaptchaValidationProcessingFilter.java

@@ -0,0 +1,126 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.captcha;
+
+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 net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+
+/**
+ * Filter for web integration of the {@link CaptchaServiceProxy}. <br/> It
+ * basically intercept calls containing the specific validation parameter, use
+ * the {@link CaptchaServiceProxy} to validate the request, and update the
+ * {@link CaptchaSecurityContext} if the request passed the validation. <br/>
+ * <br/> This Filter should be placed after the ContextIntegration filter and
+ * before the {@link CaptchaChannelProcessor} filter in the filter stack in
+ * order to update the {@link CaptchaSecurityContext} before the humanity
+ * verification routine occurs. <br/> <br/> This filter should only be used in
+ * conjunction with the {@link CaptchaSecurityContext} <br/> <br/>
+ * 
+ * 
+ * @author marc antoine Garrigue
+ * @version $Id$
+ */
+public class CaptchaValidationProcessingFilter implements InitializingBean,
+		Filter {
+	// ~ Static fields/initializers
+	// =============================================
+	public static String CAPTCHA_VALIDATION_SECURITY_PARAMETER_KEY = "_captcha_parameter";
+
+	protected static final Log logger = LogFactory
+			.getLog(HttpSessionContextIntegrationFilter.class);
+
+	// ~ Instance fields
+	// ========================================================
+
+	private CaptchaServiceProxy captchaService;
+
+	// ~ Methods
+	// ================================================================
+
+	public CaptchaServiceProxy getCaptchaService() {
+		return captchaService;
+	}
+
+	public void setCaptchaService(CaptchaServiceProxy captchaService) {
+		this.captchaService = captchaService;
+	}
+
+	public void afterPropertiesSet() throws Exception {
+		if (this.captchaService == null) {
+			throw new IllegalArgumentException(
+					"CaptchaServiceProxy must be defined ");
+		}
+	}
+
+	/**
+	 * Does nothing. We use IoC container lifecycle services instead.
+	 */
+	public void destroy() {
+	}
+
+	public void doFilter(ServletRequest request, ServletResponse response,
+			FilterChain chain) throws IOException, ServletException {
+
+		if ((request != null)
+				&& (request
+						.getParameter(CAPTCHA_VALIDATION_SECURITY_PARAMETER_KEY) != null)) {
+			logger.debug("captcha validation parameter not found, do nothing");
+			// validate the request against CaptchaServiceProxy
+			boolean valid = false;
+
+			logger.debug("try to validate");
+			valid = this.captchaService.validateRequest(request);
+			logger.debug("captchaServiceProxy says : request is valid ="
+					+ valid);
+			if (valid) {
+				logger.debug("update the context");
+				((CaptchaSecurityContext) SecurityContextHolder.getContext())
+						.setHuman();
+
+			}
+
+		} else {
+			logger.debug("captcha validation parameter not found, do nothing");
+		}
+		logger.debug("chain...");
+		chain.doFilter(request, response);
+	}
+
+	/**
+	 * Does nothing. We use IoC container lifecycle services instead.
+	 * 
+	 * @param filterConfig
+	 *            ignored
+	 * 
+	 * @throws ServletException
+	 *             ignored
+	 */
+	public void init(FilterConfig filterConfig) throws ServletException {
+	}
+}

+ 536 - 0
core/src/test/java/org/acegisecurity/captcha/CaptchaChannelProcessorTests.java

@@ -0,0 +1,536 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.captcha;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import junit.framework.TestCase;
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.MockFilterChain;
+import net.sf.acegisecurity.SecurityConfig;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/** 
+ * Tests {@link CaptchaChannelProcessor} 
+ * @author marc antoine Garrigue
+ * @version $Id$
+ */
+public class CaptchaChannelProcessorTests extends TestCase {
+
+	public void testDecideRequestsFirstTestRequests() throws Exception {
+		ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
+		cad.addConfigAttribute(new SecurityConfig("SOME_IGNORED_ATTRIBUTE"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_REQUESTS"));
+
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		CaptchaEntryPoint epoint = new CaptchaEntryPoint();
+		epoint.setCaptchaFormUrl("/jcaptcha.do");
+		processor.setEntryPoint(epoint);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setQueryString("info=true");
+		request.setServerName("localhost");
+		request.setContextPath("/demo");
+		request.setServletPath("/restricted");
+		request.setScheme("http");
+		request.setServerPort(8000);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+		processor.decide(fi, cad);
+		assertEquals(response.getRedirectedUrl(),
+				"http://localhost:8000/demo/jcaptcha.do");
+
+		processor.setMaxRequestsBeforeFirstTest(1);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(response.getRedirectedUrl(), null);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(response.getRedirectedUrl(),
+				"http://localhost:8000/demo/jcaptcha.do");
+
+		processor.setMaxRequestsBeforeFirstTest(2);
+		processor.setMaxMillisBeforeReTest(0);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+	}
+
+	public void testDecideRequestsFirstTestMillis() throws Exception {
+		ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
+		cad.addConfigAttribute(new SecurityConfig("SOME_IGNORED_ATTRIBUTE"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_MILLIS"));
+
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		CaptchaEntryPoint epoint = new CaptchaEntryPoint();
+		epoint.setCaptchaFormUrl("/jcaptcha.do");
+		processor.setEntryPoint(epoint);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setQueryString("info=true");
+		request.setServerName("localhost");
+		request.setContextPath("/demo");
+		request.setServletPath("/restricted");
+		request.setScheme("http");
+		request.setServerPort(8000);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+		processor.decide(fi, cad);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeFirstTest(1);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeFirstTest(2);
+		processor.setMaxRequestsBeforeReTest(0);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+	}
+
+	public void testDecideRequestsReTest() throws Exception {
+		ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
+		cad.addConfigAttribute(new SecurityConfig("SOME_IGNORED_ATTRIBUTE"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_REQUESTS"));
+
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		CaptchaEntryPoint epoint = new CaptchaEntryPoint();
+		epoint.setCaptchaFormUrl("/jcaptcha.do");
+		processor.setEntryPoint(epoint);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setQueryString("info=true");
+		request.setServerName("localhost");
+		request.setContextPath("/demo");
+		request.setServletPath("/restricted");
+		request.setScheme("http");
+		request.setServerPort(8000);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+		processor.decide(fi, cad);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeFirstTest(1);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(response.getRedirectedUrl(), null);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeReTest(2);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxMillisBeforeReTest(0);
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+	}
+
+	private MockHttpServletResponse decideWithNewResponse(
+			ConfigAttributeDefinition cad, CaptchaChannelProcessor processor,
+			MockHttpServletRequest request) throws IOException,
+			ServletException {
+		MockHttpServletResponse response;
+		MockFilterChain chain;
+		FilterInvocation fi;
+		response = new MockHttpServletResponse();
+		chain = new MockFilterChain();
+		fi = new FilterInvocation(request, response, chain);
+		processor.decide(fi, cad);
+		return response;
+	}
+
+	public void testDecideRejectsNulls() throws Exception {
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		processor.setEntryPoint(new CaptchaEntryPoint());
+		processor.afterPropertiesSet();
+
+		try {
+			processor.decide(null, null);
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+			assertTrue(true);
+		}
+	}
+
+	public void testDecideMillis() throws Exception {
+		ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
+		cad.addConfigAttribute(new SecurityConfig("SOME_IGNORED_ATTRIBUTE"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_MILLIS"));
+
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		CaptchaEntryPoint epoint = new CaptchaEntryPoint();
+		epoint.setCaptchaFormUrl("/jcaptcha.do");
+		processor.setEntryPoint(epoint);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setQueryString("info=true");
+		request.setServerName("localhost");
+		request.setContextPath("/demo");
+		request.setServletPath("/restricted");
+		request.setScheme("http");
+		request.setServerPort(8000);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+		processor.decide(fi, cad);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeFirstTest(1);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(response.getRedirectedUrl(), null);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxMillisBeforeReTest(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		Thread.sleep(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeReTest(0);
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		Thread.sleep(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		Thread.sleep(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+	}
+
+	public void testDecideBoth() throws Exception {
+		ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
+		cad.addConfigAttribute(new SecurityConfig("SOME_IGNORED_ATTRIBUTE"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_MILLIS"));
+		cad.addConfigAttribute(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_REQUESTS"));
+
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		CaptchaEntryPoint epoint = new CaptchaEntryPoint();
+		epoint.setCaptchaFormUrl("/jcaptcha.do");
+		processor.setEntryPoint(epoint);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setQueryString("info=true");
+		request.setServerName("localhost");
+		request.setContextPath("/demo");
+		request.setServletPath("/restricted");
+		request.setScheme("http");
+		request.setServerPort(8000);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		MockFilterChain chain = new MockFilterChain();
+		FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+		processor.decide(fi, cad);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxRequestsBeforeFirstTest(1);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(response.getRedirectedUrl(), null);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		processor.setMaxMillisBeforeReTest(100);
+		processor.setMaxRequestsBeforeReTest(2);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		Thread.sleep(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		context.setHuman();
+		SecurityContextHolder.setContext(context);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals(null, response.getRedirectedUrl());
+
+		Thread.sleep(100);
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+
+		response = decideWithNewResponse(cad, processor, request);
+		assertEquals("http://localhost:8000/demo/jcaptcha.do", response
+				.getRedirectedUrl());
+	}
+
+	public void testGettersSetters() {
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		assertEquals("REQUIRES_HUMAN_AFTER_MAX_MILLIS", processor
+				.getRequiresHumanAfterMaxMillisKeyword());
+		processor.setRequiresHumanAfterMaxMillisKeyword("X");
+		assertEquals("X", processor.getRequiresHumanAfterMaxMillisKeyword());
+
+		assertEquals("REQUIRES_HUMAN_AFTER_MAX_REQUESTS", processor
+				.getRequiresHumanAfterMaxRequestsKeyword());
+		processor.setRequiresHumanAfterMaxRequestsKeyword("Y");
+		assertEquals("Y", processor.getRequiresHumanAfterMaxRequestsKeyword());
+
+		assertEquals(0, processor.getMaxRequestsBeforeFirstTest());
+		processor.setMaxRequestsBeforeFirstTest(1);
+		assertEquals(1, processor.getMaxRequestsBeforeFirstTest());
+
+		assertEquals(-1, processor.getMaxRequestsBeforeReTest());
+		processor.setMaxRequestsBeforeReTest(11);
+		assertEquals(11, processor.getMaxRequestsBeforeReTest());
+
+		assertEquals(-1, processor.getMaxMillisBeforeReTest());
+		processor.setMaxMillisBeforeReTest(111);
+		assertEquals(111, processor.getMaxMillisBeforeReTest());
+
+		assertTrue(processor.getEntryPoint() == null);
+		processor.setEntryPoint(new CaptchaEntryPoint());
+		assertTrue(processor.getEntryPoint() != null);
+	}
+
+	public void testMissingEntryPoint() throws Exception {
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		processor.setEntryPoint(null);
+
+		try {
+			processor.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+			assertEquals("entryPoint required", expected.getMessage());
+		}
+	}
+
+	public void testMissingKeyword() throws Exception {
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		processor.setRequiresHumanAfterMaxMillisKeyword(null);
+
+		try {
+			processor.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+
+		}
+		processor.setRequiresHumanAfterMaxMillisKeyword("");
+
+		try {
+			processor.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+
+		}
+		processor.setRequiresHumanAfterMaxRequestsKeyword("");
+
+		try {
+			processor.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+
+		}
+
+		processor.setRequiresHumanAfterMaxRequestsKeyword(null);
+
+		try {
+			processor.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+
+		}
+
+	}
+
+	public void testSupports() {
+		CaptchaChannelProcessor processor = new CaptchaChannelProcessor();
+		assertTrue(processor.supports(new SecurityConfig(processor
+				.getRequiresHumanAfterMaxMillisKeyword())));
+		assertTrue(processor.supports(new SecurityConfig(processor
+				.getRequiresHumanAfterMaxRequestsKeyword())));
+
+		assertTrue(processor.supports(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_REQUESTS")));
+		assertTrue(processor.supports(new SecurityConfig(
+				"REQUIRES_HUMAN_AFTER_MAX_MILLIS")));
+
+		assertFalse(processor.supports(null));
+
+		assertFalse(processor.supports(new SecurityConfig("NOT_SUPPORTED")));
+	}
+
+}

+ 385 - 0
core/src/test/java/org/acegisecurity/captcha/CaptchaEntryPointTests.java

@@ -0,0 +1,385 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.captcha;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+import net.sf.acegisecurity.MockPortResolver;
+import net.sf.acegisecurity.securechannel.RetryWithHttpEntryPoint;
+import net.sf.acegisecurity.util.PortMapperImpl;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/**
+ * Tests {@link RetryWithHttpEntryPoint}.
+ * 
+ * @author Ben Alex
+ * @version $Id: RetryWithHttpEntryPointTests.java,v 1.4 2005/04/11 01:07:02
+ *          luke_t Exp $
+ */
+public class CaptchaEntryPointTests extends TestCase {
+	// ~ Methods
+	// ================================================================
+
+	public final void setUp() throws Exception {
+		super.setUp();
+	}
+
+	public static void main(String[] args) {
+		junit.textui.TestRunner.run(CaptchaEntryPointTests.class);
+	}
+
+	public void testDetectsMissingCaptchaFormUrl() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(80, 443));
+
+		try {
+			ep.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+			assertEquals("captchaFormUrl must be specified", expected
+					.getMessage());
+		}
+	}
+
+	public void testDetectsMissingPortMapper() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("xxx");
+		ep.setPortMapper(null);
+
+		try {
+			ep.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+			assertEquals("portMapper must be specified", expected.getMessage());
+		}
+	}
+
+	public void testDetectsMissingPortResolver() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("xxx");
+		ep.setPortResolver(null);
+
+		try {
+			ep.afterPropertiesSet();
+			fail("Should have thrown IllegalArgumentException");
+		} catch (IllegalArgumentException expected) {
+			assertEquals("portResolver must be specified", expected
+					.getMessage());
+		}
+
+	}
+
+	public void testGettersSetters() {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(8080, 8443));
+		assertEquals("/hello", ep.getCaptchaFormUrl());
+		assertTrue(ep.getPortMapper() != null);
+		assertTrue(ep.getPortResolver() != null);
+
+		assertEquals("originalRequest", ep.getOriginalRequestParameterName());
+		ep.setOriginalRequestParameterName("Z");
+		assertEquals("Z", ep.getOriginalRequestParameterName());
+
+		assertEquals(false, ep.isIncludeOriginalRequest());
+		ep.setIncludeOriginalRequest(true);
+		assertEquals(true, ep.isIncludeOriginalRequest());
+
+		assertEquals(false, ep.isOutsideWebApp());
+		ep.setOutsideWebApp(true);
+		assertEquals(true, ep.isOutsideWebApp());
+
+		ep.setForceHttps(false);
+		assertFalse(ep.getForceHttps());
+		ep.setForceHttps(true);
+		assertTrue(ep.getForceHttps());
+
+	}
+
+	public void testHttpsOperationFromOriginalHttpUrl() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setScheme("http");
+		request.setServerName("www.example.com");
+		request.setContextPath("/bigWebApp");
+		request.setServerPort(80);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setForceHttps(true);
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(80, 443));
+		ep.afterPropertiesSet();
+
+		ep.commence(request, response);
+		assertEquals("https://www.example.com/bigWebApp/hello", response
+				.getRedirectedUrl());
+
+		request.setServerPort(8080);
+		response = new MockHttpServletResponse();
+		ep.setPortResolver(new MockPortResolver(8080, 8443));
+		ep.commence(request, response);
+		assertEquals("https://www.example.com:8443/bigWebApp/hello", response
+				.getRedirectedUrl());
+
+		// Now test an unusual custom HTTP:HTTPS is handled properly
+		request.setServerPort(8888);
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals("https://www.example.com:8443/bigWebApp/hello", response
+				.getRedirectedUrl());
+
+		PortMapperImpl portMapper = new PortMapperImpl();
+		Map map = new HashMap();
+		map.put("8888", "9999");
+		portMapper.setPortMappings(map);
+		response = new MockHttpServletResponse();
+
+		ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setForceHttps(true);
+		ep.setPortMapper(portMapper);
+		ep.setPortResolver(new MockPortResolver(8888, 9999));
+		ep.afterPropertiesSet();
+
+		ep.commence(request, response);
+		assertEquals("https://www.example.com:9999/bigWebApp/hello", response
+				.getRedirectedUrl());
+	}
+
+	public void testHttpsOperationFromOriginalHttpsUrl() throws Exception {
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setScheme("https");
+		request.setServerName("www.example.com");
+		request.setContextPath("/bigWebApp");
+		request.setServerPort(443);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setForceHttps(true);
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(80, 443));
+		ep.afterPropertiesSet();
+
+		ep.commence(request, response);
+		assertEquals("https://www.example.com/bigWebApp/hello", response
+				.getRedirectedUrl());
+
+		request.setServerPort(8443);
+		response = new MockHttpServletResponse();
+		ep.setPortResolver(new MockPortResolver(8080, 8443));
+		ep.commence(request, response);
+		assertEquals("https://www.example.com:8443/bigWebApp/hello", response
+				.getRedirectedUrl());
+	}
+
+	public void testNormalOperation() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(80, 443));
+		ep.afterPropertiesSet();
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setContextPath("/bigWebApp");
+		request.setScheme("http");
+		request.setServerName("www.example.com");
+		request.setContextPath("/bigWebApp");
+		request.setServerPort(80);
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		ep.afterPropertiesSet();
+		ep.commence(request, response);
+		assertEquals("http://www.example.com/bigWebApp/hello", response
+				.getRedirectedUrl());
+	}
+
+	public void testOperationWhenHttpsRequestsButHttpsPortUnknown()
+			throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		ep.setPortMapper(new PortMapperImpl());
+		ep.setPortResolver(new MockPortResolver(8888, 1234));
+		ep.setForceHttps(true);
+		ep.afterPropertiesSet();
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setContextPath("/bigWebApp");
+		request.setScheme("http");
+		request.setServerName("www.example.com");
+		request.setContextPath("/bigWebApp");
+		request.setServerPort(8888); // NB: Port we can't resolve
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		ep.afterPropertiesSet();
+		ep.commence(request, response);
+
+		// Response doesn't switch to HTTPS, as we didn't know HTTP port 8888 to
+		// HTTP port mapping
+		assertEquals("http://www.example.com:8888/bigWebApp/hello", response
+				.getRedirectedUrl());
+	}
+
+	public void testOperationWithOriginalRequestIncludes() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("/hello");
+		PortMapperImpl mapper = new PortMapperImpl();
+		mapper.getTranslatedPortMappings().put(new Integer(8888),
+				new Integer(1234));
+		ep.setPortMapper(mapper);
+
+		ep.setPortResolver(new MockPortResolver(8888, 1234));
+		ep.setIncludeOriginalRequest(true);
+		ep.afterPropertiesSet();
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setScheme("http");
+		request.setServerName("www.example.com");
+		// request.setContextPath("/bigWebApp");
+		// TODO correct this when the getRequestUrl from mock works...
+
+		request.setServerPort(8888); // NB: Port we can't resolve
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		ep.afterPropertiesSet();
+		ep.commence(request, response);
+		assertEquals(
+				"http://www.example.com:8888/hello?originalRequest=http://www.example.com:8888/some_path",
+				response.getRedirectedUrl());
+
+		// test the query params
+		request.addParameter("name", "value");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"http://www.example.com:8888/hello?originalRequest=http://www.example.com:8888/some_path?name=value",
+				response.getRedirectedUrl());
+
+		// test the multiple query params
+		request.addParameter("name", "value");
+		request.addParameter("name1", "value2");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"http://www.example.com:8888/hello?originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+		// test add parameter to captcha form url??
+
+		ep.setCaptchaFormUrl("/hello?toto=titi");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"http://www.example.com:8888/hello?toto=titi&originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+		// with forcing!!!
+		ep.setForceHttps(true);
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.example.com:1234/hello?toto=titi&originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+	}
+
+	public void testOperationWithOutsideWebApp() throws Exception {
+		CaptchaEntryPoint ep = new CaptchaEntryPoint();
+		ep.setCaptchaFormUrl("https://www.jcaptcha.net/dotest/");
+		PortMapperImpl mapper = new PortMapperImpl();
+		mapper.getTranslatedPortMappings().put(new Integer(8888),
+				new Integer(1234));
+		ep.setPortMapper(mapper);
+
+		ep.setPortResolver(new MockPortResolver(8888, 1234));
+		ep.setIncludeOriginalRequest(true);
+		ep.setOutsideWebApp(true);
+
+		ep.afterPropertiesSet();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request.setRequestURI("/some_path");
+		request.setScheme("http");
+		request.setServerName("www.example.com");
+		// request.setContextPath("/bigWebApp");
+		// TODO correct this when the getRequestUrl from mock works...
+
+		request.setServerPort(8888); // NB: Port we can't resolve
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+
+		ep.afterPropertiesSet();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.jcaptcha.net/dotest/?originalRequest=http://www.example.com:8888/some_path",
+				response.getRedirectedUrl());
+
+		// test the query params
+		request.addParameter("name", "value");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.jcaptcha.net/dotest/?originalRequest=http://www.example.com:8888/some_path?name=value",
+				response.getRedirectedUrl());
+
+		// test the multiple query params
+		request.addParameter("name", "value");
+		request.addParameter("name1", "value2");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.jcaptcha.net/dotest/?originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+		// test add parameter to captcha form url??
+
+		ep.setCaptchaFormUrl("https://www.jcaptcha.net/dotest/?toto=titi");
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.jcaptcha.net/dotest/?toto=titi&originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+		// with forcing!!!
+		ep.setForceHttps(true);
+		response = new MockHttpServletResponse();
+		ep.commence(request, response);
+		assertEquals(
+				"https://www.jcaptcha.net/dotest/?toto=titi&originalRequest=http://www.example.com:8888/some_path?name=value&name1=value2",
+				response.getRedirectedUrl());
+
+	}
+
+}

+ 64 - 0
core/src/test/java/org/acegisecurity/captcha/CaptchaSecurityContextImplTests.java

@@ -0,0 +1,64 @@
+package net.sf.acegisecurity.captcha;
+
+import net.sf.acegisecurity.context.SecurityContextImplTests;
+
+public class CaptchaSecurityContextImplTests extends SecurityContextImplTests {
+
+	public void testDefaultValues() {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		assertEquals("should not be human", false, context.isHuman());
+		assertEquals("should be 0", 0, context
+				.getLastPassedCaptchaDateInMillis());
+		assertEquals("should be 0", 0, context
+				.getHumanRestrictedResourcesRequestsCount());
+	}
+
+	public void testSetHuman() {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		long now = System.currentTimeMillis();
+		context.setHuman();
+		assertEquals("should be human", true, context.isHuman());
+		assertTrue("should be more than 0", context
+				.getLastPassedCaptchaDateInMillis()
+				- now >= 0);
+		assertTrue("should be less than 0,1 seconde", context
+				.getLastPassedCaptchaDateInMillis()
+				- now < 100);
+		assertEquals("should be 0", 0, context
+				.getHumanRestrictedResourcesRequestsCount());
+	}
+
+	public void testIncrementRequests() {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		context.setHuman();
+		assertEquals("should be human", true, context.isHuman());
+		assertEquals("should be 0", 0, context
+				.getHumanRestrictedResourcesRequestsCount());
+		context.incrementHumanRestrictedRessoucesRequestsCount();
+		assertEquals("should be 1", 1, context
+				.getHumanRestrictedResourcesRequestsCount());
+	}
+
+	public void testResetHuman() {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		context.setHuman();
+		assertEquals("should be human", true, context.isHuman());
+		assertEquals("should be 0", 0, context
+				.getHumanRestrictedResourcesRequestsCount());
+		context.incrementHumanRestrictedRessoucesRequestsCount();
+		assertEquals("should be 1", 1, context
+				.getHumanRestrictedResourcesRequestsCount());
+		long now = System.currentTimeMillis();
+		context.setHuman();
+		assertEquals("should be 0", 0, context
+				.getHumanRestrictedResourcesRequestsCount());
+		assertTrue("should be more than 0", context
+				.getLastPassedCaptchaDateInMillis()
+				- now >= 0);
+		assertTrue("should be less than 0,1 seconde", context
+				.getLastPassedCaptchaDateInMillis()
+				- now < 100);
+
+	}
+
+}

+ 83 - 0
core/src/test/java/org/acegisecurity/captcha/CaptchaValidationProcessingFilterTests.java

@@ -0,0 +1,83 @@
+package net.sf.acegisecurity.captcha;
+
+import junit.framework.TestCase;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+import net.sf.acegisecurity.util.MockFilterChain;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+
+public class CaptchaValidationProcessingFilterTests extends TestCase {
+
+	/*
+	 */
+	public void testAfterPropertiesSet() throws Exception {
+		CaptchaValidationProcessingFilter filter = new CaptchaValidationProcessingFilter();
+
+		try {
+			filter.afterPropertiesSet();
+			fail("should have thrown an invalid argument exception");
+		} catch (Exception e) {
+			assertTrue("should be an InvalidArgumentException",
+					IllegalArgumentException.class.isAssignableFrom(e
+							.getClass()));
+		}
+		filter.setCaptchaService(new MockCaptchaServiceProxy());
+		filter.afterPropertiesSet();
+
+	}
+
+	/*
+	 * Test method for
+	 * 'net.sf.acegisecurity.captcha.CaptchaValidationProcessingFilter.doFilter(ServletRequest,
+	 * ServletResponse, FilterChain)'
+	 */
+	public void testDoFilterWithoutRequestParameter() throws Exception {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		CaptchaValidationProcessingFilter filter = new CaptchaValidationProcessingFilter();
+		MockCaptchaServiceProxy service = new MockCaptchaServiceProxy();
+		MockFilterChain chain = new MockFilterChain(true);
+		filter.setCaptchaService(service);
+		filter.doFilter(request, null, chain);
+		assertFalse("proxy should not have been called", service.hasBeenCalled);
+		assertFalse("context should not have been updated", context.isHuman());
+		// test with valid
+		service.valid = true;
+		filter.doFilter(request, null, chain);
+		assertFalse("proxy should not have been called", service.hasBeenCalled);
+		assertFalse("context should not have been updated", context.isHuman());
+
+	}
+
+	/*
+	 * Test method for
+	 * 'net.sf.acegisecurity.captcha.CaptchaValidationProcessingFilter.doFilter(ServletRequest,
+	 * ServletResponse, FilterChain)'
+	 */
+	public void testDoFilterWithRequestParameter() throws Exception {
+		CaptchaSecurityContext context = new CaptchaSecurityContextImpl();
+		SecurityContextHolder.setContext(context);
+
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		request
+				.addParameter(
+						CaptchaValidationProcessingFilter.CAPTCHA_VALIDATION_SECURITY_PARAMETER_KEY,
+						"");
+
+		CaptchaValidationProcessingFilter filter = new CaptchaValidationProcessingFilter();
+		MockCaptchaServiceProxy service = new MockCaptchaServiceProxy();
+		MockFilterChain chain = new MockFilterChain(true);
+		filter.setCaptchaService(service);
+		filter.doFilter(request, null, chain);
+		assertTrue("should have been called", service.hasBeenCalled);
+		assertFalse("context should not have been updated", context.isHuman());
+		// test with valid
+		service.valid = true;
+		filter.doFilter(request, null, chain);
+		assertTrue("should have been called", service.hasBeenCalled);
+		assertTrue("context should have been updated", context.isHuman());
+
+	}
+
+}

+ 32 - 0
core/src/test/java/org/acegisecurity/captcha/MockCaptchaServiceProxy.java

@@ -0,0 +1,32 @@
+/* Copyright 2004 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.captcha;
+
+import javax.servlet.ServletRequest;
+
+public class MockCaptchaServiceProxy implements CaptchaServiceProxy {
+
+	public boolean valid = false;
+
+	public boolean hasBeenCalled = false;
+
+	public boolean validateRequest(ServletRequest request) {
+		hasBeenCalled = true;
+		return valid;
+
+	}
+
+}