Java tutorial
/* * The Fascinator - Common Library * Copyright (C) 2008-2009 University of Southern Queensland * Copyright (C) 2012 Queensland Cyber Infrastructure Foundation (http://www.qcif.edu.au/) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package com.googlecode.fascinator.authentication.custom.ldap; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Hashtable; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.googlecode.fascinator.api.authentication.AuthenticationException; /** * A custom LDAP authentication Handler based on: * <ul> * <li>https://github.com/the-fascinator-contrib/plugin-authentication-ldap/blob/master/src/main/java/com/googlecode/fascinator/authentication/ldap/LdapAuthenticationHandler.java</li> * <li>https://github.com/fcrepo/fcrepo/blob/master/fcrepo-security/fcrepo-security-jaas/src/main/java/org/fcrepo/server/security/jaas/auth/module/LdapModule.java</li> * </ul> * <p> * The handler handles 3 types of binding: * <ul> * <li>direct bind</li> * <li>bind search compare</li> * <li>bind search bind</li> * </ul> * @author danielt@intersect.org.au */ public class CustomLdapAuthenticationHandler { /** Logging */ private static Logger log = LoggerFactory.getLogger(CustomLdapAuthenticationHandler.class); private final static String LDAP_PASSWORD_REGEX = "\\{(.+)\\}(.+)"; private final static Pattern LDAP_PASSWORD_PATTERN = Pattern.compile(LDAP_PASSWORD_REGEX); private static Cache credentialCache; /** LDAP environment */ private Hashtable<String, String> env; /** LDAP Base DN */ private String baseDn; /** Name of the LDAP attribute that defines the role */ private String ldapRoleAttr; /** LDAP identifier attribute */ private String idAttr; /** Base LDAP URL */ private String baseUrl; /** LDAP security principal */ private String ldapSecurityPrincipal; /** LDAP security credentials */ private String ldapSecurityCredentials; /** Prefix for the LDAP query filter */ private String filterPrefix = ""; /** Suffix for the LDAP query filter */ private String filterSuffix = ""; private Map<String, List<String>> ldapRolesMap; /** Bind mode for LDAP */ private String bindMode; /** * Creates an LDAP authenticator for the specified server and base DN, using * the default identifier attribute "uid" * * @param baseUrl * LDAP server URL * @param baseDn * LDAP base DN */ public CustomLdapAuthenticationHandler(String baseUrl, String baseDn, String ldapSecurityPrincipal, String ldapSecurityCredentials) { this(baseUrl, baseDn, ldapSecurityPrincipal, ldapSecurityCredentials, "objectClass", "uid"); } /** * Creates an LDAP authenticator for the specified server, base DN and given * identifier attribute * * @param baseUrl * LDAP server URL * @param baseDn * LDAP base DN * @param ldapSecurityPrincipal * LDAP Security Principal * @param ldapSecurityCredentials * Credentials for Security Principal * @param ldapRoleAttr * Name of the LDAP attribute that defines the role * @param idAttr * LDAP user identifier attribute */ public CustomLdapAuthenticationHandler(String baseUrl, String baseDn, String ldapSecurityPrincipal, String ldapSecurityCredentials, String ldapRoleAttr, String idAttr) { // Set public variables this.baseDn = baseDn; this.idAttr = idAttr; this.ldapRoleAttr = ldapRoleAttr; this.baseUrl = baseUrl; this.ldapSecurityPrincipal = ldapSecurityPrincipal; this.ldapSecurityCredentials = ldapSecurityCredentials; if (CustomLdapAuthenticationHandler.credentialCache == null) { CacheManager singletonManager = CacheManager.create(); CustomLdapAuthenticationHandler.credentialCache = new Cache("credentialCache", 500, false, false, 3600, 1800); singletonManager.addCache(CustomLdapAuthenticationHandler.credentialCache); } // Initialise the LDAP environment env = new Hashtable<String, String>(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, baseUrl); env.put(Context.SECURITY_AUTHENTICATION, "simple"); if (!ldapSecurityPrincipal.equals("")) { env.put(Context.SECURITY_PRINCIPAL, ldapSecurityPrincipal); env.put(Context.SECURITY_CREDENTIALS, ldapSecurityCredentials); } } /** * Creates an LDAP authenticator for the specified server, base DN and given * identifier attribute * * @param baseUrl * LDAP server URL * @param baseDn * LDAP base DN * @param ldapSecurityPrincipal * LDAP Security Principal * @param ldapSecurityCredentials * Credentials for Security Principal * @param ldapRoleAttr * Name of the LDAP attribute that defines the role * @param idAttr * LDAP user identifier attribute * @param ldapRolesMap * Maps relevant LDAP roles to Fascinator roles */ public CustomLdapAuthenticationHandler(String baseUrl, String baseDn, String ldapSecurityPrincipal, String ldapSecurityCredentials, String ldapRoleAttr, String idAttr, Map<String, List<String>> ldapRolesMap) { this(baseUrl, baseDn, ldapSecurityPrincipal, ldapSecurityCredentials, ldapRoleAttr, idAttr); this.ldapRolesMap = ldapRolesMap; } /** * Creates an LDAP authenticator for the specified server, base DN and given * identifier attribute * * @param baseUrl * LDAP server URL * @param baseDn * LDAP base DN * @param ldapSecurityPrincipal * LDAP Security Principal * @param ldapSecurityCredentials * Credentials for Security Principal * @param ldapRoleAttr * Name of the LDAP attribute that defines the role * @param idAttr * LDAP user identifier attribute * @param ldapRolesMap * Maps relevant LDAP roles to Fascinator roles */ public CustomLdapAuthenticationHandler(String baseUrl, String baseDn, String ldapSecurityPrincipal, String ldapSecurityCredentials, String ldapRoleAttr, String idAttr, String filterPrefix, String filterSuffix, Map<String, List<String>> ldapRolesMap) { this(baseUrl, baseDn, ldapSecurityPrincipal, ldapSecurityCredentials, ldapRoleAttr, idAttr, ldapRolesMap); this.filterPrefix = filterPrefix; this.filterSuffix = filterSuffix; } /** * Tries to authenticate user by using default settings, otherwise searches * for the DN of the user * * @param username * a username * @param password * a password * @return <code>true</code> if authentication was successful, * <code>false</code> otherwise * @throws NamingException * @throws AuthenticationException */ public boolean authenticate(String username, String password) throws AuthenticationException, NamingException { if ("bind-search-compare".equals(bindMode)) { return bindSearchX(username, password, env, false); } else if ("bind-search-bind".equals(bindMode)) { return bindSearchX(username, password, env, true); } else if ("bind".equals(bindMode)) { return bind(username, password); } else { throw new AuthenticationException("wrong binding mode used"); } } /** * Attempts to authenticate user credentials with the LDAP server * * @param username * a username * @param password * a password * @param dn * if precise dn known, otherwise should be empty string * @return <code>true</code> if authentication was successful, * <code>false</code> otherwise */ private boolean bind(String username, String password) { try { String principal = String.format("%s=%s,%s", idAttr, username, baseDn); env.put(Context.SECURITY_PRINCIPAL, principal); env.put(Context.SECURITY_CREDENTIALS, password); DirContext ctx = new InitialDirContext(env); ctx.lookup(principal); ctx.close(); return true; } catch (NamingException ne) { log.warn("Failed LDAP lookup doAuthenticate", ne); } return false; } private boolean bindSearchX(String username, String password, Hashtable<String, String> env, boolean bind) throws AuthenticationException, NamingException { env.put(Context.SECURITY_PRINCIPAL, ldapSecurityPrincipal); env.put(Context.SECURITY_CREDENTIALS, ldapSecurityCredentials); DirContext ctx = null; try { ctx = new InitialDirContext(env); } catch (NamingException ne) { log.error("Failed to bind as: {}", ldapSecurityPrincipal); } // ensure we have the userPassword attribute at a minimum String[] attributeList = new String[] { "userPassword" }; SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); sc.setReturningAttributes(attributeList); sc.setDerefLinkFlag(true); sc.setReturningObjFlag(false); sc.setTimeLimit(5000); String filter = "(" + filterPrefix + idAttr + "=" + username + filterSuffix + ")"; // Do the search NamingEnumeration<SearchResult> results = ctx.search(baseDn, filter, sc); if (!results.hasMore()) { log.warn("no valid user found."); return false; } SearchResult result = results.next(); log.debug("authenticating user: {}", result.getNameInNamespace()); if (bind) { // setup user context for binding Hashtable<String, String> userEnv = new Hashtable<String, String>(); userEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); userEnv.put(Context.SECURITY_AUTHENTICATION, "simple"); userEnv.put(Context.PROVIDER_URL, baseUrl); userEnv.put(Context.SECURITY_PRINCIPAL, result.getNameInNamespace()); userEnv.put(Context.SECURITY_CREDENTIALS, password); try { new InitialDirContext(userEnv); } catch (NamingException ne) { log.error("failed to authenticate user: " + result.getNameInNamespace()); throw ne; } } else { // get userPassword attribute Attribute up = result.getAttributes().get("userPassword"); if (up == null) { log.error("unable to read userPassword attribute for: {}", result.getNameInNamespace()); return false; } byte[] userPasswordBytes = (byte[]) up.get(); String userPassword = new String(userPasswordBytes); // compare passwords - also handles encodings if (!passwordsMatch(password, userPassword)) { return false; } } return true; } /** * Performs a search of LDAP * * @param username * The username to be used in the search * @param dc * The directory context to use for the search * @return An enumeration containing the search results * @throws NamingException */ private NamingEnumeration<SearchResult> performLdapSearch(String username, DirContext dc) throws NamingException { SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.SUBTREE_SCOPE); String filter = "(" + filterPrefix + idAttr + "=" + username + filterSuffix + ")"; NamingEnumeration<SearchResult> ne = dc.search(baseDn, filter, sc); log.trace(String.format("performing LDAP search using baseDn: %s, filter: %s", baseDn, filter)); return ne; } /** * Get the value of an attribute from a search result * * @param attrName * The name of the attribute that we're interested in * @param sr * The search result * @return The attribute value * @throws NamingException */ private String getAttrValue(String attrName, SearchResult sr) throws NamingException { // Get all attributes Attributes entry = sr.getAttributes(); // Get the attribute value and return Attribute attrValues = entry.get(attrName); if (attrValues == null) return null; String[] strArr = attrValues.toString().split(":"); return strArr[1].trim(); } /** * Method to compare two passwords. The method attempts to encode the user * password based on the ldap password encoding extracted from the storage * format (e.g. {SHA}g0bbl3d3g00ka12@#19/=). * * @param userPassword * the password that the user entered * @param ldapPassword * the password from the ldap directory * @return true if userPassword equals ldapPassword with respect to encoding */ private static boolean passwordsMatch(String userPassword, String ldapPassword) { Matcher m = LDAP_PASSWORD_PATTERN.matcher(ldapPassword); boolean match = false; if (m.find() && m.groupCount() == 2) { // if password is encoded in the LDAP, encode the password before // compare String encoding = m.group(1); String password = m.group(2); if (log.isDebugEnabled()) { log.debug("Encoding: {}, Password: {}", encoding, password); } MessageDigest digest = null; try { digest = MessageDigest.getInstance(encoding.toUpperCase()); } catch (NoSuchAlgorithmException e) { log.error("Unsupported Algorithm used: {}", encoding); log.error(e.getMessage()); return false; } byte[] resultBytes = digest.digest(userPassword.getBytes()); byte[] result = Base64.encodeBase64(resultBytes); String pwd = new String(password); String ldp = new String(result); match = pwd.equals(ldp); } else { // if passwords are not encoded, just do raw compare match = userPassword.equals(ldapPassword); } return match; } /** * Tries to find the value of the given attribute. Note that this method * only uses the first search result. * * @param username * a username * @param attrName * the name of the attribute to find * @return the value of the attribute, or an empty string */ public String getAttr(String username, String attrName) { String val = ""; try { DirContext dc = new InitialDirContext(env); NamingEnumeration<SearchResult> ne = performLdapSearch(username, dc); if (ne.hasMore()) { val = getAttrValue(attrName, ne.next()); } ne.close(); dc.close(); } catch (NamingException ne) { log.warn("Failed LDAP lookup getAttr", ne); log.warn("username:", username); log.warn("attrName:", attrName); } log.trace(String.format("getAttr search result: %s", val)); return val; } /** * Tries to find the value(s) of the given attribute. Note that this method * uses all search results. * * @param username * a username * @param attrName * the name of the attribute to find * @return a list of values for the attribute, or an empty list */ public List<String> getAllAttrs(String username, String attrName) { List<String> resultList = new ArrayList<String>(); try { DirContext dc = new InitialDirContext(env); NamingEnumeration<SearchResult> ne = performLdapSearch(username, dc); while (ne.hasMore()) { resultList.add(getAttrValue(attrName, ne.next())); } ne.close(); dc.close(); } catch (NamingException ne) { log.warn("Failed LDAP lookup getAllAttrs" + username, ne); } log.trace("getAllAttrs search result: " + resultList); if (log.isTraceEnabled()) { log.trace("getAllAttrs search result: " + resultList); } return resultList; } /** * Searches through the role attribute values and tries to match the given * string. * * @param username * a username * @param testSubj * the string to look for * @return <code>true</code> if string was found <code>false</code> * otherwise */ public boolean testIfInObjectClass(String username, String testSubj) { try { List<String> attrValues = getAllAttrs(username, ldapRoleAttr); for (String attrValue : attrValues) { String[] allVals = attrValue.split(","); for (int i = 0; i < allVals.length; i++) { if (testSubj.equals(allVals[i].trim())) { return true; } } } } catch (Exception e) { // Some problem exists, return false for now return false; } return false; } /** * Get the list of roles that the user is a member of. Maps LDAP roles to * Fascinator roles. * * @param username * The username that identifies the user * @return A list of Fascinator role names */ public List<String> getRoles(String username) { //cache roles Element rolesElement = CustomLdapAuthenticationHandler.credentialCache.get(username); if (rolesElement != null) { return (List<String>) rolesElement.getObjectValue(); } Set<String> roles = new LinkedHashSet<String>(); List<String> attrValues = getAllAttrs(username, ldapRoleAttr); List<String> userRoles = new ArrayList<String>(); //it's always in one row .. if (attrValues.isEmpty()) { return new ArrayList<String>(); } for (String rolesString : attrValues) { addRole(rolesString, userRoles); } for (String objectClass : userRoles) { String[] roleNames = objectClass.split(","); String roleName = roleNames[0]; if (!roleName.startsWith("CN=")) continue; List<String> roleList = ldapRolesMap.get(roleName.substring("CN=".length()).trim()); if (roleList != null) { roles.addAll(roleList); } } CustomLdapAuthenticationHandler.credentialCache.put(new Element(username, new ArrayList<String>(roles))); return new ArrayList<String>(roles); } private void addRole(String rolesString, List<String> userRoles) { String[] roles = rolesString.split("CN="); for (String role : roles) { int commaPos = role.indexOf(","); if (commaPos > -1) { String roleName = "CN=" + role.substring(0, commaPos); String location = role.substring(commaPos + 1).trim(); userRoles.add(roleName); if (location.endsWith(",")) location = location.substring(0, location.length() - 1); String attrValues = performRoleSearch(location, roleName); if (attrValues != null) { addRole(attrValues, userRoles); } } } } private String performRoleSearch(String location, String roleName) { String val = null; try { DirContext dc = new InitialDirContext(env); SearchControls sc = new SearchControls(); sc.setSearchScope(SearchControls.ONELEVEL_SCOPE); //String filter = "(" + filterPrefix + roleName + ")"; NamingEnumeration<SearchResult> ne = dc.search(location, roleName, sc); if (ne.hasMore()) { val = getAttrValue("memberOf", ne.next()); } ne.close(); dc.close(); } catch (NamingException ne) { log.warn("Failed LDAP lookup getAttr", ne); log.warn("roleName:", roleName); log.warn("location:", location); } return val; } public String getBindMode() { return bindMode; } public void setBindMode(String bindMode) { this.bindMode = bindMode; } public Map<String, List<String>> getLdapRolesMap() { return ldapRolesMap; } public void setLdapRolesMap(Map<String, List<String>> ldapRolesMap) { this.ldapRolesMap = ldapRolesMap; } }