You are on page 1of 8

1/03/2011 Implementing Ajax Authentication usi…

Published on Javalobby (http://java.dzone.com)


Implementing Ajax Authentication using jQuery, Spring Security and
HTTPS
By mraible
Created 2011/02/24 - 1:33am

I've always had a keen interest in implementing security in webapps. I


implemented container-managed authentication (CMA) in AppFuse [1] in
2002, watched Tomcat improve it's implementation in 2003 [2] and
implemented Remember Me with CMA [3] in 2004. In 2005, I switched from
CMA to Acegi Security [4] (now Spring Security [5]) and never looked back.
I've been very happy with Spring Security over the years, but also hope to
learn more about Apache Shiro [6] and implementing OAuth to protect
JavaScript APIs [7] in the near future.

I was recently re-inspired to learn more about security when working on a


new feature at Overstock.com [8]. 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 [9]. 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
[10] 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 [11] attribute that can be used for
this. I used this to force HTTPS on the "users" page and it subsequently
java.dzone.com/print/35841?utm_so… 1/8
1/03/2011 Implementing Ajax Authentication usi…
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 [12], I had to modify my
jWebUnit test [13] 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
[14], I wrote about adding my 'untrusted' cert to my JVM keystore [15]. 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.ne
<javax.net.ssl.trustStorePassword>appfuse</javax.net.ssl.trustStorePassword
</systemPropertyVariables>
</configuration>

[16]

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
[17].

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.UsernamePasswordAuthentication
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 {
java.dzone.com/print/35841?utm_so… 2/8
1/03/2011 Implementing Ajax Authentication usi…

@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.isAuthent
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 UsernamePasswordAuthenticatio


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 [18]. 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;
java.dzone.com/print/35841?utm_so… 3/8
1/03/2011 Implementing Ajax Authentication usi…
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())
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.<authentication>anyObject())
.thenThrow(new BadCredentialsException("Bad Credentials"));
LoginService.LoginStatus status = loginService.login("foo", "bar");
assertFalse(status.isLoggedIn());
java.dzone.com/print/35841?utm_so… 4/8
1/03/2011 Implementing Ajax Authentication usi…
assertEquals(null, status.getUsername());
}
}
</authentication></authentication>

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 [19] 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 agai
};

$("#login").live('click', function(e) {
e.preventDefault();
$.ajax({url: getHost() + "/api/login.json",
type: "POST",
data: $("#loginForm").serialize(),

java.dzone.com/print/35841?utm_so… 5/8
1/03/2011 Implementing Ajax Authentication usi…
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 [20] and the jQuery plugin [21] 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 [22] helped me figure this out.
public class OptionsHeadersFilter implements Filter {

public void doFilter(ServletRequest req, ServletResponse res, FilterChain cha


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
[10] 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 [23] 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
java.dzone.com/print/35841?utm_so… 6/8
1/03/2011 Implementing Ajax Authentication usi…
unable to make the authentication persist [24]. 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 res

UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenti


...

try {
Authentication auth = authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(auth);
// save the updated context to the session
repository.saveContext(SecurityContextHolder.getContext(), request,
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 [25] has a lot of good tips in this area.

Update: I've posted a demo of the ajax-login webapp [26]. Thanks to


Contegix [27] for hosting the demo and helping obtain/install an SSL
certificate so quickly.

From
http://raibledesigns.com/rd/entry/implementing_ajax_authentication_using_jq
[28]

java.dzone.com/print/35841?utm_so… 7/8
1/03/2011 Implementing Ajax Authentication usi…
Source URL: http://java.dzone.com/articles/implementing-ajax

Links:
[1] http://appfuse.org/
[2]
http://raibledesigns.com/rd/entry/container_managed_authentication_enhancements_
[3] http://raibledesigns.com/rd/entry/appfuse_refactorings_part_iii_remember
[4] http://raibledesigns.com/rd/entry/ann_appfuse_1_8_released
[5] http://static.springsource.org/spring-security/site/
[6] http://shiro.apache.org/
[7] http://www.quora.com/Is-OAuth-the-best-way-to-implement-security-for-a-
JavaScript-API
[8] http://www.overstock.com/
[9] http://uxexchange.com/questions/1877/the-usability-of-lightbox-uis
[10] https://github.com/mraible/ajax-login
[11] http://static.springsource.org/spring-security/site/docs/3.0.x/reference/ns-
config.html#ns-requires-channel
[12] https://github.com/mraible/ajax-login/blob/master/src/main/webapp/WEB-
INF/security.xml
[13] https://github.com/mraible/ajax-
login/blob/master/src/test/java/org/appfuse/examples/web/UserWebTest.java
[14] http://raibledesigns.com/rd/entry/integration_testing_with_http_https
[15] http://blogs.sun.com/gc/entry/unable_to_find_valid_certification
[16] http://java.dzone.com/articles/implementing-ajax#about
[17] http://blog.springsource.com/2010/01/25/ajax-simplifications-in-spring-3-0/
[18] http://mockito.org/
[19] http://raibledesigns.com/rd/entry/ajaxified_body
[20] http://www.sitepen.com/blog/2008/07/22/windowname-transport/
[21] http://friedcellcollective.net/outbreak/jsjquerywindownameplugin/
[22] http://stackoverflow.com/questions/1099787/jquery-ajax-post-sending-
options-as-request-method-in-firefox
[23] http://raibledesigns.com/rd/entry/apache_2_on_os_x
[24] http://stackoverflow.com/questions/5087137/is-it-possible-to-
programmatically-authenticate-with-spring-security-and-make-it
[25] http://www.owasp.org/index.php/SSL_Best_Practices#Secure_Login_Pages
[26] http://demo.raibledesigns.com/ajax-login/
[27] http://www.contegix.com/
[28]
http://raibledesigns.com/rd/entry/implementing_ajax_authentication_using_jquery

java.dzone.com/print/35841?utm_so… 8/8