Monday, April 18, 2011

Securing jQuery Mobile Dynamic Content with Spring Security

In my last post we looked at getting started with dynamic content in jQuery Mobile. In this post we will continue where we left off last time with integrating Spring Security to protect this dynamic content or serve user specific content. It is important to note that I am only worried about protecting the dynamic content and not the static page content. As we will see, much of the work is augmenting the default behaviors of both Spring Security and jQuery Mobile to make them play nice together.
Again, I will not go into basic spring security configuration as there are many available resources to help you get up to speed. We will start will augmenting a basic Spring Security config to only allow authenticated users to access /dynamicData.
<intercept-url pattern="/dynamicData" access="isAuthenticated()" />
This will prevent access to the method but Spring Security’s default behavior is to respond with a 302 redirecting to the login page. jQuery will follow this redirect yielding the html login page as the result of the dynamic data ajax request. To fix this problem we will configure a custom entry point utilizing the DelegatingAuthenticationEntryPoint. By looking for the X-Requested-With=XMLHttpRequest header attribute we can augment the behavior for ajax requests while maintaining the original functionality for traditional requests.
<http use-expressions="true" entry-point-ref="entryPoint">
...
<beans:bean id="entryPoint" class="org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint">
  <beans:constructor-arg>
    <beans:map>
      <beans:entry key="hasHeader('X-Requested-With','XMLHttpRequest')" value-ref="http401UnauthorizedEntryPoint" />
    </beans:map>
  </beans:constructor-arg>
  <beans:property name="defaultEntryPoint">
    <beans:bean class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
      <beans:property name="loginFormUrl" value="/signin"/>
    </beans:bean>
  </beans:property>
</beans:bean>
Once the header element has been identified we will need to specify a AuthenticationEntryPoint that will return a HTTP status code. While it might be tempting to use the Http403ForbiddenEntryPoint that is predefined by Spring, this status code should be used for resource requests by an authenticated user that does not have adequate permissions for the resource. Therefore will will need to create a custom entry point that will return the appropriate code, 401.
@Component
public class Http401UnauthorizedEntryPoint implements AuthenticationEntryPoint {

  /**
   * Always returns a 401 error code to the client.
   */
  public void commence(HttpServletRequest request, HttpServletResponse response, 
      AuthenticationException arg2) throws IOException, ServletException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Not Authenticated");
  }
}
Now that our request will receive the appropriate status code we handle this case in our in our dynamic data request and add a login page.
var dynamicDataResp = 
    $.ajax({
   url: "/dynamicData.json",
   async: false,
   cache: false 
 });
if(dynamicDataResp.status == 200){
  $('#dynamicContentHolder').text(dynamicDataResp.responseText);
}
else if(dynamicDataResp.status == 401){
  $.mobile.changePage('#login');
}
else{
  $.mobile.changePage('#error', 'none', false, false);
}
By default jQuery Mobile will submit forms via ajax with the expectation of receiving an HTML to render. As this functionality does not work with our desired flow, we will disable this feature by setting data-ajax="false".
<div id="login" data-role="page">
  <div data-role="header">
    <h1>Login</h1>
  </div>
  <div data-role="content">
    <span id="loginError" class="error">Your credentials are invalid. Please try again.</span>
    <form id="signInForm" data-ajax="false">
      <div data-role="fieldcontain">
        <label for="login">Username or Email</label>
        <input id="login" name="j_username" type="text" size="25" autocorrect="off" autocapitalize="off" />
      </div>
      <div data-role="fieldcontain">
        <label for="password">Password</label>
        <input id="password" name="j_password" type="password" size="25" /> 
      </div>
      <div id="submitDiv" data-role="fieldcontain">    
        <input type="submit" value="login" data-inline="true"/>
      </div>
    </form> 
  </div>
</div>
Now that we have disabled jQuery Mobile’s ajax form submission we will want to setup our own. By binding to the submit event we can create our own asynchronous ajax call to post the form to Spring Security’s authenticate method.
$('#signInForm').live("submit", function() {
  $.mobile.pageLoading(); 
  // Submit the form
  $.ajax({
 type: 'POST',
 url: '/signin/authenticate',
 data: $('#signInForm').serialize(),
 complete: function(transport) {
  if(transport.status == 200) {
  history.back();
  } else {
  $('#loginError').show();
  }
 }
  });
  return false;
});
Again Spring Security’s default action will not work with our flow. If the login was successful we would get redirected to the entry page (or the last requested page) and if the login failed we would receive the contents of the login form page. To fix this we will add custom success and failure handlers to our form-login.
<form-login login-page="/signin"  login-processing-url="/signin/authenticate"
authentication-success-handler-ref="ajaxAuthenticationSuccessHandler"
authentication-failure-handler-ref="ajaxAuthenticationFailureHandler" />
Below are simple implementations that return HTTP status codes. To maintain the traditional form capabilities for other parts of your site simply extend one of springs handlers, such as SavedRequestAwareAuthenticationSuccessHandler and SimpleUrlAuthenticationFailureHandler to first check for the X-Requested-With=XMLHttpRequest header attribute.
@Component
public class AjaxAuthenticationSuccessHandler implements
    AuthenticationSuccessHandler {

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication authentication)
   throws IOException, ServletException {
 response.sendError(HttpServletResponse.SC_OK, "User Authenticated");
  }
}
@Component
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {
 
  public void onAuthenticationFailure(HttpServletRequest request,
      HttpServletResponse response, AuthenticationException exception) throws  
      IOException, ServletException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
      "User Could Not Be Authenticated");
  }
}
The last feature needed is sign out capabilities. To accomplish this we will add a signout button to bar.
<div data-role="content"> 
  <h2>Bar</h2>
  <a href="#foo">To foo</a>
  <span id="dynamicContentHolder"></span>
  <a id="buttonSignOut" name="buttonSignOut" href="#" data-role="button" data-inline="true">Sign Out</a>
