Friday, January 28, 2011

Expired Password??? No problem with Spring Security!


I've got a user story which requires a feature that lets a user change their expired password. We've already got the views defined/written, and they'll work pretty much right away, so I'd like to leverage Spring Security's authentication framework to detect when a user's password has expired, and redirect them to the view that will allow them to change their password.

Spending some time on the forum, didn't yield a whole lot of answers that were directly relevant to 3.0.x, but after digging through the API's I found that I could swap the <form-login >'s authentication-failure-handler with Spring Security's own org.springframework.security.web.authentication.ExceptionMappingAuthenticationFailureHandler, which will let me provide separate views for each type of exception that is thrown during authentication. In my particular case, I'm concerned with org.springframework.security.authentication.AccountExpiredException.

So, I need to change the <form-login > namespace a bit:
   

Next, I configure the authenticationFailureHandler bean above, and provide it with the appropriate mappings:


  
  
   
    /login.jsp?login_error=1
    
    /expiredPassword.htm
    
    /expiredPassword.htm
    
   
  
 

Before I can load the view, I need to populate the backing bean (or form) for the view, which requires that I populate it with a valid Users' object...I can find a user based on their username, and luckily, Spring puts the username into the requests' session scope, so I can handle the request like so
 @RequestMapping(value="/expiredPassword.htm", method=RequestMethod.GET)
 public ModelAndView goToChangePassword(HttpServletRequest request,
   HttpServletResponse response) throws Exception {
     String username = (String) request.getSession().getAttribute(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_LAST_USERNAME_KEY);
     
     
     Users user = userService.getUserByUsername(username);
     if (user == null){
      return new ModelAndView( new RedirectView("/home.htm"));
     }
     
     PasswordForm form = new PasswordForm(user);
  ModelAndView mav = new ModelAndView(CHANGE_PASSWORD);
  mav.addObject(form);
  return mav;
 }

I can do whatever I want in the case I can't load the username, but for now, I'll just redirect the user to the login page (ambiguously through the home.htm mapping).

Everything else behaves the way it did before; other exceptions get mapped to the default view (login.jsp?login_error=1); I can change this in the future, if I need to handle any other special cases. Thank you Spring Security team for a really easy implementation...

Tuesday, September 21, 2010

Spring Security 3 Ajaxified Login with jQuery (1.4.x)

On a recent project at work, I was tasked with porting a web app (or at least certain views of the UI) to a mobile device friendly UI. For whatever reason, the mandate was to make it "slick" on an iPhone. Since the project already uses jQuery extensively, it naturally made sense to use a framework that is compatible with jQuery, and mobile device. I chose (for a prototype) jQtouch, as jQuery mobile isn't slated for release until end of year.

Too many pros to talk about this framework it kicks ass. Unfortunately, looks like everything runs on a single (index.*) page, with separate divs which forms the panels.

My first hiccup using jQtouch was the authentication process. I'd hoped to be able to re-use ALL of our customized authentication code. We use Spring Security, along with some Roll-Your-Own UserdDetailsService. After some research, I determined that I could run multiple Authentication Processing filters on Spring Security's filter chain. Here's how I made the whole thing work using an ajaxified login form with Spring's UsernamePasswordAuthenticationFilter, and LogoutFilter.

first the application-context beans required:


 
     
     
  
   
       
      
  
     
      
       
      
     
 
 
 
 
  
   
  
  
   
    
    
   
  
     
 


Then, I added these beans to the existing Spring Security Configuration's filter chain (pay specific to the last 2 lines);

     
     
        
        
        
        
        
        
        
    

Don't forget to update the web.xml with the new filters;


    mobileAuthenticationProcessingFilter
    org.springframework.web.filter.DelegatingFilterProxy
  
  
    mobileAuthenticationProcessingFilter
    /*
  
  
  
    mobileAuthenticationInvalidationFilter
    org.springframework.web.filter.DelegatingFilterProxy
  
  
    mobileAuthenticationInvalidationFilter
    /*
  

Since I don't want to redirect after login, I need to override the default behaviour in the MobileAuthenticationProcessingFilter:

public class MobileAuthenticationProcessingFilter extends UsernamePasswordAuthenticationFilter {
 
 private static Logger log = Logger.getLogger(MobileAuthenticationProcessingFilter.class);

 @Override
 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
   Authentication authResult) throws IOException, ServletException {

  super.successfulAuthentication(request, response, authResult);
  HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response);
  responseWrapper.setContentType("text/json");
  String json = "{authenticated:true, navTag : \"" + request.getRequestURI() + "#Crew \"}";
  log.debug("Successful Authentication, writing JSON success Response: " + json );

  renderResponse(responseWrapper, json);
 }

 @Override
 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException failed) throws IOException, ServletException {
  /*
   * Hacky, but works.  What's happening is that the normal "onAuthenticationFailure" method
   * is sending a redirect, which is screwing up the ajax call to authenticate.  Inb this case, 
   * we'll let the AbstractAuthenticationFilter populate the session scope with the EXCEPTION_KEY,
   * but then gracefully STFU.
   */
  super.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler() {
   @Override
   public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
     AuthenticationException exception) throws IOException, ServletException {
    // do nothing
   }
  });
  
  super.unsuccessfulAuthentication(request, response, failed);
  
  HttpServletResponseWrapper responseWrapper = new HttpServletResponseWrapper(response);
  String failureReason = request.getSession().getAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY) != null ?  
    ((Exception)request.getSession().getAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY)).getMessage() :
     "Invalid login attempt, check your authentication credentials.";
  responseWrapper.setContentType("text/json");
  String json = "{ authenticated: false,\"errors\": { \"reason\": \" " + failureReason + " \"} }";
  log.debug("Failed Authentication, writing JSON failure Response: " + json);
  renderResponse(responseWrapper, json);

 }

 private void renderResponse(HttpServletResponseWrapper responseWrapper, String json) throws IOException {
  Writer out = responseWrapper.getWriter();
  out.write(json);
  out.close();
 }
}

Additionally, if you noticed in the application-context bean we provided the com.denlab.web.filter.MobileLogoutSuccessHandler class as a constructor arg, which also overrides the behaviour of the default:

public class MobileLogoutSuccessHandler implements LogoutSuccessHandler {

 @Override
 public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) throws IOException, ServletException {
  
  //Do Nothing, we'll write a response later.
  Logger.getLogger(this.getClass()).debug("Logging out");
  
 }

}

Now that we've configured spring, implemented our classes, all that's left is to provide the login form, inside the index.jsp, with Spring's default params:




And a logout button:


   Logout
  

Use jQuery to intercept the form submission, and fire the ajax event:
 

When the principal authenticates, the MobileAuthenticationProcessingFilter will prevent the response redirect and instead write a json object directly to the response.