How to Integrate JCaptcha in Spring Security

11,844

Solution 1

Problem SOLVED! I've found the answer. So we don't need the CaptchaVerifierFilter after all. I validate the captcha inside AuthenticationProvider.

these are the list of changes:

in spring-security.xml, this one

<!--JCaptcha Filtering-->
<custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
<custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>

become this one

<!--JCaptcha Filtering-->
<custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>

remove

<!-- For verifying CAPTCHA fields -->
<!-- Private key is assigned by the JCaptcha service -->
<beans:bean id="captchaVerifierFilter" class="com.util.CaptchaVerifierFilter"
  p:failureUrl="/session/loginfailed/"
  p:captchaCaptureFilter-ref="captchaCaptureFilter"/>

and validate the captcha in here

<beans:bean id="customAuthenticationProvider" class="com.pusilkom.artajasa.ecr.backend.util.MyAuthenticationProvider"
            p:captchaCaptureFilter-ref="captchaCaptureFilter"/>

Solution 2

I am not sure if it is correct way of doing but its works for me perfectly. I have created same classes as yours with few changes in the filter class code and a small change in security-context.xml.

  1. I validate the captcha in CaptchaCaptureFilter and store the validation result in variable iscaptchaPassed which a property in CaptchaCaptureFilter. Along with iscaptchaPassed I also store response as captchaResponse to check whether it is null later in CaptchaVerifierFilter.

public class CaptchaCaptureFilter extends OncePerRequestFilter {

private String captchaResponse;

private boolean iscaptchaPassed;

//setters and getters

@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,FilterChain chain) throws IOException, ServletException {

    logger.info("Captcha capture filter");

    String captcha_Response=req.getParameter("jcaptcha");
    logger.info("response captcha captured : " +captcha_Response);

    if(captcha_Response!=null)
    {
        iscaptchaPassed = SimpleImageCaptchaServlet.validateResponse(req, req.getParameter("jcaptcha"));
        captchaResponse=captcha_Response;
        logger.info("isCaptchaPassed value is "+iscaptchaPassed);
    }

    // Proceed with the remaining filters
    chain.doFilter(req, res);
}
  1. Read the values for captchaCaptureFilter injected in CaptchaVerifyFilter and handle as following.

public class CaptchaVerifierFilter extends OncePerRequestFilter {

protected Logger logger = LoggerFactory.getLogger(Filter.class);

private CaptchaCaptureFilter captchaCaptureFilter;

private String failureUrl;

//getters and setters**strong text**

private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

@Override
public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,FilterChain chain) throws IOException, ServletException {

    //logger.info("Captcha verifier filter");
    boolean captchaPassed=captchaCaptureFilter.getIscaptchaPassed();
    String captchaResponse=captchaCaptureFilter.getCaptchaResponse();
    //logger.info("captcha captured :"+captchaResponse+" validation result of captcha : " +captchaPassed);

    if(captchaResponse!=null)
    {
        if(captchaPassed)
        {
            logger.info("Captcha is valid!");
        }
        else
        {
            logger.info("Captcha is invalid!");
            failureHandler.setDefaultFailureUrl(failureUrl);
            failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid!"));
        }
        resetCaptchaFields();   
    }

    chain.doFilter(req, res);           
}

/** 
 * Reset Captcha fields
 */
public void resetCaptchaFields() {
    captchaCaptureFilter.setCaptchaResponse(null);
    captchaCaptureFilter.setIscaptchaPassed(false);;
}
  1. Change the following in security-context.xml.

security:custom-filter ref="captchaCaptureFilter" before="FIRST"

security:custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"

I am adding captchaCaptureFilter before all the filters where captcha is validated. the result of validation is used after UserNameAndPasswordAuthFilter that is FORM_LOGIN_FILTER.

Share:
11,844
Salingga
Author by

Salingga

Updated on June 04, 2022

