ReCaptcha login form with Spring Security

Today I’ll show you how to customize Spring Security to create a login form with ReCaptcha verification

The captcha is based on Google ReCaptcha plugin. More info on ReCaptcha here

The following implementation is based on Spring MVC & ThymeLeaf. If you are using another web framework, you’ll need to adapt a little bit the code

I Features description

The final login form looks like this:

ReCaptcha_Login

Below is the functional requirements for such a login form:

  • Do not attempt to verify ReCaptcha response if the user input is blank
  • Check ReCaptcha first before any attempt to verify login/password
  • Return a default error message if the Recaptcha verification fails. Pre-fill the login field with previous value but reset the password field
  • Return a default error message if login/password verification fails

The requirements are quite simple. I will not explain in details the ReCaptcha verification process because it is out of the scope of this post. You can have more details on their documentation page

 

II Code Implementation

A Custom Authentication filter

Let’s see how we can implements the requirement with a custom Authentication filter, extending the existing UsernamePasswordAuthenticationFilter

public class ReCaptchaAuthenticationFilter extends UsernamePasswordAuthenticationFilter implements InitializingBean
{
	private final String CAPTCHA_CHALLENGE_FIELD = "recaptcha_challenge_field";
	private final String CAPTCHA_RESPONSE_FIELD = "recaptcha_response_field";
	private final ReCaptchaImpl reCaptcha;
	private String privateKey;
	private final Logger log = LoggerFactory.getLogger(ReCaptchaAuthenticationFilter.class);

	public ReCaptchaAuthenticationFilter() {
		this.reCaptcha = new ReCaptchaImpl();
	}

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException
	{

		String reCaptchaChallenge = request.getParameter(CAPTCHA_CHALLENGE_FIELD);
		String reCaptchaResponse = request.getParameter(CAPTCHA_RESPONSE_FIELD);
		String remoteAddress = request.getRemoteAddr();

		if (!StringUtils.isEmpty(reCaptchaChallenge))
		{
			log.debug("ReCaptcha Challenge not null");
			if (!StringUtils.isEmpty(reCaptchaResponse))
			{
				log.debug("ReCaptcha Answser not null, call ReCaptcha to verify it");
				ReCaptchaResponse reCaptchaCheck = reCaptcha.checkAnswer(remoteAddress, reCaptchaChallenge, reCaptchaResponse);

				if (reCaptchaCheck.isValid())
				{
					log.debug("ReCaptcha answer is valid, attempt authentication");
					return super.attemptAuthentication(request, response);
				}
				else
				{
					this.reCaptchaError(request, response, "ReCaptcha failed : " + reCaptchaCheck.getErrorMessage());
					return null;
				}
			}
			else
			{
				this.reCaptchaError(request, response, "ReCaptcha failed : empty answer");
				return null;
			}

		}
		else
		{
			return super.attemptAuthentication(request, response);
		}
	}

	private void reCaptchaError(HttpServletRequest request, HttpServletResponse response, String errorMsg)
	{
		log.error("ReCaptcha failed : " + errorMsg);
		try
		{

			RequestDispatcher dispatcher = request.getRequestDispatcher("/login?error=2");

			dispatcher.forward(request, response);
		}
		catch (ServletException e)
		{
			throw new AuthenticationServiceException("ReCaptcha failed : " + errorMsg);
		}
		catch (IOException e)
		{
			throw new AuthenticationServiceException("Recaptcha failed : " + errorMsg);
		}
	}

	public void setPrivateKey(String privateKey)
	{
		this.privateKey = privateKey;
	}

	public void afterPropertiesSet()
	{
		if (StringUtils.isEmpty(this.privateKey))
		{
			throw new IllegalArgumentException("The 'privateKey' should be set for the bean type 'ReCaptchaAuthenticationFilter'");
		}
		else
		{
			reCaptcha.setPrivateKey(this.privateKey);
		}
	}
}

