Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Implementing Ajax Authentication using jQuery, Spring Security and HTTPS

I've always had a keen interest in implementing security in webapps. I implemented container-managed authentication (CMA) in AppFuse in 2002, watched Tomcat improve it's implementation in 2003 and implemented Remember Me with CMA in 2004. In 2005, I switched from CMA to Acegi Security (now Spring Security) and never looked back. I've been very happy with Spring Security over the years, but also hope to learn more about Apache Shiro and implementing OAuth to protect JavaScript APIs in the near future.

I was recently re-inspired to learn more about security when working on a new feature at Overstock.com. The feature hasn't been released yet, but basically boils down to allowing users to login without leaving a page. For example, if they want to leave a review on a product, they would click a link, be prompted to login, enter their credentials, then continue to leave their review. The login prompt and subsequent review would likely be implemented using a lightbox. While lightboxes are often seen in webapps these days because they look good, it's also possible Lightbox UIs provide a poor user experience. User experience aside, I think it's interesting to see what's required to implement such a feature.

To demonstrate how we did it, I whipped up an example using AppFuse Light, jQuery and Spring Security. The source is available in my ajax-login project on GitHub. To begin, I wanted to accomplish a number of things to replicate the Overstock environment:

  1. Force HTTPS for authentication.
  2. Allow testing HTTPS without installing a certificate locally.
  3. Implement a RESTful LoginService that allows users to login.
  4. Implement login with Ajax, with the request coming from an insecure page.

Forcing HTTPS with Spring Security
The first feature was fairly easy to implement thanks to Spring Security. Its configuration supports a requires-channel attribute that can be used for this. I used this to force HTTPS on the "users" page and it subsequently causes the login to be secure.

<intercept-url pattern="/app/users" access="ROLE_ADMIN" requires-channel="https"/>

Testing HTTPS without adding a certificate locally
After making the above change in security.xml, I had to modify my jWebUnit test to work with SSL. In reality, I didn't have to modify the test, I just had to modify the configuration that ran the test. In my last post, I wrote about adding my 'untrusted' cert to my JVM keystore. For some reason, this works for HttpClient, but not for jWebUnit/HtmlUnit. The good news is I figured out an easier solution - adding the trustStore and trustStore password as system properties to the maven-failsafe-plugin configuration.

