Java Web Application Security - Part IV: Programmatic Login APIs
Over the last month, I've posted a number of articles on implementing authentication with Java EE 6, Spring Security and Apache Shiro. One of the things I demonstrated in my live demos (at Utah's JUG Meetings) was programmatic authentication. I left this out of my screencasts and previous tutorials because I thought it'd fit better in a comparison article.
In this article, I'd like to show you how you can programmatically login to an application using the aforementioned security frameworks. To do this, I'll be using my ajax-login application that I wrote for Implementing Ajax Authentication using jQuery, Spring Security and HTTPS.
To begin, I implemented a LoginController as a Spring MVC Controller that returns JSON.
package org.appfuse.examples.webapp.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; 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 LoginController { @Autowired LoginService loginService; @RequestMapping(method = RequestMethod.GET) @ResponseBody public LoginStatus getStatus() { return loginService.getStatus(); } @RequestMapping(method = RequestMethod.POST) @ResponseBody public LoginStatus login(@RequestParam("j_username") String username, @RequestParam("j_password") String password) { return loginService.login(username, password); } }
This controller delegates its logic to a LoginService interface.
package org.appfuse.examples.webapp.security; public interface LoginService { LoginStatus getStatus(); LoginStatus login(String username, String password); }
The Client
The client for this controller is the same as mentioned in my previous article, but I'll post it again for your convenience. 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.
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; }); });
The login page then has the following JavaScript to add a click handler to the "login" button that submits the request securely to the LoginController.
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('Login failed, please try again.'); }; $("#login").live('click', function(e) { e.preventDefault(); $.ajax({url: getHost() + "${ctx}/api/login.json", type: "POST", beforeSend: function(xhr) { xhr.withCredentials = true; }, data: $("#loginForm").serialize(), success: function(data, status) { if (data.loggedIn) { // success dialog.dialog('close'); location.href = getHost() + '${ctx}/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.
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", "http://" + req.getServerName()); response.setHeader("Access-Control-Allow-Methods", "GET,POST"); response.setHeader("Access-Control-Max-Age", "360"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with"); response.setHeader("Access-Control-Allow-Credentials", "true"); chain.doFilter(req, res); } public void init(FilterConfig filterConfig) { } public void destroy() { } }
Java EE 6 LoginService
Java EE 6 has a few new methods in HttpServletRequest:
- authenticate(response)
- login(user, pass)
- logout()
In this example, I'll use the new login(username, password) method. The hardest part about getting this working was finding the right Maven dependency. At first, I tried the one that seemed to make the most sense:
<dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> </dependency>
Unfortunately, this resulted in a strange error that means the dependency has the interfaces, but not the implementation classes. I ended up using GlassFish's dependency instead (thanks to Stack Overflow for the tip).
<dependency> <groupId>org.glassfish</groupId> <artifactId>javax.servlet</artifactId> <version>3.0</version> <scope>provided</scope> </dependency>
Since Servlet 3.0 doesn't appear to be in Maven Central, I had to add the GlassFish Repository to my pom.xml's <repositories> element.
<repository> <id>glassfish-repo</id> <url>http://download.java.net/maven/glassfish</url> </repository>
After that, it was easy to implement the LoginService interface with a JavaEELoginService class:
package org.appfuse.examples.webapp.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @Service("javaeeLoginService") public class JavaEELoginService implements LoginService { private Log log = LogFactory.getLog(JavaEELoginService.class); @Autowired HttpServletRequest request; public LoginStatus getStatus() { if (request.getRemoteUser() != null) { return new LoginStatus(true, request.getRemoteUser()); } else { return new LoginStatus(false, null); } } @Override public LoginStatus login(String username, String password) { try { if (request.getRemoteUser() == null) { request.login(username, password); log.debug("Login succeeded!"); } return new LoginStatus(true, request.getRemoteUser()); } catch (ServletException e) { e.printStackTrace(); return new LoginStatus(false, null); } } }
I tried to use this with "mvn jetty:run" (with version 8.0.0.M2 of the jetty-maven-plugin), but I got the following error:
javax.servlet.ServletException at org.eclipse.jetty.server.Request.login(Request.java:1927) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:178) at $Proxy52.login(Unknown Source) at org.appfuse.examples.webapp.security.JavaEELoginService.login(JavaEELoginService.java:30)
This lead me to believe that Servlet 3 is not quite implemented, so I tried it with Tomcat 7.0.8. To support SSL and container-managed authentication, I had to create a certificate keystore and uncomment the SSL Connector in $CATALINA_HOME/conf/server.xml. I also had to add an "admin" user with roles="ROLE_ADMIN" to $CATALINA_HOME/conf/tomcat-users.xml.
<user username="admin" password="admin" roles="ROLE_ADMIN"/>
With Tomcat 7, I was able to login successfully, proven by the following logging.
DEBUG - JavaEELoginService.login(31) | Login succeeded!
However, in the UI, I still got a "Login failed, please try again." message. Recalling that I had some issues with ports previous, I configured Apache to proxy the default http/https ports to 8080/8443 and tried again. This time it worked!
Spring Security LoginService
Spring Security offers a programmatic API and I was able to implement its LoginService as follows:
package org.appfuse.examples.webapp.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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.Service; @Service("springLoginService") public class SpringSecurityLoginService implements LoginService { private Log log = LogFactory.getLog(SpringSecurityLoginService.class); @Autowired(required = false) @Qualifier("authenticationManager") AuthenticationManager authenticationManager; 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); } } public LoginStatus login(String username, String password) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); User details = new User(username); token.setDetails(details); try { Authentication auth = authenticationManager.authenticate(token); log.debug("Login succeeded!"); SecurityContextHolder.getContext().setAuthentication(auth); return new LoginStatus(auth.isAuthenticated(), auth.getName()); } catch (BadCredentialsException e) { return new LoginStatus(false, null); } } }
I then modified the LoginService dependency in LoginController so this implementation would be used.
@Autowired @Qualifier("springLoginService") LoginService loginService;
Since Spring's API doesn't depend on Servlet 3, I tried it in Jetty using "mvn jetty:run". Of course, I modified my web.xml accordingly for Spring Security before doing so. Interestingly enough, I found that the my SpringSecurityLoginService seemed to work:
DEBUG - SpringSecurityLoginService.login(39) | Login succeeded!
But in the UI, the login failed with a "Login failed, please try again." message. Using the standard ports with Apache in front of Jetty solved this issue.
Apache Shiro LoginService
Apache Shiro is nice enough to offer a programmatic API as well. I was able to implement a ShiroLoginService as follows:
package org.appfuse.examples.webapp.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Service; @Service("shiroLoginService") public class ShiroLoginService implements LoginService { private Log log = LogFactory.getLog(ShiroLoginService.class); public LoginStatus getStatus() { Subject currentUser = SecurityUtils.getSubject(); if (currentUser.isAuthenticated()) { return new LoginStatus(true, currentUser.getPrincipal().toString()); } else { return new LoginStatus(false, null); } } public LoginStatus login(String username, String password) { if (!getStatus().isLoggedIn()) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject currentUser = SecurityUtils.getSubject(); try { currentUser.login(token); log.debug("Login succeeded!"); return new LoginStatus(currentUser.isAuthenticated(), currentUser.getPrincipal().toString()); } catch (AuthenticationException e) { return new LoginStatus(false, null); } } else { return getStatus(); } } }
Then I modified the LoginService dependency in LoginController so this implementation would be used.
@Autowired @Qualifier("shiroLoginService") LoginService loginService;
Next, I modified my web.xml for Apache Shiro and tried "mvn jetty:run". Again, the login appeared to succeed (based on log messages) on the server, but failed in the UI. When using http://localhost instead of http://localhost:8080, everything worked fine.
Summary
This article has shown you how you can programmatically login using Java EE 6, Spring Security and Apache Shiro. Before Java EE 6 (and Servlet 3), there was no API to programmatically login, so this is a welcome addition. The fact that my Ajax login example didn't work when ports differed is because of browsers' same origin policy, which specifies the ports have to be the same. Specifying no ports (the defaults), seems to be the loophole.
On a related note, I've discovered some interesting articles recently from the AppSec Blog.
The 2nd article has an interesting paragraph:
... there's Apache Shiro (FKA JSecurity and then later as Apache Ki), another secure framework for Java apps. Although it looks simpler to use and understand than ESAPI and covers most of the main security bases (authentication, authorization, session management and encryption), it doesn't help take care of important functions like input validation and output encoding. And Spring users have Spring Security (Acegi) a comprehensive, but heavyweight authorization and authentication framework.
So according to this blog, the security frameworks discussed here aren't the best.The most comprehensive, up-to-date choice for Java developers is OWASP's ESAPI Enterprise Security API especially now that the 2.0 release has just come out.
I haven't heard of many organizations adopting ESAPI over Java EE 6, Spring Security or Apache Shiro, but maybe I'm wrong. Is ESAPI something that's being used out there by companies?
Posted by luke on June 20, 2011 at 10:53 PM MDT #
Posted by Raible Designs on June 29, 2011 at 03:34 PM MDT #
There is an interface or a class named LoginStatus, but I can't see where it is declared. Any help?
thanks in advance
Posted by Fernando Wermus on August 07, 2013 at 08:19 PM MDT #
Posted by Matt Raible on August 27, 2013 at 04:00 PM MDT #