package com.occamlab.te.realm; import java.io.File; import java.lang.reflect.Constructor; import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.catalina.Realm; import org.apache.catalina.realm.GenericPrincipal; import org.apache.catalina.realm.RealmBase; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.occamlab.te.realm.PasswordStorage.CannotPerformOperationException; import com.occamlab.te.realm.PasswordStorage.InvalidHashException; /** * A custom Tomcat Realm implementation that reads user information from an XML * file located in a sub-directory of the TEAMengine users directory. A sample * representation is shown below. * * <pre> * <user> * <name>p.fogg</name> * <roles> * <name>user</name> * </roles> * <password>password-digest</password> * <email>p.fogg@example.org</email> * </user> * </pre> * <p> * The password digest must be generated using the {@link PasswordStorage * PBKDF2} function; it consists of five fields separated by the colon (':') * character. For example: * <code>sha1:64000:18:a6BHX18eMTR1WnCvyR6NzG6VMJcdJE2D:8qPU0jpdPIapbyC+H5dqiaNE</code> * </p> * * @see <a href="https://github.com/defuse/password-hashing">Secure Password * Storage v2.0</a> */ public class PBKDF2Realm extends RealmBase { private static final Logger LOGR = Logger.getLogger(PBKDF2Realm.class.getName()); private String rootPath = null; private DocumentBuilder DB = null; private HashMap<String, Principal> principals = new HashMap<String, Principal>(); public String getRoot() { return rootPath; } /** * Return the Principal associated with the specified username and * credentials, if one exists in the user data store; otherwise return null. */ @Override public Principal authenticate(String username, String credentials) { GenericPrincipal principal = (GenericPrincipal) getPrincipal(username); if (null != principal) { try { if (!PasswordStorage.verifyPassword(credentials, principal.getPassword())) { principal = null; } } catch (CannotPerformOperationException | InvalidHashException e) { LOGR.log(Level.WARNING, e.getMessage()); principal = null; } } return principal; } @Override protected String getPassword(String username) { GenericPrincipal principal = (GenericPrincipal) getPrincipal(username); if (principal == null) { return null; } else { return principal.getPassword(); } } /** * Return the Principal associated with the given user name. If the user * name starts with an asterisk (*), the credentials are refreshed without * having to restart Tomcat. */ @Override protected Principal getPrincipal(String username) { Principal principal; // force lookup of user info if (username.startsWith("*")) { principal = readPrincipal(username.substring(1)); if (principal != null) { synchronized (principals) { principals.put(username.substring(1), principal); } } } synchronized (principals) { principal = (Principal) principals.get(username); } if (principal == null) { principal = readPrincipal(username); if (principal != null) { synchronized (principals) { principals.put(username, principal); } } } return principal; } @Override protected String getName() { return "UserFilesRealm"; } /** * Sets the location of the root users directory. This is specified by the * "root" attribute of the Realm element in the context definition. * * @param root * A String specifying a directory location (TE_BASE/users). */ public void setRoot(String root) { rootPath = root; } private GenericPrincipal readPrincipal(String username) { List<String> roles = new ArrayList<String>(); File usersdir = new File(rootPath); if (!usersdir.isDirectory()) { usersdir = new File(System.getProperty("TE_BASE"), "users"); } File userfile = new File(new File(usersdir, username), "user.xml"); if (!userfile.canRead()) { return null; } Document userInfo = null; try { if (DB == null) { DB = DocumentBuilderFactory.newInstance().newDocumentBuilder(); } userInfo = DB.parse(userfile); } catch (Exception e) { LOGR.log(Level.WARNING, "Failed to read user info at " + userfile.getAbsolutePath(), e); } Element userElement = (Element) (userInfo.getElementsByTagName("user").item(0)); Element passwordElement = (Element) (userElement.getElementsByTagName("password").item(0)); String password = passwordElement.getTextContent(); Element rolesElement = (Element) (userElement.getElementsByTagName("roles").item(0)); NodeList roleElements = rolesElement.getElementsByTagName("name"); for (int i = 0; i < roleElements.getLength(); i++) { String name = ((Element) roleElements.item(i)).getTextContent(); roles.add(name); } GenericPrincipal principal = createGenericPrincipal(username, password, roles); return principal; } /** * Creates a new GenericPrincipal representing the specified user. * * @param username * The username for this user. * @param password * The authentication credentials for this user. * @param roles * The set of roles (specified using String values) associated * with this user. * @return A GenericPrincipal for use by this Realm implementation. */ @SuppressWarnings({ "rawtypes", "unchecked" }) GenericPrincipal createGenericPrincipal(String username, String password, List<String> roles) { Class klass = null; try { klass = Class.forName("org.apache.catalina.realm.GenericPrincipal"); } catch (ClassNotFoundException ex) { LOGR.log(Level.SEVERE, ex.getMessage()); } Constructor[] ctors = klass.getConstructors(); Class firstParamType = ctors[0].getParameterTypes()[0]; Class[] paramTypes = new Class[] { Realm.class, String.class, String.class, List.class }; Object[] ctorArgs = new Object[] { this, username, password, roles }; GenericPrincipal principal = null; try { if (Realm.class.isAssignableFrom(firstParamType)) { // Tomcat 6 Constructor ctor = klass.getConstructor(paramTypes); principal = (GenericPrincipal) ctor.newInstance(ctorArgs); } else { // Realm parameter removed in Tomcat 7 Constructor ctor = klass.getConstructor(Arrays.copyOfRange(paramTypes, 1, paramTypes.length)); principal = (GenericPrincipal) ctor.newInstance(Arrays.copyOfRange(ctorArgs, 1, ctorArgs.length)); } } catch (Exception ex) { LOGR.log(Level.WARNING, ex.getMessage()); } return principal; } }