I have a project that uses Amazon’s SimpleDB service for data storage. Being a Java programmer, I have become fond of using JPA (Java Persistence Architecture) implementations. In some cases, I’ve used EclipseLink, but more recently I’ve been playing with SimpleJPA. This is a partial JPA implementation on top of SimpleDB. The benefits include writing value objects with minimal annotations to indicate relationships.
Anyway, enough about why I do it. Since my user list is also stored in JPA entities, I’d like to tie this into the container managed authentication. The web app I’m writing is being deployed to tomcat and so realms are used to define a authentication provider. Tomcat provides several realms that hook into a JDBC Database, JAAS, JNDI Datasource and more. In my case, I wanted to rely in data access via JPA. Before discussing the challenges, I should point out that in a Java web app container, there are different class loaders to contend with. The container has its own classloader, and each web application has its own. My application obviously contains all of the supporting jars for SimpleJPA and my value objects. Since authentication is being handled by the container, it doesn’t have access to my app’s classloader. So, I’d need to deploy about 12 jar files into the tomcat/lib directory to make them available to the container. One of those contains my value objects and could change in the future. I don’t think that’s a very nice deployment strategy (deploying a war, and then a separate jar for each software update).
To solve this problem, I had to come up with a way to write my own Realm with as few dependencies on my application as possible. What I came up with is a socket listener, running on a dedicated socket, within my web application. It only accepts connections from localhost, so it is not likely to become a security concern. The socket listener receives a username and returns username,password,role1,role2,… as a string. That is the contract between my web application and the authentication realm. The realm interfaces with the socket listener and uses that to get information about the user trying to authenticate, which is converts to the object format used within realms in tomcat.
The code for the socket listener is fairly simple;
package org.scalabletype.util; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.ServerSocket; import java.net.UnknownHostException; import javax.persistence.EntityManager; import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.scalabletype.data.DataHelper; import org.scalabletype.data.User; /** * This class listens on a port, receives a username, looks up user record, then responds with data. */ public class AuthServer extends Thread { private static Log logger = LogFactory.getLog(AuthServer.class); public static final int AUTH_SOCKET = 2000; public AuthServer() { } public void run() { while (!isInterrupted()) { try { ServerSocket ss = new ServerSocket(AUTH_SOCKET); while (!isInterrupted()) { Socket sock = ss.accept(); try { // confirm connection from localhost only InetAddress addr = sock.getInetAddress(); if (addr.getHostName().equals("localhost")) { // get user to authenticate InputStream iStr = sock.getInputStream(); byte [] buf = new byte[1024]; int bytesRead = iStr.read(buf); String username = new String(buf, 0, bytesRead); logger.info("username to authenticate:"+username); // fetch user from JPA EntityManager em = DataHelper.getEntityManager(); Query query = em.createQuery("select object(o) from User o where o.username = :name"); query.setParameter("name", username); User usr = (User)query.getSingleResult(); // return user data, or nothing OutputStream oStr = sock.getOutputStream(); logger.info("got connection, going to respond"); if (usr != null) { StringBuilder ret = new StringBuilder(); ret.append(usr.getUsername()); ret.append(","); ret.append(usr.getPassword()); ret.append(","); ret.append(usr.getAuthGroups()); oStr.write(ret.toString().getBytes()); } oStr.flush(); } sock.close(); } catch (Exception ex) { logger.error("Some problem handling the request", ex); } } } catch (Exception ex) { logger.error("problem accepting connection. will keep going.", ex); } } } }
The socket listener needs to be invoked when the web application is initialized and a ServletContextListener is a good place to do that;
public class ScalableTypeStarter implements ServletContextListener { private AuthServer auth; public void contextInitialized(ServletContextEvent evt) { // init data persistence layer DataHelper.initDataHelper(evt.getServletContext()); // start authorization socket listener auth = new AuthServer(); auth.start(); } public void contextDestroyed(ServletContextEvent evt) { if (auth != null) { auth.interrupt(); auth = null; } } }
Here is the code for my realm, which is packaged by itself into a jar, and deployed (once) into the tomcat/lib directory.
package org.scalabletype.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; import java.security.Principal; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.catalina.Group; import org.apache.catalina.Role; import org.apache.catalina.User; import org.apache.catalina.UserDatabase; import org.apache.catalina.realm.GenericPrincipal; import org.apache.catalina.realm.RealmBase; /** * This realm authenticates against user data via the socket listener. * */ public class UserRealm extends RealmBase { public static final int AUTH_SOCKET = 2000; protected final String info = "org.scalabletype.util.UserRealm/1.0"; protected static final String name = "UserRealm"; /** * Return descriptive information about this Realm implementation and * the corresponding version number, in the format * <code><description>/<version></code>. */ public String getInfo() { return info; } /** * Return <code>true</code> if the specified Principal has the specified * security role, within the context of this Realm; otherwise return * <code>false</code>. This implementation returns <code>true</code> * if the <code>User</code> has the role, or if any <code>Group</code> * that the <code>User</code> is a member of has the role. * * @param principal Principal for whom the role is to be checked * @param role Security role to be checked */ public boolean hasRole(Principal principal, String role) { if (principal instanceof GenericPrincipal) { GenericPrincipal gp = (GenericPrincipal)principal; if(gp.getUserPrincipal() instanceof User) { principal = gp.getUserPrincipal(); } } if (!(principal instanceof User) ) { //Play nice with SSO and mixed Realms return super.hasRole(principal, role); } if ("*".equals(role)) { return true; } else if(role == null) { return false; } User user = (User)principal; UserInfo usr = findUser(user.getFullName()); if (usr == null) { return false; } for (String group : usr.groups) { if (role.equals(group)) return true; } return false; } /** * Return a short name for this Realm implementation. */ protected String getName() { return name; } /** * Return the password associated with the given principal's user name. */ protected String getPassword(String username) { UserInfo user = findUser(username); if (user == null) { return null; } return (user.password); } /** * Return the Principal associated with the given user name. */ protected Principal getPrincipal(String username) { UserInfo user = findUser(username); if(user == null) { return null; } List roles = new ArrayList(); for (String group : user.groups) { roles.add(group); } return new GenericPrincipal(this, username, user.password, roles); } private UserInfo findUser(String username) { UserInfo user = new UserInfo(); try { Socket sock = new Socket("localhost", AUTH_SOCKET); OutputStream oStr = sock.getOutputStream(); oStr.write(username.getBytes()); oStr.flush(); InputStream iStr = sock.getInputStream(); byte [] buf = new byte[4096]; int len = iStr.read(buf); if (len == 0) { return null; } String [] data = new String(buf, 0, len).split(","); user.username = data[0]; user.password = data[1]; ArrayList<String> groups = new ArrayList<String>(); for (int i=2; i<data.length; i++) { groups.add(data[i]); } user.groups = groups; } catch (UnknownHostException ex) { ex.printStackTrace(); } catch (IOException ex) { ex.printStackTrace(); } return user; } class UserInfo { String username; String password; List<String> groups; } }
The web app’s context.xml contains this line to configure the realm;
<Realm className="org.scalabletype.util.UserRealm" resourceName="ScalableTypeAuth"/>
Image may be NSFW.
Clik here to view.
Clik here to view.