The implementation is quite simple.

  • If CAPTCHA_CHALLENGE_FIELD is blank (the requested page is not the login page), call the super.attemptAuthentication() method
  • Else

    • If the CAPTCHA_RESPONSE_FIELD is empty (the user does not answer the ReCaptcha), forward the user to the login page with the error code 2
    • Else

      • If the ReCaptcha verification fails, forward the user to the login page with the error code 2
      • Else call the super.attemptAuthentication() method

Please note the injection of the private key for ReCaptcha plugin and the initialization of the ReCaptchaImpl instance in the constructor.

 

B Spring MVC Controller for login page

@RequestMapping("/login")
public String loginPage(@RequestParam(value = "error", required = false) Integer errorCode, Model model, HttpServletRequest servletRequest)
{
	log.info("Is current device mobile : " + currentDevice.isMobile());
	if (errorCode != null)
	{
		if (errorCode.equals(1))
		{
			model.addAttribute("authenticationError", true);
		}
		else if (errorCode.equals(2))
		{
			String login = servletRequest.getParameter("j_username");
			model.addAttribute("reCaptchaError", true);
			model.addAttribute("login", login);
		}
	}

	return "pages/login.html";
}

In the controller class, we extract the request parameter error (if any) and, depending on its value, set the authenticationError or reCaptchaError value to the model to be passed to the view handler.

Please notice the required = false setting on the @RequestParam(value = “error”, required = false). If not set (default value is true) Spring MVC will raise an error if the parameter is not found, which we don’t want.

 

C Login form design

Below is the design of the login form. Please notice that I use ThymeLeaf and Twitter Bootstrap.

            <h1>Authentication</h1>
            <form action="authentication" th:action="@{/authentication}" method="post" class="well center">
            	
            	<br th:if="${authenticationError}"/>
            	<div th:if="${authenticationError}" class="alert alert-error">
            		Incorrect Login/Password
            	</div>
            	<br th:if="${authenticationError}"/>
            	
            	<br th:if="${reCaptchaError}"/>
            	<div th:if="${reCaptchaError}" class="alert alert-error">
            		Incorrect reCaptcha
            	</div>
            	<br th:if="${reCaptchaError}"/>
            	
                <fieldset>
                    <label>Login :</label> 
                    	<input id="j_username" name="j_username"
                        	type="text" required="required" autofocus="autofocus" class="input span3"
                        	th:value="${login}"
                            placeholder="Your login..."/>
                    <label>Password :</label> 
                    	<input id="j_password" name="j_password"
                        	type="password" required="required" class="input span3"
                            placeholder="Your password..."/>
				<label class="checkbox left">
	                <input type="checkbox"
	                       name="_spring_security_remember_me" 
	                       id="_spring_security_remember_me"
	                       value="true" />&nbsp;Remember me
            	</label>
                                                     
                </fieldset>
                <br/>
                Additional anti-bot authentication
                <br/>
                <script type="text/javascript"
     				src="http://www.google.com/recaptcha/api/challenge?k=your_public_recaptcha_key">
                </script>
                <noscript>
					< iframe src="http://www.google.com/recaptcha/api/noscript?k=your_public_recaptcha_key" height="300" width="500" > < /iframe >
					<br/>
					<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
					<input type="hidden" name="recaptcha_response_field" value="manual_challenge"></input>
				</noscript>
				<br/>
				<button type="submit" class="btn btn-success">Authenticate</button>
            </form>

Notice the check for authenticationError and reCaptchaError attribute for error message display (lines 5 & 11)
 

III Spring Security configuration

A Full XML config