</div>
Again we will use jQuery to hijack the click of the button.
$('#buttonSignOut').live("click", function() {
  $.mobile.pageLoading(); 
  $.ajax({
      url: '/signout',
      complete: function(transport) {
        if(transport.status == 200) {
          $.mobile.changePage('#foo');
        } else {
          $.mobile.changePage('#error', 'none', false, false);
        }
   $.mobile.pageLoading(true);
    }
  });
  return false;      
});
And configure Spring Security to return status codes. Again extend SimpleUrlLogoutSuccessHandler if you need to maintain existing form functionality.
<logout logout-url="/signout" delete-cookies="JSESSIONID" success-handler-ref="ajaxLogoutSuccessHandler"/>
@Component
public class AjaxLogoutSuccessHandler implements LogoutSuccessHandler {
 
  public void onLogoutSuccess(HttpServletRequest request,
      HttpServletResponse response, Authentication authentication)
      throws IOException, ServletException {
    response.sendError(HttpServletResponse.SC_OK, "User Logged Out");
  }
}
One final note before I show you the front end code all together; I have read that sometimes IE will submit a ajax request with the XMLHttpRequest attribute in lower case. I have not tested this yet, but I would watch out for this.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1">
 <link rel="stylesheet" href="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.css" />
 <script type="text/javascript" src="http://code.jquery.com/jquery-1.5.2.min.js"></script>
 <script type="text/javascript">
 $(document).bind("mobileinit", function(){
  $('#buttonSignOut').live("click", function() {
   $.mobile.pageLoading(); 
   $.ajax({
    url: '/signout',
    complete: function(transport) {
     if(transport.status == 200) {
       $.mobile.changePage('#foo');
      } else {
       $.mobile.changePage('#error', 'none', false, false);
      }
    }
   });
        return false;      
      });
   $('#signInForm').live("submit", function() {
  $.mobile.pageLoading(); 
     // Submit the form
  $.ajax({
   type: 'POST',
   url: '/signin/authenticate',
   data: $('#signInForm').serialize(),
   complete: function(transport) {
    if(transport.status == 200) {
     history.back();
    } else {
     $('#loginError').show();
     $.mobile.pageLoading(true);
    }
      }
  });
  return false;
   });
   
   $('#login').live('pagebeforeshow', function(event, ui){
  $('#loginError').hide();
   });
 
 
  $('#bar').live('pagebeforeshow',function(event, ui){
   $.mobile.pageLoading();
   $('#dynamicContentHolder').text('');
   var dynamicDataResp = $.ajax({
       url: "/dynamicData",
       async: false,
       cache: false 
     });
   if(dynamicDataResp.status == 200){
    $('#dynamicContentHolder').text(dynamicDataResp.responseText);
   }
   else if(dynamicDataResp.status == 401){
    $.mobile.changePage('#login');
   }
   else{
    $.mobile.changePage('#error', 'none', false, false);
   }
  });
 });
 </script>
 <script type="text/javascript" src="http://code.jquery.com/mobile/1.0a4.1/jquery.mobile-1.0a4.1.min.js"></script>
  </head> 
  <body> 
    <div id="foo" data-role="page">
      <div data-role="header">
  <h1>Foo</h1>
   </div>
   <div data-role="content"> 
  <h2>Foo</h2>
  <a href="#bar">To bar</a>
   </div>
    </div>
    <div id="bar" data-role="page">
      <div data-role="header">
  <h1>Bar</h1>
   </div>
   <div data-role="content"> 
  <h2>Bar</h2>
  <a href="#foo">To foo</a>
  <span id="dynamicContentHolder"></span>
  <a id="buttonSignOut" name="buttonSignOut" href="#" data-role="button" data-inline="true">Sign Out</a>
   </div>
    </div>
    <div id="login" data-role="page">
      <div data-role="header">
  <h1>Login</h1>
   </div>
   <div data-role="content">
  <span id="loginError" class="error">Your credentials are invalid. Please try again.</span>
     <form id="signInForm" data-ajax="false">
       <div data-role="fieldcontain">
            <label for="login">Username or Email</label>
            <input id="login" name="j_username" type="text" size="25" autocorrect="off" autocapitalize="off" />
          </div>
     <div data-role="fieldcontain">
    <label for="password">Password</label>
    <input id="password" name="j_password" type="password" size="25" /> 
   </div>
   <div id="submitDiv" data-role="fieldcontain">    
    <input type="submit" value="login" data-inline="true"/>
   </div>
     </form> 
   </div>
    </div>
 <div id="error" data-role="page">
      <div data-role="header">
  <h1>Error</h1>
   </div>
   <div data-role="content"> 
  Click back to return to the previous page.
   </div>
    </div>
  </body>
</html>

1 comment:

Sanjay Patel said...

Can you upload whole project that I can run locally in my machine? It will be very helpful to see this in action. thanks.