[jboss-user] [JBoss Portal] - Re: OpenLDAP integration, just plan lost what to do after se

jon_french@fws.gov do-not-reply at jboss.com
Fri Aug 4 10:46:50 EDT 2006


I was asked by Daniel Valero to post my authentication solution, so here it is. Note that this solution will work against both LDAP servers which allow an anonymous bind and those that do not.

In the example login-config.xml below, I've shown how to set up the LoginModule in the case in which you need a "service" LDAP account in order to do an initial bind to the LDAP server before attempting to bind the given username and password.

Also note that I'm assuming that the user is handing me a username of an email.

Thanks to Scott Dawson of Unisys for putting me on the right track with this solution.

The login-config.xml for JBoss Portal:

 
  | <?xml version='1.0'?>
  | <!DOCTYPE policy PUBLIC
  |       "-//JBoss//DTD JBOSS Security Config 3.0//EN"
  |       "http://www.jboss.org/j2ee/dtd/security_config.dtd">
  | <policy>
  |     ...
  | 
  |     <!-- portal login policy -->
  |     <application-policy name="portal">
  |         <authentication>
  |             <login-module code="org.jboss.security.auth.spi.LdapAuthenticatorLoginModule" flag="optional">
  |                 <module-option name="java.naming.provider.url">ldap://ldap.server.org:636/</module-option>
  |                 <module-option name="java.naming.security.protocol">ssl</module-option><!-- only required for LDAP over SSL -->
  |                 <module-option name="java.naming.security.principal">[insert DN here: ie. CN=My Name</module-option>
  |                 <module-option name="java.naming.security.credentials">[insert password here]</module-option>
  |                 <module-option name="searchBase">dc=mycompany,dc=net</module-option>
  |             </login-module>
  | 
  |             <login-module code="org.jboss.portal.core.security.jaas.ModelLoginModule" flag="required">
  |                 <module-option name="unauthenticatedIdentity">guest</module-option>
  |                 <module-option name="hashAlgorithm">MD5</module-option>
  |                 <module-option name="hashEncoding">HEX</module-option>
  |                 <module-option name="userModuleJNDIName">java:/portal/UserModule</module-option>
  |                 <module-option name="additionalRole">Authenticated</module-option>
  |                 <!-- 
  |                 It is important that the useFirstPass option is set because 
  |                 JbpLdapAuthenticatorLoginModule will set a different password in the
  |                 javax.security.auth.login.password shared module map than that which
  |                 was provided by the Subject if a user successfully authenticates via LDAP
  |                 -->
  |                 <module-option name="password-stacking">useFirstPass</module-option>
  |             </login-module>
  |         </authentication>
  |     </application-policy>
  | </policy>
  | 
  | 

LdapAuthenticatorLoginModule.java


  | package org.jboss.security.auth.spi;
  | 
  | import java.security.acl.Group;
  | import javax.security.auth.callback.CallbackHandler;
  | import javax.security.auth.Subject;
  | import javax.naming.InitialContext;
  | import javax.security.auth.login.LoginException;
  | 
  | import java.util.Hashtable;
  | import java.util.Iterator;
  | import java.util.Map;
  | import java.util.Map.Entry;
  | 
  | import org.jboss.security.SimpleGroup;
  | 
  | import org.apache.commons.lang.StringUtils;
  | 
  | import gov.doi.usgs.util.AuthenticationHelper;
  | 
  | /**
  |  * LoginModule that authenticates users based on an email and password lookup
  |  * from an LDAP server. The following options must be configured for this module:
  |  * <ul>
  |  * <li>java.naming.provider.url - the url to the ldap server (ie. ldap://ldap.myserver.gov:389)</li>
  |  * <li>searchBase - Use searchbase as the starting point for the LDAP search. [required]</li>
  |  * </ul>
  |  * The following options may be optionally configured for this module:
  |  * <ul>
  |  * <li>java.naming.security.principal - the distinguished name used to bind to the ldap server 
  |  * in order to perform the orginal email query. Leave blank for anonymous bind.</li>
  |  * <li>java.naming.security.credentials - the password used to bind to the ldap sesrver in order
  |  * to perform the orginal email query. Leave blank for anonymous bind.</li>
  |  * <li>socketTimeout - How long should the module wait in millisecs for a connection to the LDAP server before it timesout.</li>
  |  * <li>java.naming.security.protocol - set to "ssl" to use ssl encrpytion</li>
  |  * </ul>
  |  * <p>
  |  * This LoginModule is used strictly for authentication (role assignment) and performs no authorization. 
  |  * Subsequent LoginModules should perform authorization.
  |  * <p>
  |  * The module works like this:
  |  * <ol>
  |  * <li>The module will attempt to find a node of objectClass=person and mail=[given username] 
  |  * by binding to the ldap server using the url, dn, and password configured in the module options</li>
  |  * <li>If a matching dn is found, the module will then attempt to bind to the ldap server using
  |  * the found dn and the given password.</li>
  |  * <li>If the second bind is successful, the module will ensure that
  |  *      <ol>
  |  *          <li>the user's email and password are set into the LoginModule shareMemory map under
  |  *          the <tt>javax.security.auth.login.name</tt> and <tt>javax.security.auth.login.password</tt>
  |  *          keys respectively. This makes sure that all subsequent LoginModules that use password-stacking
  |  *          will use these values as the username/password instead of those provided by the CallbackHandler.</li>
  |  *      </ol>
  |  * </li>
  |  * </ol>
  |  * 
  |  * @author Jon French
  |  */ 
  | public class LdapAuthenticatorLoginModule extends UsernamePasswordLoginModule
  | {
  |     public static final String SEARCH_BASE = "searchBase";
  |     public static final String SOCKET_TIMEOUT = "socketTimeout";
  | 
  |     public LdapAuthenticatorLoginModule(){}
  | 
  |     private transient SimpleGroup userRoles = new SimpleGroup("Roles");
  | 
  |     /** 
  |      * Overriden to return an empty password string as typically one cannot
  |      * obtain a user's password. We also override the validatePassword so
  |      * this is ok.
  |      @return and empty password String
  |      */
  |     protected String getUsersPassword() throws LoginException
  |     {
  |        return "";
  |     }
  | 
  |     /** 
  |      * Return a dummy set of userRoles. We don't care that they are not
  |      * appropriately populated. The population of the user roles will be
  |      * the responsibility of a second LoginModule.
  |      * @return Group[] containing the sets of roles 
  |      */
  |     protected Group[] getRoleSets() throws LoginException
  |     {
  |        Group[] roleSets = {userRoles};
  | 
  |        return roleSets;
  |     }
  | 
  |     /** 
  |      * Returns true if the username and password validates against the
  |      * configured LDAP options.
  |      * 
  |      * @param inputPassword the password to validate.
  |      * @param expectedPassword ignored
  |      */
  |     protected boolean validatePassword(String inputPassword, String expectedPassword)
  |     {
  |        boolean isValid = false;
  |        if (inputPassword != null)
  |        {
  |           // See if this is an empty password that should be disallowed
  |           if (inputPassword.trim().length() == 0)
  |           {
  |               return false; //never allow empty passwords
  |           }
  |           try
  |           {
  |              // Validate the password by trying to create an initial context
  |              String username = getUsername();
  | 
  |              verifyLdapBind(username, inputPassword);
  | 
  |              sharedState.put("javax.security.auth.login.name",username);
  |              sharedState.put("javax.security.auth.login.password",inputPassword);
  | 
  |              isValid = true;
  |           }
  |           catch (Exception e)
  |           {
  |              super.log.debug("Failed to validate password", e);
  |           }
  |        }
  |        return isValid;
  |     }
  | 
  |     /**
  |      * Attempt to bind to the LDAP server configured in the module options and 
  |      * confirm that there [1] is a user with an mail attribute matching the given
  |      * username and [2] the DN of that user will allow a success bind with the
  |      * given credential as a String password.
  |      * 
  |      * @throws Exception if either step [1] or [2] fails
  |      */
  |     private void verifyLdapBind(String username, Object credential)
  |     throws Exception{
  | 
  |         Hashtable env = new Hashtable();
  |        
  |         // Map all option into the JNDI InitialLdapContext env
  |         Iterator iter = options.entrySet().iterator();
  |         while (iter.hasNext())
  |         {
  |            Entry entry = (Entry) iter.next();
  |            env.put(entry.getKey(), entry.getValue());
  |         }
  | 
  |         /* Set the timeout in case an LDAP Server may not be connected */
  |         String timeout = (String) options.get(SOCKET_TIMEOUT);
  |         if (timeout == null) timeout = "2000";
  |         env.put("com.sun.jndi.ldap.connect.timeout", timeout);
  | 
  |         switch(AuthenticationHelper.verifyLdapBind(env,username,(String) credential,(String)options.get(SEARCH_BASE))) {
  |         case AuthenticationHelper.USERNAME_PASSWORD_AUTHENTICATED:
  |             return;
  |         case AuthenticationHelper.NO_USERNAME_FOUND:
  |             throw new Exception("Failed to find dn for : " + username);
  |         case AuthenticationHelper.INVALID_PASSWORD:
  |             InitialContext _ctx = new InitialContext();
  |             throw new Exception("Failed to validate password for : " + username);
  |         }
  |     }
  | 
  | }
  | 

Authentication Helper:


  | package gov.doi.usgs.util;
  | 
  | import javax.naming.Context;
  | import javax.naming.NamingEnumeration;
  | import javax.naming.NamingException;
  | import javax.naming.ldap.LdapContext;
  | import javax.naming.directory.SearchResult;
  | import javax.naming.directory.SearchControls;
  | import javax.naming.ldap.InitialLdapContext;
  | 
  | import java.util.Hashtable;
  | 
  | import org.apache.commons.logging.Log;
  | import org.apache.commons.logging.LogFactory;
  | 
  | /**
  |  * Contains static methods to aid in Authentication coordination in the 
  |  * my.usgs.gov appliation
  |  */
  | 
  | public class AuthenticationHelper {
  | 
  |     private static final Log LOG = LogFactory.getLog(AuthenticationHelper.class);
  | 
  |     private static final String[] EMPTY_STR_ARRAY = new String[0]; 
  | 
  |     public final static int 
  |         USERNAME_PASSWORD_AUTHENTICATED = 100,
  |         NO_USERNAME_FOUND = 1000,
  |         INVALID_PASSWORD = 200;
  | 
  |     /**
  |      * Returns an int which indicates if the given username and password 
  |      * authenticates against the LDAP server configured in the given Map.
  |      * <p>
  |      * The method will attempt to bind to the LDAP server configured by the 
  |      * <tt>envMap</tt> argument and confirm that there [1] is a user with an mail 
  |      * attribute matching the given email and [2] the DN of that user will allow 
  |      * a successful bind with the given credential as a String password.
  |      * 
  |      * @param envMap A Hashtable that will be passed to the InitialLdapContext with the
  |      * appropriately configured to do a initial bind to the ldap server configured
  |      * in the map. The method will add the following properties to the Hashtable and thus
  |      * override any prior configured map entries: Context.INITIAL_CONTEXT_FACTORY,
  |      * Context.SECURITY_AUTHENTICATION
  |      * 
  |      * @param email The email for which to search. 
  |      * 
  |      * @param searchBase The LDAP search base to use.
  |      * 
  |      * @returns 
  |      * USERNAME_PASSWORD_AUTHENTICATED if the username/password successfully authenticates;   
  |      * NO_USERNAME_FOUND if the username was not found in the LDAP server;
  |      * INVALID_PASSWORD if the username was found, but the password was invalid;
  |      */
  |     public static int verifyLdapBind(Hashtable envMap, String email, String password, String searchBase)
  |     throws Exception {
  | 
  |         envMap.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
  |         envMap.put(Context.SECURITY_AUTHENTICATION,"simple");
  | 
  |         LdapContext ctx = null;
  | 
  |         try{
  | 
  |             if (LOG.isTraceEnabled()) {
  |                 LOG.trace("Logging into LDAP server, env=" + envMap);
  |             }
  | 
  |             ctx = new InitialLdapContext(envMap,null);
  |             SearchControls sc = new SearchControls();
  | 
  |             sc.setReturningAttributes(EMPTY_STR_ARRAY);
  |             sc.setReturningObjFlag(true);// required to get the dn of the returned Context#getNameInNamespace() in the SearchResults
  | 
  |             //Specify the search scope
  |             sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
  | 
  |             //specify the LDAP search filter
  |             String sf = "(&(objectClass=person)(mail=" + email + "))";
  | 
  |             sc.setCountLimit(1); // only one result needed if found.
  |             sc.setTimeLimit(2000); // 2 second timelimit. -- Is this used??
  | 
  |             String dn = null;
  | 
  |             NamingEnumeration results = ctx.search(searchBase, sf, sc);
  | 
  |             //Loop through the search results
  |             while (results.hasMoreElements()) {
  | 
  |                 if (LOG.isTraceEnabled()) {
  |                     LOG.trace("Result found!");
  |                 }
  | 
  |                 SearchResult sr = (SearchResult)results.next();
  | 
  |                 dn = ((Context)sr.getObject()).getNameInNamespace();
  | 
  |                 if (LOG.isTraceEnabled()) LOG.trace("SearchResult: " + sr);
  |             }
  | 
  |             if (dn == null) {
  |                 return NO_USERNAME_FOUND;
  |             } else {
  |                 /* Now that you have the dn, try to reconnect to the LDAP server
  |                 binding with the new DN and password. This will throw an
  |                 exception if the reconnect is not successuful*/
  |                 try {
  |                     ctx.addToEnvironment(Context.SECURITY_PRINCIPAL,dn);
  |                     ctx.addToEnvironment(Context.SECURITY_CREDENTIALS,password);
  |                     ctx.reconnect(ctx.getConnectControls());
  |                 } catch(NamingException e){
  |                     /* 
  |                     If we are at this point, the user's email was found in the 
  |                     LDAP server, but the provided password did not authenticate. 
  |                     */
  |                     return INVALID_PASSWORD;
  |                 }
  |                 return USERNAME_PASSWORD_AUTHENTICATED;
  |             }
  | 
  |         } finally{
  |             if (ctx != null) ctx.close();
  |         }
  |     }
  | }
  | 

View the original post : http://www.jboss.com/index.html?module=bb&op=viewtopic&p=3963206#3963206

Reply to the post : http://www.jboss.com/index.html?module=bb&op=posting&mode=reply&p=3963206



More information about the jboss-user mailing list