Below is the full XML config to use the above custom ReCaptchaAuthenticationFilter

	<bean id="authenticationProcessingFilter" class="fr.doan.security.ReCaptchaAuthenticationFilter">
  		<property name="authenticationManager" ref="authenticationManager"/>
  		<property name="filterProcessesUrl" value="/authentication"/>
  		<property name="authenticationSuccessHandler">
	        <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
	        	<property name="alwaysUseDefaultTargetUrl" value="true"/>
	            <property name="defaultTargetUrl" value="/home" />
	        </bean>
	    </property>
	    <property name="authenticationFailureHandler">
	    	<bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
	            <property name="defaultFailureUrl" value="/login?error=1" />
	        </bean>
	    </property>
	    <property name="privateKey" value="your_private_recaptcha_key"/>
	</bean> 

The configuration is quite straightforward. The only major difference with a “classic” UsernamePasswordAuthenticationFilter is the injection of the privateKey (line 15).

 

B Spring Security namespace config

If you’re using the Spring Security namespace, the configuration is a little bit trickier

   <http auto-config="false" 
    	use-expressions="true"
    	entry-point-ref="loginUrlAuthenticationEntryPoint">
    	<custom-filter ref="authenticationProcessingFilter" position="FORM_LOGIN_FILTER"/>
        <intercept-url pattern="/**" access="isAuthenticated()" />
        <!-- 
        <form-login
                login-processing-url="/authentication"
                login-page="/login"
                authentication-failure-url="/login?error=1"
                default-target-url="/home"
                always-use-default-target="true"
		/>
		 -->
        <remember-me user-service-ref="userDetailsService"/>
        <logout logout-url="/logout"
                logout-success-url="/login"/>
    </http>
	
	<beans:bean id="loginUrlAuthenticationEntryPoint"
  		class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
  		<beans:property name="loginFormUrl" value="/login" />
	</beans:bean>
	
	<beans:bean id="authenticationProcessingFilter" class="fr.doan.security.ReCaptchaAuthenticationFilter">
  		<beans:property name="authenticationManager" ref="authenticationManager"/>
  		<beans:property name="filterProcessesUrl" value="/authentication"/>
  		<beans:property name="authenticationSuccessHandler">
	        <beans:bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
	        	<beans:property name="alwaysUseDefaultTargetUrl" value="true"/>
	            <beans:property name="defaultTargetUrl" value="/home" />
	        </beans:bean>
	    </beans:property>
	    <beans:property name="authenticationFailureHandler">
	    	<beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
	            <beans:property name="defaultFailureUrl" value="/login?error=1" />
	        </beans:bean>
	    </beans:property>
	    <beans:property name="privateKey" value="your_private_recaptcha_keyword"/>
	</beans:bean> 
		
    <authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="userDetailsService"/>
    </authentication-manager>

First, we need to declare the same ReCaptchaAuthenticationFilter as previously (line 25). No big change here.

To inject this filter into the security chain, we need to

  • Add a custom filter at the same position as the FORM_LOGIN_FILTER
  • Remove the <form-login> tag (lines 7 to 13) otherwise you’ll end up having the ReCaptchaAuthenticationFilter and the UsernamePasswordAuthenticationFilter at the same time ( the <form-login> tag is translated by Spring Security into a UsernamePasswordAuthenticationFilter)

Since the <form-login> tag is removed, we need to define an entry point for the login process (line 3) by instanciating the LoginUrlAuthenticationEntryPoint separately (line 20)

Last but not least, we need to give an alias to the AuthenticationManager defined with the <authentication-manager> tag. By default this tag is read and converted into a bean declaration with the default name ‘org.springframework.security.authenticationManager’.

Either we use this default name and inject it into our custom RecaptchaAuthenticationFilter, or we define an alias and use this alias for injection (lines 26 & 42).

 
 

1 Comment

  1. geoffreydv

    Thanks for this great tutorial. I had to tweak the redirect as it was not working for me. I have changed:

    RequestDispatcher dispatcher = request.getRequestDispatcher(“/login?error=2”);
    dispatcher.forward(request, response);

    to:

    response.sendRedirect(“/login?error=2”);

    Reply

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.