Java Web Application Security - Part I: Java EE 6 Login Demo
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 proposals are accepted). For these events, I'm hoping to present the following talk:
Webapp Security: Develop. Penetrate. Protect. Relax.
In this session, you'll learn how to implement authentication in your Java web applications using Spring Security, Apache Shiro and good ol' Java EE Container Managed Authentication. You'll also learn how to secure your REST API with OAuth and lock it down with SSL.
After learning how to develop authentication, I'll introduce you to OWASP, the OWASP Top 10, its Testing Guide and its Code Review Guide. From there, I'll discuss using WebGoat to verify your app is secure and commercial tools like webapp firewalls and accelerators.
Fast forward a couple months and I'm happy to say that I've completed my talk at the Utah JUG and it's been accepted at Jazoon and Über Conf. For this talk, I created a presentation that primarily consists of demos implementing basic, form and Ajax authentication using Java EE 6, Spring Security and Apache Shiro. In the process of creating the demos, I learned (or re-educated myself) how to do a number of things in all 3 frameworks:
- Implement Basic Authentication
- Implement Form-based Authentication
- Implement Ajax HTTP -> HTTPS Authentication (with programmatic APIs)
- Force SSL for certain URLs
- Implement a file-based store of users and passwords (in Jetty/Maven and Tomcat standalone)
- Implement a database store of users and passwords (in Jetty/Maven and Tomcat standalone)
- Encrypt Passwords
- Secure methods with annotations
For the demos, I showed the audience how to do almost all of these, but skipped Tomcat standalone and securing methods in the interest of time. In July, when I do this talk at ÜberConf, I plan on adding 1) hacking the app (to show security holes) and 2) fixing it to protect it against vulnerabilities.
I told the audience at UJUG that I would post the presentation and was planning on recording screencasts of the various demos so the online version of the presentation would make more sense. Today, I've finished the first screencast showing how to implement security with Java EE 6. Below is the presentation (with the screencast embedded on slide 10) as well as a step-by-step tutorial.
Java EE 6 Login Tutorial
- Download and Run the Application
- Implement Basic Authentication
- Implement Form-based Authentication
- Force SSL
- Store Users in a Database
- Summary
Download and Run the Application
To begin, download the application you'll be implementing security in. This app is a stripped-down version of the Ajax Login application I wrote for my article on Implementing Ajax Authentication using jQuery, Spring Security and HTTPS. You'll need Java 6 and Maven installed to run the app. Run it using mvn jetty:run and open http://localhost:8080 in your browser. You'll see it's a simple CRUD application for users and there's no login required to add or delete users.
Implement Basic Authentication
The first step is to protect the list screen so people have to login to view users. To do this, add the following to the bottom of src/main/webapp/WEB-INF/web.xml:
<security-constraint> <web-resource-collection> <web-resource-name>users</web-resource-name> <url-pattern>/users</url-pattern> <http-method>GET</http-method> <http-method>POST</http-method> </web-resource-collection> <auth-constraint> <role-name>ROLE_ADMIN</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>Java EE Login</realm-name> </login-config> <security-role> <role-name>ROLE_ADMIN</role-name> </security-role>
At this point, if you restart Jetty (Ctrl+C and jetty:run again), you'll get an error about a missing LoginService. This happens because Jetty doesn't know where the "Java EE Login" realm is located. Add the following to pom.xml, just after </webAppConfig> in the Jetty plugin's configuration.
<loginServices> <loginService implementation="org.eclipse.jetty.security.HashLoginService"> <name>Java EE Login</name> <config>${basedir}/src/test/resources/realm.properties</config> </loginService> </loginServices>
The realm.properties file already exists in the project and contains user names and passwords. Start the app again using mvn jetty:run and you should be prompted to login when you click on the "Users" tab. Enter admin/admin to login.
After logging in, you can try to logout by clicking the "Logout" link in the top-right corner. This calls a LogoutController with the following code that logs the user out.
public void logout(HttpServletResponse response) throws ServletException, IOException { request.getSession().invalidate(); response.sendRedirect(request.getContextPath()); }
You'll notice that clicking this link doesn't log you out, even though the session is invalidated. The only way to logout with basic authentication is to close the browser. In order to get the ability to logout, as well as to have more control over the look-and-feel of the login, you can implement form-based authentication.
Implement Form-based Authentication
To change from basic to form-based authentication, you simply have to replace the <login-config> in your web.xml with the following:
<login-config> <auth-method>FORM</auth-method> <form-login-config> <form-login-page>/login.jsp</form-login-page> <form-error-page>/login.jsp?error=true</form-error-page> </form-login-config> </login-config>
The login.jsp page already exists in the project, in the src/main/webapp directory. This JSP has 3 important elements: 1) a form that submits to "${contextPath}/j_security_check", 2) an input element named "j_username" and 3) an input element named "j_password". If you restart Jetty, you'll now be prompted to login with this JSP instead of the basic authentication dialog.
Force SSL
Another thing you might want to implement to secure your application is forcing SSL for certain URLs. To do this on the same <security-constraint> you already have in web.xml, add the following after </auth-constraint>:
<user-data-constraint> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint>
To configure Jetty to listen on an SSL port, add the following just after </loginServices> in your pom.xml:
<connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <forwarded>true</forwarded> <port>8080</port> </connector> <connector implementation="org.eclipse.jetty.server.ssl.SslSelectChannelConnector"> <forwarded>true</forwarded> <port>8443</port> <maxIdleTime>60000</maxIdleTime> <keystore>${project.build.directory}/ssl.keystore</keystore> <password>appfuse</password> <keyPassword>appfuse</keyPassword> </connector> </connectors>
The keystore must be generated for Jetty to start successfully, so add the keytool-maven-plugin just above the jetty-maven-plugin in pom.xml.
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>keytool-maven-plugin</artifactId> <version>1.0</version> <executions> <execution> <phase>generate-resources</phase> <id>clean</id> <goals> <goal>clean</goal> </goals> </execution> <execution> <phase>generate-resources</phase> <id>genkey</id> <goals> <goal>genkey</goal> </goals> </execution> </executions> <configuration> <keystore>${project.build.directory}/ssl.keystore</keystore> <dname>cn=localhost</dname> <keypass>appfuse</keypass> <storepass>appfuse</storepass> <alias>appfuse</alias> <keyalg>RSA</keyalg> </configuration> </plugin>
Now if you restart Jetty, go to http://localhost:8080 and click on the "Users" tab, you'll get a 403. What the heck?! When this first happened to me, it took me a while to figure out. It turns out that Jetty doesn't redirect to HTTPS when using Java EE authentication, so you have to manually type in https://localhost:8443/ (or add a filter to redirect for you). If you deployed this same application on Tomcat (after enabling SSL), it would redirect for you.
Store Users in a Database
Finally, to store your users in a database instead of file, you'll need to change the <loginService> in the Jetty plugin's configuration. Replace the existing <loginService> element with the following:
<loginServices> <loginService implementation="org.eclipse.jetty.security.JDBCLoginService"> <name>Java EE Login</name> <config>${basedir}/src/test/resources/jdbc-realm.properties</config> </loginService> </loginServices>
The jdbc-realm.properties file already exists in the project and contains the database settings and table/column names for the user and role information.
jdbcdriver = com.mysql.jdbc.Driver url = jdbc:mysql://localhost/appfuse username = root password = usertable = app_user usertablekey = id usertableuserfield = username usertablepasswordfield = password roletable = role roletablekey = id roletablerolefield = name userroletable = user_role userroletableuserkey = user_id userroletablerolekey = role_id cachetime = 300
Of course, you'll need to install MySQL for this to work. After installing it, you should be able to create an "appfuse" database and populate it using the following commands:
mysql -u root -p -e 'create database appfuse' curl https://gist.github.com/raw/958091/ceecb4a6ae31c31429d5639d0d1e6bfd93e2ea42/create-appfuse.sql > create-appfuse.sql mysql -u root -p appfuse < create-appfuse.sql
Next you'll need to configure Jetty so it has MySQL's JDBC Driver in its classpath. To do this, add the following dependency just after the <configuration> element (before <executions>) in pom.xml:
<dependencies> <!-- MySQL for JDBC Realm --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.14</version> </dependency> </dependencies>
Now run the jetty-password.sh file in the root directory of the project to generate a password of your choosing. For example:
$ sh jetty-password.sh javaeelogin javaeelogin OBF:1vuj1t2v1wum1u9d1ugo1t331uh21ua51wts1t3b1vur MD5:53b176e6ce1b5183bc970ef1ebaffd44
The last two lines are obfuscated and MD5 versions of the password. Update the admin user's password to this new value. You can do this with the following SQL statement.
UPDATE app_user SET password='MD5:53b176e6ce1b5183bc970ef1ebaffd44' WHERE username = 'admin';
Now if you restart Jetty, you should be able to login with admin/javaeelogin and view the list of users.
Summary
In this tutorial, you learned how to implement authentication using standard Java EE 6. In addition to the basic XML configuration, there's also some new methods in HttpServletRequest for Java EE 6 and Servlet 3.0:
- authenticate(response)
- login(user, pass)
- logout()
This tutorial doesn't show you how to use them, but I did play with them a bit as part of my UJUG demo when implementing Ajax authentication. I found that login() did work, but it didn't persist the authentication for the users session. I also found that after calling logout(), I still needed to invalidate the session to completely logout the user. There are some additional limitations I found with Java EE authentication, namely:
- No error messages for failed logins
- No Remember Me
- No auto-redirect from HTTP to HTTPS
- Container has to be configured
- Doesn’t support regular expressions for URLs
Of course, no error messages indicating why login failed is probably a good thing (you don't want to tell users why their credentials failed). However, when you're trying to figure out if your container is configured properly, the lack of container logging can be a pain.
In the next couple weeks, I'll post Part II of this series, where I'll show you how to implement this same set of features using Spring Security. In the meantime, please let me know if you have any questions.