<artifactId>maven-failsafe-plugin</artifactId>
<version>2.7.2</version>
<configuration>
    <includes>
        <include>**/*WebTest.java</include>
    </includes>
    <systemPropertyVariables>
      <javax.net.ssl.trustStore>${project.build.directory}/ssl.keystore</javax.net.ssl.trustStore>
      <javax.net.ssl.trustStorePassword>appfuse</javax.net.ssl.trustStorePassword>
    </systemPropertyVariables>
</configuration>

The disadvantage to doing things this way is you'll have to pass these in as arguments when running unit tests in your IDE.

Implementing a LoginService
Next, I set about implementing a LoginService as a Spring MVC Controller that returns JSON thanks to the @ResponseBody annotation and Jackson.

package org.appfuse.examples.web;

import org.appfuse.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("/api/login.json")
public class LoginService {

  @Autowired
  @Qualifier("authenticationManager")
  AuthenticationManager authenticationManager;

  @RequestMapping(method = RequestMethod.GET)
  @ResponseBody
  public LoginStatus getStatus() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    if (auth != null && !auth.getName().equals("anonymousUser") && auth.isAuthenticated()) {
      return new LoginStatus(true, auth.getName());
    } else {
      return new LoginStatus(false, null);
    }
  }

  @RequestMapping(method = RequestMethod.POST)
  @ResponseBody
  public LoginStatus login(@RequestParam("j_username") String username,
                           @RequestParam("j_password") String password) {

    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
    User details = new User(username);
    token.setDetails(details);

    try {
      Authentication auth = authenticationManager.authenticate(token);
      SecurityContextHolder.getContext().setAuthentication(auth);
      return new LoginStatus(auth.isAuthenticated(), auth.getName());
    } catch (BadCredentialsException e) {
      return new LoginStatus(false, null);
    }
  }

  public class LoginStatus {

    private final boolean loggedIn;
    private final String username;

    public LoginStatus(boolean loggedIn, String username) {
      this.loggedIn = loggedIn;
      this.username = username;
    }

    public boolean isLoggedIn() {
      return loggedIn;
    }

    public String getUsername() {
      return username;
    }
  }
}

To verify this class worked as expected, I wrote a unit test using JUnit and Mockito. I used Mockito because Overstock is transitioning to it from EasyMock and I've found it very simple to use.

package org.appfuse.examples.web;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

public class LoginServiceTest {

  LoginService loginService;
  AuthenticationManager authenticationManager;

  @Before
  public void before() {
    loginService = new LoginService();
    authenticationManager = mock(AuthenticationManager.class);
    loginService.authenticationManager = authenticationManager;
  }

  @After
  public void after() {
    SecurityContextHolder.clearContext();
  }

  @Test
  public void testLoginStatusSuccess() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(true);
    SecurityContext context = new SecurityContextImpl();
    context.setAuthentication(auth);
    SecurityContextHolder.setContext(context);

    LoginService.LoginStatus status = loginService.getStatus();
    assertTrue(status.isLoggedIn());
  }

  @Test
  public void testLoginStatusFailure() {
    LoginService.LoginStatus status = loginService.getStatus();
    assertFalse(status.isLoggedIn());
  }

  @Test
  public void testGoodLogin() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(true);
    when(authenticationManager.authenticate(Matchers.<Authentication>anyObject())).thenReturn(auth);
    LoginService.LoginStatus status = loginService.login("foo", "bar");
    assertTrue(status.isLoggedIn());
    assertEquals("foo", status.getUsername());
  }

  @Test
  public void testBadLogin() {
    Authentication auth = new TestingAuthenticationToken("foo", "bar");
    auth.setAuthenticated(false);
    when(authenticationManager.authenticate(Matchers.anyObject()))
        .thenThrow(new BadCredentialsException("Bad Credentials"));
    LoginService.LoginStatus status = loginService.login("foo", "bar");
    assertFalse(status.isLoggedIn());
    assertEquals(null, status.getUsername());
  }
}

Implement login with Ajax
The last feature was the hardest to implement and still isn't fully working as I'd hoped. I used jQuery and jQuery UI to implement a dialog that opens the login page on the same page rather than redirecting to the login page. The "#demo" locator refers to a button in the page.

Passing in the "ajax=true" parameter disables SiteMesh decoration on the login page, something that's described in my Ajaxified Body article.

var dialog = $('<div></div>');

$(document).ready(function() {
    $.get('/login?ajax=true', function(data) {
        dialog.html(data);
        dialog.dialog({
            autoOpen: false,
	       title: 'Authentication Required'
        });
    });

    $('#demo').click(function() {
      dialog.dialog('open');
      // prevent the default action, e.g., following a link
      return false;
    });
});

Instead of adding a click handler to a specific id, it's probably better to use a CSS class that indicates authentication is required for a link, or -- even better -- use Ajax to see if the link is secured.

The login page then has the following JavaScript to add a click handler to the "login" button that submits the request securely to the LoginService.

var getHost = function() {
    var port = (window.location.port == "8080") ? ":8443" : "";
    return ((secure) ? 'https://' : 'http://') + window.location.hostname + port;
};

var loginFailed = function(data, status) {
    $(".error").remove();
    $('#username-label').before('<div class="error">Login failed, please try again.</div>');
};

$("#login").live('click', function(e) {
    e.preventDefault();
    $.ajax({url: getHost() + "/api/login.json",
        type: "POST",
        data: $("#loginForm").serialize(),
        success: function(data, status) {
            if (data.loggedIn) {
                // success
                dialog.dialog('close');
                location.href= getHost() + '/users';
            } else {
                loginFailed(data);
            }
        },
        error: loginFailed
    });
});

The biggest secret to making this all work (the HTTP -> HTTPS communication, which is considered cross-domain), is the window.name Transport and the jQuery plugin that implements it. To make this plugin work with Firefox 3.6, I had to implement a Filter that adds Access-Control headers. A question on Stackoverflow helped me figure this out.

public class OptionsHeadersFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET,POST");
        response.setHeader("Access-Control-Max-Age", "360");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");

        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {
    }

    public void destroy() {
    }
}

Issues
I encountered a number of issues when implementing this in the ajax-login project.

  • If you try to run this with ports (e.g. 8080 and 8443) in your URLs, you'll get a 501 (Not Implemented) response. Removing the ports by fronting with Apache and mod_proxy solves this problem.
  • If you haven't accepted the certificate in your browser, the Ajax request will fail. In the example, I solved this by clicking on the "Users" tab to make a secure request, then going back to the homepage to try and login.
  • The jQuery window.name version 0.9.1 doesn't work with jQuery 1.5.0. The error is "$.httpSuccess function not found."
  • Finally, even though I was able to authenticate successfully, I was unable to make the authentication persist. I tried adding the following to persist the updated SecurityContext to the session, but it doesn't work. I expect the solution is to create a secure JSESSIONID cookie somehow.
    @Autowired
    SecurityContextRepository repository;
    
    @RequestMapping(method = RequestMethod.POST)
    @ResponseBody
    public LoginStatus login(@RequestParam("j_username") String username,
                             @RequestParam("j_password") String password,
                             HttpServletRequest request, HttpServletResponse response) {
    
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
        ...
    
        try {
            Authentication auth = authenticationManager.authenticate(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
            // save the updated context to the session
            repository.saveContext(SecurityContextHolder.getContext(), request, response);
            return new LoginStatus(auth.isAuthenticated(), auth.getName());
        } catch (BadCredentialsException e) {
            return new LoginStatus(false, null);
        }
    }
    

Conclusion
This article has shown you how to force HTTPS for login, how to do integration testing with a self-generated certificate, how to implement a LoginService with Spring MVC and Spring Security, as well as how to use jQuery to talk to a service cross-domain with the window.name Transport. While I don't have everything working as much as I'd like, I hope this helps you implement a similar feature in your applications.

One thing to be aware of is with lightbox/dialog logins and HTTP -> HTTPS is that users won't see a secure icon in their address bar. If your app has sensitive data, you might want to force https for your entire app. OWASP's Secure Login Pages has a lot of good tips in this area.

Update: I've posted a demo of the ajax-login webapp. Thanks to Contegix for hosting the demo and helping obtain/install an SSL certificate so quickly.

Posted in Java at Feb 23 2011, 04:55:55 PM MST 12 Comments
Comments:

I just overrode the SimpleUrlAuthenticationSuccessHandler for a more out-of-the-box Ajax-Spring Security impl, see: http://stackoverflow.com/questions/3444864/ajax-login-with-spring-webmvc-and-spring-security.

-Pete (big fan of AppFuse BTW)

Posted by Peter Hawkins on February 24, 2011 at 10:53 AM MST #

@Peter - I tried this technique and was unable to get it to work. Firefox makes an OPTIONS request when posting and gets back a redirect to login?error=true rather than succeeding. I've uploaded the patch I used.

Posted by Matt Raible on February 24, 2011 at 01:09 PM MST #

Just to update anyone reading the comments - I've successfully used Peter's technique to talk to j_security_check and get it working as good as the LoginService implementation. However, I'm still facing the same issues with getting the HTTPS login to persist to HTTP. Here's a patch to show you what's changed. I've also created a j_security_check branch to try and get this alternative implementation working.

Posted by Matt Raible on February 24, 2011 at 10:20 PM MST #

My name is Rob Winch and I am a committer for Spring Security. I'm not sure how far you have gotten with the issues you were having, but I read your blog post and thought I would do my best to assist.

It is not necessary to call SecurityContextRepository.saveContext from the LoginService. This is because by using the http namespace configuration, you have already included the SecurityContextPersistenceFilter which does saves it for you.

I believe I can shed some light on the issue with authentication not persisting when using ajax too. For me this only happens when I try and authenticate from the http page. It works fine when doing it from the https page. This indicated to me that it was likely a Cross-site HTTP requests issue. There were a few issues related to Cross-site HTTP requests that I found that were preventing the JSESSIONID cookie from being accepted.

The first issue is that Access-Control-Allow-Credentials header must be set to true. This is so that the browser knows it can send and accept cookies. The second issue is that XMLHttpRequest.withCredentials should be set to true. The last change was that in order to allow credentials to work across domains, the Access-Control-Allow-Origin must be a specific value (i.e. it won't work if you use a wildcard). For more information, you can read about it on mozilla's site.

I went ahead and made these changes and verified that they worked in the latest Firefox (4) and Chrome on Linux and submitted a pull request with those changes. Hopefully this information helps.

Cheers,
Rob

PS: If you have a question about Spring Security in the future, I would be glad to help out if I am able. I am very active in the forums, so just drop the question out there and I will do my best to be of assistance.

Posted by Rob Winch on March 23, 2011 at 11:08 PM MDT #

Thanks Rob! I got your pull request and committed it to the master branch. Works great! I deployed the latest version with your changes at http://demo.raibledesigns.com/ajax-login.

Posted by Matt Raible on March 24, 2011 at 12:51 PM MDT #

[Trackback] A couple weeks ago, I wrote a tutorial on how to implement security with Spring Security . The week prior, I wrote a similar tutorial for Java EE 6 . This week, I'd like to show you how to implement the same features using Apache Shiro . As I mentio...

Posted by Raible Designs on June 06, 2011 at 07:50 PM MDT #

[Trackback] Back in February, I wrote about my upcoming conferences : In addition to Vegas and Poland, there's a couple other events I might speak at in the next few months: the Utah Java Users Group (possibly in April), Jazoon and Ã?berConf (if my pro...

Posted by Raible Designs on June 06, 2011 at 07:50 PM MDT #

[Trackback] Last week, I wrote a tutorial on how to implement Security in Java EE 6 . This week, I'd like to show you how to implement the same features using Spring Security . Before I begin, I'd like to explain my reason for writing this article. Last mon...

Posted by Raible Designs on June 06, 2011 at 07:50 PM MDT #

@Rob
It there any way i could use one of these magic HTTP headers to let Tomcat/Browser share JSESSIONID cookie when switching between HTTP and HTTPS in case i use "full" (not AJAX) HTTP requests? In Spring FAQ there is a solution that states of creating session for HTTP and then reusing it when logging in on HTTPS channel. THe other soultuion i am aware of mode is to trick the browser by creating the non-secure cookie, when the secure cookie is getting created. Both of these sounds as hacks. I am wondering if theres in more standard solution...

@Matt
Programmatic Login seems much easier than logout. I took a look at your github project and it seems that logout is implemented by invalidation HTTP session. Asfaik when "rememberme" or any other "exotic login approaches" are employed,session invalidation is often not enough.

Posted by jmilkiewicz on December 12, 2011 at 11:17 AM MST #

Hey guy

Thank you very much for your share, and I'd like to say that based on your implementation, I could figure out how to make the remember-me service work! Yey!

Took me some time digging into the security code, but...it's quite simple:

First of all you need an RememberMeServices instance, which can be easily injected using @Autowired

And then, after the login process all you have to do is call:

rememberMeServices.loginSuccess(request, response, authentication);

Don't forget that the remember me parameter needs to be sent within the request (default is _spring_security_remember_me=on)

Please, let me know if it works for you, for me it's working like a charm!

Posted by Rafael Roman on February 07, 2012 at 04:02 PM MST #

I just tried to follow your tutorial but undergoing some issue to get it done. this is what i am doing in my JQuery

jQuery("#loginForm").submit(function (e) {
    e.preventDefault();
    jQuery.ajax({
        url:"https://localhost:9002/abc/springSecurity/login.json",
        beforeSend:function (xhr) {
            xhr.withCredentials = true;
        },
        type:"POST",
        data:jQuery("#loginForm").serialize(),
        dataType:'json',
        success:function (data, status) {
            if (data.loggedIn) {
                alert("jai ho");
            } else {
                loginFailed(data);
            }
        },
        error:loginFailed
    });
});

Unfortunately this is causing me some troubles.Seems like the post method of my controller is not getting called. on removing the method with get signature it showing me following error

"NetworkError: 405 Method Not Allowed - http://localhost:9001/landmarkshopsstorefront/springSecurity/login.json"

seems like browser is redirecting and calling get method.

Posted by Shekher on June 14, 2012 at 11:12 AM MDT #

Great post.
I posted another, more conventional way (utilizing Spring) which overcome some of the issues you discussed.

http://gal-levinsky.blogspot.co.il/2011/08/spring-security-3-ajax-login.html

Posted by Gal on June 20, 2012 at 10:32 AM MDT #

Post a Comment:
  • HTML Syntax: Allowed