/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.login.local.view; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.lang.Validate; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.acegisecurity.Authentication; import org.acegisecurity.AuthenticationException; import org.acegisecurity.AuthenticationServiceException; import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter; import com.octo.captcha.service.CaptchaService; import com.octo.captcha.service.CaptchaServiceException; /** A filter that adds a captcha validation over the normal acegi * authentication. */ public class AuthenticationWithCaptchaProcessingFilter extends AuthenticationProcessingFilter implements InitializingBean, MessageSourceAware { /** The service used to validate the captcha, it is never null. */ private CaptchaService captchaService; /** The request parameter name under which the user will post the response to * the captcha challenge. * * It is never null. */ private String captchaValidationParameter = "_captcha_parameter"; /** The context relative url that will show the login form with a captcha * challenge in case the user failed to be authenticated. * * This is never null. */ private String captchaFailureUrl; /** The IP BlackList, it is never be null. */ private IpBlacklist blackList; /** The message source accessor to be used to access the message resources. * * This is never null. */ private MessageSourceAccessor messages; /** Flag to indicate if this class will ignore the call to setMessageSource. */ boolean ignoreSetMessageSource = false; /** {@inheritDoc} * * Validates the invariants, called by the container (spring) after setting * all properties. */ public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); Assert.notNull(captchaFailureUrl, "captchaFailureUrl required"); Assert.notNull(captchaService, "captchaService required"); Assert.notNull(blackList, "ipBlacklist required"); Assert.notNull(messages, "messageSource required"); } /** Validates the captcha if the source ip has been blacklisted additionally * to the standar acegi implementation. * * {@inheritDoc} */ @Override public Authentication attemptAuthentication( final HttpServletRequest request) { try { if (mustValidateCaptcha(request)) { validateCaptcha(request); } return super.attemptAuthentication(request); } catch (AuthenticationException e) { blackList.blacklistIp(request.getRemoteAddr()); throw e; } } /** Decides if the filter must validate also the captcha. * * @param request The servlet request, it cannot be null. * * @return true if the source ip is blacklisted, so the user must pass a * captcha challenge, false otherwise. */ private boolean mustValidateCaptcha(final HttpServletRequest request) { // We also validate the captcha if the user entered a captcha token. This // is for the case where the user failed a login, the captcha is shown, and // then retries to log in after the blacklisting timeout. if (request.getParameter(captchaValidationParameter) != null) { return true; } return blackList.isBlacklisted(request.getRemoteAddr()); } /** Validates the captcha sent by the user. * * A failure to validate the captcha throws an exception * (AuthenticationException). * * @param request The servlet request, it cannot be null. */ private void validateCaptcha(final HttpServletRequest request) { HttpSession session = request.getSession(); if (session == null) { throw new AuthenticationServiceException(messages.getMessage( "AuthenticationWithCaptchaProcessingFilter.incorrectSecurityCode", "Incorrect security code")); } String id = session.getId(); String captchaResponse = request.getParameter(captchaValidationParameter); try { if (!captchaService.validateResponseForID(id, captchaResponse)) { throw new AuthenticationServiceException(messages.getMessage( "AuthenticationWithCaptchaProcessingFilter.incorrectSecurityCode", "Incorrect security code")); } } catch (CaptchaServiceException e) { // If the session expired before validating the captcha, // validateResponseForID throws this exception. We just ignore this case // and trick the user into thinking that he misspelled the captcha. throw new AuthenticationServiceException(messages.getMessage( "AuthenticationWithCaptchaProcessingFilter.incorrectSecurityCode", "Incorrect security code"), e); } } /** {@inheritDoc} * * If the fail was related to the captcha, the url returned is * captchaFailureUrl (see setCaptchaFailureUrl). */ protected String determineFailureUrl(final HttpServletRequest request, final AuthenticationException failed) { if (mustValidateCaptcha(request)) { return captchaFailureUrl; } else { return super.determineFailureUrl(request, failed); } } /** Sets the captcha service implementation. * * @param service The captcha service, used to validate the user provided * captcha response. It cannot be null. */ public void setCaptchaService(final CaptchaService service) { Validate.notNull(service, "The captcha service cannot be null."); captchaService = service; } /** Sets the request parameter where the user will post the response to the * captcha challenge. * * @param parameter the request parameter name, cannot be null. */ public void setCaptchaValidationParameter(final String parameter) { Validate.notNull(parameter, "The captcha request parameter name cannot be null."); captchaValidationParameter = parameter; } /** Sets the url to present the user the login form with the captcha * challenge after a failed login. * * @param url the url of the login page with the captcha. It cannot be null. */ public void setCaptchaFailureUrl(final String url) { Validate.notNull(url, "the captcha failure url cannot be null"); captchaFailureUrl = url; } /** Sets the blacklist used to decide over an invocation. * * @param ipBlackList the ip blacklist, cannot be null */ public void setIpBlacklist(final IpBlacklist ipBlackList) { Validate.notNull(ipBlackList, "Blacklist cannot be null"); blackList = ipBlackList; } /** Sets the message source to use to resolve locale aware message codes. * * If this operation is called mutiple times, only the first one is * considered, all the other ones are ignored. * * @param messageSource the provided message source. It cannot be null. * * Implementation notes: removing MessageSourceAware from the list of * implemented interfaces does not work because something in the class * hierarchy implements MessageSourceAware anyway. */ @Override public void setMessageSource(final MessageSource messageSource) { if (!ignoreSetMessageSource) { Validate.notNull(messageSource, "The message source cannot be null."); messages = new MessageSourceAccessor(messageSource); super.setMessageSource(messageSource); ignoreSetMessageSource = true; } } }