Comments

  • Salingga
    Salingga almost 2 years

    Before start answering I know there is ReCaptcha which is simpler and easier, but I can't use that. The production server is not online. So here we go.

    I'm using Spring mvc 3 with spring security on maven Project and weblogic as the web server (jetty while developing). I'll be very specific on this one.

    Before seeing my configurations and files, I'd like to show you the list of my problems:

    • I've tried ReCaptcha before JCaptcha with the same coding structure and it works fine.
    • logger.debug doesn't appear at all in CaptchaCaptureFilter class and/or CaptchaVerifierFilter class (while it appears in ArtajasaAuthenticationProvider class).
    • I can see the captcha image, but whatever the answer is, it always invalid.
    • With the current state, it doesn't work in jetty nor weblogic, but if I change the custom filter position to the one below, it works only in jetty.

      <custom-filter ref="captchaCaptureFilter" position="FIRST"/>
      <custom-filter ref="captchaVerifierFilter" after="FIRST"/>
      

    Thanks for viewing and many thanks for answering my question. Below are the details.

    The repository for JCaptcha is this one:

    <repository>
            <id>sourceforge-releases</id>
            <name>Sourceforge Releases</name>
            <url>https://oss.sonatype.org/content/repositories/sourceforge-releases</url>
        </repository>
    
    <dependency>
            <groupId>com.octo.captcha</groupId>
            <artifactId>jcaptcha-integration-simple-servlet</artifactId>
            <version>2.0-alpha-1</version>
        </dependency>
    

    Here are some configuration I made in .xml files:

    web.xml

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring/spring-security.xml
        </param-value>
    </context-param>
     <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
    
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>
    
    <servlet>
        <servlet-name>jcaptcha</servlet-name>
        <servlet-class>com.octo.captcha.module.servlet.image.SimpleImageCaptchaServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>jcaptcha</servlet-name>
        <url-pattern>/jcaptcha.jpg</url-pattern>
    </servlet-mapping>
    

    spring-security.xml

    <http auto-config="true" use-expressions="true">
        <intercept-url pattern="/resources/**" access="permitAll()" />
        <intercept-url pattern="/jcaptcha.jpg" access="permitAll()" />
        <intercept-url pattern="/**" access="isAuthenticated()" />
    
        <form-login login-page="/session/login/" default-target-url="/"
                    authentication-failure-url="/session/loginfailed/" />
        <logout logout-success-url="/session/logout/" />
        <access-denied-handler error-page="/session/403/" />
    
        <!--JCaptcha Filtering-->
        <custom-filter ref="captchaCaptureFilter" before="FORM_LOGIN_FILTER"/>
        <custom-filter ref="captchaVerifierFilter" after="FORM_LOGIN_FILTER"/>
        <anonymous />
    </http>
    
    <!-- For capturing CAPTCHA fields -->
    <beans:bean id="captchaCaptureFilter" class="com.util.CaptchaCaptureFilter" />
    
    <!-- For verifying CAPTCHA fields -->
    <!-- Private key is assigned by the JCaptcha service -->
    <beans:bean id="captchaVerifierFilter" class="com.util.CaptchaVerifierFilter"
          p:failureUrl="/session/loginfailed/"
          p:captchaCaptureFilter-ref="captchaCaptureFilter"/>
    
    <beans:bean id="customAuthFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
        <beans:property name="sessionAuthenticationStrategy" ref="sas"/>
        <beans:property name="authenticationManager" ref="authenticationManager" />
        <beans:property name="allowSessionCreation" value="true" />
    </beans:bean>
    
    <beans:bean id="sas" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy">
        <beans:constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
        <beans:property name="maximumSessions" value="1" />
    </beans:bean>
    
    <beans:bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" />
    
    <beans:bean id="userService" class="com.service.mybatis.UserManager" />
    
    <beans:bean id="customAuthenticationProvider" class="com.util.ArtajasaAuthenticationProvider" />
    <authentication-manager alias="authenticationManager">
        <authentication-provider ref="customAuthenticationProvider" />
    </authentication-manager>
    
    <beans:bean id="accessDeniedHandler" class="com.util.ThouShaltNoPass">
        <beans:property name="accessDeniedURL" value="/session/403/" />
    </beans:bean>
    

    And these are the java classes:

    ArtajasaAuthenticationProvider.java

    public class ArtajasaAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserService userService;
    private Logger logger = LoggerFactory.getLogger(ArtajasaAuthenticationProvider.class);
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = String.valueOf(authentication.getPrincipal());
        String password = String.valueOf(authentication.getCredentials());
        logger.debug("Checking authentication for user {}", username);
        if (StringUtils.isBlank(username)
                || StringUtils.isBlank(password)) {
            throw new BadCredentialsException("No Username and/or Password Provided.");
        } else {
            Pengguna user = userService.select(username);
            if (user == null) {
                throw new BadCredentialsException("Invalid Username and/or Password.");
            }
            if (user.getPassword().equals(new PasswordUtil().generateHash(password, user.getSalt()))) {
                List<GrantedAuthority> authorityList = (List<GrantedAuthority>) userService.getAuthorities(user);
                return new UsernamePasswordAuthenticationToken(username, password, authorityList);
            } else {
                throw new BadCredentialsException("Invalid Username and/or Password.");
            }
        }
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }
    }
    

    CaptchaCaptureFilter.java

    public class CaptchaCaptureFilter extends OncePerRequestFilter {
    
    protected Logger logger = Logger.getLogger(CaptchaCaptureFilter.class);
    private String userCaptchaResponse;
    private HttpServletRequest request;
    
    @Override
    public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                 FilterChain chain) throws IOException, ServletException {
    
        logger.debug("Captcha capture filter");
    
        // Assign values only when user has submitted a Captcha value.
        // Without this condition the values will be reset due to redirection
        // and CaptchaVerifierFilter will enter an infinite loop
        if (req.getParameter("jcaptcha") != null) {
            request = req;
            userCaptchaResponse = req.getParameter("jcaptcha");
        }
    
        logger.debug("userResponse: " + userCaptchaResponse);
    
        // Proceed with the remaining filters
        chain.doFilter(req, res);
    }
    
    public String getUserCaptchaResponse() {
        return userCaptchaResponse;
    }
    
    public void setUserCaptchaResponse(String userCaptchaResponse) {
        this.userCaptchaResponse = userCaptchaResponse;
    }
    
    public HttpServletRequest getRequest() {
        return request;
    }
    
    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }
    }
    

    CaptchaVerifierFilter.java

    public class CaptchaVerifierFilter extends OncePerRequestFilter {
    
    protected Logger logger = Logger.getLogger(CaptchaVerifierFilter.class);
    private String failureUrl;
    private CaptchaCaptureFilter captchaCaptureFilter;
    
    // Inspired by log output: AbstractAuthenticationProcessingFilter.java:unsuccessfulAuthentication:320)
    // Delegating to authentication failure handlerorg.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@15d4273
    private SimpleUrlAuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
    
    @Override
    public void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                 FilterChain chain) throws IOException, ServletException {
    
        logger.debug("Captcha verifier filter");
        logger.debug("userResponse: " + captchaCaptureFilter.getUserCaptchaResponse());
    
        // Assign values only when user has submitted a Captcha value
        if (captchaCaptureFilter.getUserCaptchaResponse() != null) {
    
            // Send HTTP request to validate user's Captcha
            boolean captchaPassed = SimpleImageCaptchaServlet.validateResponse(captchaCaptureFilter.getRequest(), captchaCaptureFilter.getUserCaptchaResponse());
    
            // Check if valid
            if (!captchaPassed) {
                logger.debug("Captcha is invalid!");
    
                // Redirect user to login page
                failureHandler.setDefaultFailureUrl(failureUrl);
                failureHandler.onAuthenticationFailure(req, res, new BadCredentialsException("Captcha invalid! " + captchaCaptureFilter.getRequest() + " " + captchaCaptureFilter.getUserCaptchaResponse()));
    
            } else {
                logger.debug("Captcha is valid!");
            }
    
            // Reset Captcha fields after processing
            // If this method is skipped, everytime we access a page
            // CaptchaVerifierFilter will infinitely send a request to the Google Captcha service!
            resetCaptchaFields();
        }
    
        // Proceed with the remaining filters
        chain.doFilter(req, res);
    }
    
    /**
     * Reset Captcha fields
     */
    public void resetCaptchaFields() {
        captchaCaptureFilter.setUserCaptchaResponse(null);
    }
    
    public String getFailureUrl() {
        return failureUrl;
    }
    
    public void setFailureUrl(String failureUrl) {
        this.failureUrl = failureUrl;
    }
    
    public CaptchaCaptureFilter getCaptchaCaptureFilter() {
        return captchaCaptureFilter;
    }
    
    public void setCaptchaCaptureFilter(CaptchaCaptureFilter captchaCaptureFilter) {
        this.captchaCaptureFilter = captchaCaptureFilter;
    }
    }
    

    Last but not least, login.jsp

    <%@ taglib prefix='c' uri='http://java.sun.com/jstl/core_rt' %>
    
    <form id="login" name="f" action="<c:url value='/j_spring_security_check'/>" method="POST">
      <div class="container">
    
        <div class="content">
            <div class="row">
                <div class="login-form">
                    <h3>Login</h3>
                    <br />
                      <fieldset>
                           <div class="clearfix">
                                username: ecr
                                <input type="text" name='j_username' value='<c:if test="${not empty param.login_error}"><c:out value="${SPRING_SECURITY_LAST_USERNAME}"/></c:if>' placeholder="[email protected]">
                           </div>
                           <div class="clearfix">
                               password: ecr123
                               <input type="password" name='j_password' placeholder="password">
                           </div>
                           <div class="clearfix">
                               <img src="../../jcaptcha.jpg" />
                               <br />
                               <input type="text" name="jcaptcha" placeholder="masukkan captcha" />
                           </div>
                           <br />
                           <button class="btn btn-primary" type="submit"><i class="icon-lock"></i> Sign in</button>
                       </fieldset>
                </div>
            </div>
        </div>
         <br />
         <c:if test="${not empty error}">
                <div class="alert alert-error">
                <button type="button" class="close" data-dismiss="alert"><i class="icon-remove"></i></button>
                Login Failed, try again.<br />
                <c:out value="${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}"/>
                </div>
              </c:if>
      </div>
    

  • V. Artyukhov
    V. Artyukhov almost 9 years
    could you tell please, where exactly into AuthenticationProvider does captcha validation occurs?