Professional Documents
Culture Documents
[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) {
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);
}
}
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;
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);
@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>
$(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;
};
$("#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 {
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);
}
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
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.
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