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
_______________________________________________
jboss-user mailing list
jboss-user@lists.jboss.org
https://lists.jboss.org/mailman/listinfo/jboss-user

Reply via email to