Integrating reCaptcha with CAS

This document describes integrating reCaptcha with CAS. reCaptcha is a free CAPTCHA service that helps to digitize books, newspapers and old time radio shows. This tutorial uses the recaptch4j library to access the reCaptcha service. The version of CAS used in this tutorial is 3.4.11. This tutorial was based on another by Axel Mendoza Pupo that covered JCAPTCHA integration.

login-webflow.xml

The following changes need to be made to WEB-INF/login-webflow.xml:

Replace view-state "viewLoginForm" with:
<view-state id="viewLoginForm" view="casLoginView" model="credentials">
	<binder>
    	<binding property="username" />
        <binding property="password" />
    </binder>
    <on-entry>
    	<set name="viewScope.commandName" value="'credentials'" />
    </on-entry>
    <transition on="submit" bind="true" validate="true" to="realSubmit">
    	<evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
    </transition>
</view-state>
Replace action-state "realSubmit" with:
<action-state id="realSubmit">
	<evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
	<transition on="warn" to="warn" />
	<transition on="success" to="captchaCheck" />
	<transition on="error" to="errorCount" />
</action-state>
Add action-states:
<action-state id="captchaValidate">
	<evaluate expression="captchaValidateAction"/>
    <transition on="success" to="sendTicketGrantingTicket" />
    <transition on="error" to="errorCount" />
</action-state>
    
<action-state id="errorCount">
	<evaluate expression="captchaErrorCountAction"/>
    <transition on="success" to="generateLoginTicket" />
</action-state>
Add decision-state:
<decision-state id="captchaCheck">
	<if test="flowScope.count != null &amp;&amp; flowScope.count >= 3" then="captchaValidate" else="sendTicketGrantingTicket"/>
</decision-state>

Implement CaptchaErrorCountAction

 

CaptchaErrorCountAction.java
package com.acme.cas.captcha;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

public class CaptchaErrorCountAction extends AbstractAction {
    @Override
    protected Event doExecute(RequestContext context) throws Exception {
        int count;
        try {
            count = (Integer) context.getFlowScope().get("count");
        } catch (Exception e) {
            count = 0;
        }
        count++;
        context.getFlowScope().put("count", count);
        return success();
    }
}

 

Implement CaptchaValidateAction

 

CaptchaValidateAction.java
package com.acme.cas.captcha;
import javax.servlet.http.HttpServletRequest;
import net.tanesha.recaptcha.ReCaptchaImpl;
import net.tanesha.recaptcha.ReCaptchaResponse;
import org.apache.commons.lang.StringUtils;
import org.jasig.cas.web.support.WebUtils;
import org.springframework.util.Assert;
import org.springframework.webflow.action.AbstractAction;
import org.springframework.webflow.execution.Event;
import org.springframework.webflow.execution.RequestContext;

public class CaptchaValidateAction extends AbstractAction {
    private ReCaptchaImpl reCaptcha;
    
    public CaptchaValidateAction(ReCaptchaImpl recaptcha) {
        this.reCaptcha = recaptcha;
        Assert.notNull(this.reCaptcha, "ReCaptchaImpl required");
    }
    
    @Override
    protected Event doExecute(RequestContext context) throws Exception {
        HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        String recaptchaChallengeField = request.getParameter("recaptcha_challenge_field");
        String recaptchaResponseField = request.getParameter("recaptcha_response_field");
        String showCaptchaField = request.getParameter("showCaptcha");
        if ((StringUtils.isNotBlank(showCaptchaField)) &&
            !verifyCaptcha(request.getRemoteAddr(), recaptchaChallengeField, recaptchaResponseField)) {
            return error();
        }
        return success();
    }
    
    /**
     * Call recaptcha4j to verify captcha response
     * 
     * @param remoteAddress ip address of the client
     * @param challenge the recaptcha challenge field
     * @param response the recaptcha response field
     * @return boolean indicating verification success or failure
     */
    protected boolean verifyCaptcha(String remoteAddress, String challenge, String response) {        
        ReCaptchaResponse reCaptchaResponse = reCaptcha.checkAnswer(remoteAddress, challenge, response);
        return reCaptchaResponse.isValid();
    }
}
 

Spring Configuration

Add the following file to WEB-INF/spring-configuration:

captcha.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/mvc 
         http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd" >
 
    <description>
      Configuration for reCaptcha
    </description>
  
    <context:annotation-config />
    
	<mvc:annotation-driven />
	<mvc:default-servlet-handler />
 
    <context:property-placeholder location="classpath:/casserver.properties" />
    
    <bean id="reCaptcha" class="net.tanesha.recaptcha.ReCaptchaImpl">
            <property name="privateKey" value="${recaptcha.private.key}" />
            <property name="publicKey" value="${recaptcha.public.key}" />
            <property name="includeNoscript" value="false" />
    </bean>
    
    <bean id="reCaptchaPublicKey" class="java.lang.String">
        <constructor-arg value="${recaptcha.public.key}"/>
    </bean>
        
    <bean id="captchaErrorCountAction" class="com.acme.cas.captcha.CaptchaErrorCountAction"/>
    
    <bean id="captchaValidateAction" class="com.acme.cas.captcha.CaptchaValidateAction">
        <constructor-arg>
            <ref local="reCaptcha"/>
        </constructor-arg>
    </bean>
</beans>

Note the reCaptchaPublicKey is exposed in the context here as a bean for reference in the view.

Add reCaptcha keys to properties file

Add the recaptcha.private.key and recaptcha.public.key properties to src/main/resources/casserver.properties.

casserver.properties
recaptcha.public.key=<your recaptcha public key>
recaptcha.private.key=<your recaptcha private key>

Add reCaptcha to casLoginView.jsp

WEB-INF/view/jsp/default/ui/casLoginView.jsp
<c:if test="${not empty count && count >= 3}">
	<div class="error"><p>Multiple failed login attempts detected!  Please enter the letters shown in the image below to prove you are human.</p></div>
    <div id="captcha"></div>
    <!--[if lte IE 6]>
    <script type="text/javascript" src="https://www.google.com/recaptcha/api/challenge?k=<spring:eval expression="@reCaptchaPublicKey"/>"></script>
    <script type="text/javascript">
    	var isRunningIE6 = true;                              
    </script>
    <![endif]-->
    <script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
    <script>
    	function showCaptcha() {
        	if (typeof(isRunningIE6) === 'undefined') {
            	Recaptcha.create('<spring:eval expression="@reCaptchaPublicKey"/>', "captcha", {
                	theme: 'white',
                    callback: Recaptcha.focus_response_field,
                    tabindex: 2
                });
            }
        }
        showCaptcha();
    </script>                            
    <input type="hidden" name="showCaptcha" value="true" />
</c:if>

Note the reCaptchaPublicKey bean is referenced here.

Add reCaptcha4j dependency in pom.xml

pom.xml
<dependency>
	<groupId>net.tanesha.recaptcha4j</groupId>
    <artifactId>recaptcha4j</artifactId>
    <version>0.0.7</version>
</dependency>