1   package com.atlassian.user.impl.ldap.security.authentication;
2   
3   import com.atlassian.user.EntityException;
4   import com.atlassian.user.impl.ldap.properties.LdapConnectionProperties;
5   import com.atlassian.user.impl.ldap.properties.LdapSearchProperties;
6   import com.atlassian.user.impl.ldap.repository.LdapContextFactory;
7   import com.atlassian.user.impl.ldap.search.DefaultLDAPUserAdaptor;
8   import com.atlassian.user.impl.ldap.search.LDAPUserAdaptor;
9   import com.atlassian.user.impl.ldap.search.LdapFilterFactory;
10  import com.atlassian.user.repository.RepositoryIdentifier;
11  import com.atlassian.user.security.authentication.Authenticator;
12  import com.atlassian.util.profiling.UtilTimerStack;
13  import net.sf.ldaptemplate.support.filter.AndFilter;
14  import net.sf.ldaptemplate.support.filter.EqualsFilter;
15  import org.apache.commons.lang.StringUtils;
16  import org.apache.log4j.Logger;
17  
18  import javax.naming.NamingException;
19  import javax.naming.AuthenticationException;
20  import javax.naming.directory.DirContext;
21  import javax.naming.directory.InitialDirContext;
22  import javax.naming.directory.SearchControls;
23  import java.util.Hashtable;
24  
25  public class DefaultLDAPAuthenticator implements Authenticator
26  {
27      private static final Logger log = Logger.getLogger(DefaultLDAPAuthenticator.class);
28      private final LDAPUserAdaptor userAdaptor;
29      private final LdapSearchProperties searchProperties;
30      private final RepositoryIdentifier repositoryIdentifier;
31      private final LdapConnectionProperties connectionProperties;
32      private final LdapFilterFactory filterFactory;
33      private final LdapContextFactory contextFactory;
34  
35      public DefaultLDAPAuthenticator(RepositoryIdentifier repositoryIdentifier, LdapContextFactory contextFactory,
36          LdapSearchProperties searchProperties, LdapConnectionProperties connectionProperties,
37          LdapFilterFactory filterFactory)
38      {
39          this.repositoryIdentifier = repositoryIdentifier;
40          this.filterFactory = filterFactory;
41          this.searchProperties = searchProperties;
42          this.connectionProperties = connectionProperties;
43          this.contextFactory = contextFactory;
44          this.userAdaptor = new DefaultLDAPUserAdaptor(contextFactory, searchProperties, filterFactory);
45      }
46  
47      /**
48       * Entering blank password will always fail, regardless of whether the underlying LDAP allows anonymous user
49       * connects.
50       * <p/>
51       * This code duplicates the logic in the LDAPCredentialsProvider of OSUUser:  authenticate(String name, String
52       * password)
53       * <p/>
54       * Sets up a LDAP DirContext (that will contain the credentials the user has just entered), and tries to perform a
55       * search with it as a means of testing whether the credentials passed in are correct.
56       *
57       * @return true if the username and password were successfully authenticated, false otherwise
58       */
59      public boolean authenticate(String username, String password) throws EntityException
60      {
61          if (UtilTimerStack.isActive())
62              UtilTimerStack.push(this.getClass().getName() + "_authenticate__" + username);
63  
64          // USER-140
65          if (StringUtils.isEmpty(password))
66          {
67              if (log.isDebugEnabled())
68                  log.debug("Cannot perform authentication on empty passwords.");
69  
70              return false;
71          }
72  
73          String userDN;
74          DirContext authCtx = null;
75  
76          try
77          {
78              userDN = userAdaptor.getUserDN(username);
79          }
80          catch (EntityException e)
81          {
82              log.error("Could not construct DN to authenticate user: " + username, e);
83              return false;
84          }
85          try
86          {
87              Hashtable authEnv = contextFactory.getAuthenticationJndiEnvironment(userDN, password);
88              authCtx = new InitialDirContext(authEnv);
89  
90              // we need to attempt a search with this context to see if the user has been authenticated (its not enough to just create a context!)
91              SearchControls ctls = new SearchControls();
92              ctls.setReturningAttributes(new String[]{searchProperties.getUsernameAttribute()});
93              ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
94  
95              AndFilter filter = new AndFilter();
96              filter.and(filterFactory.getUserSearchFilter());
97              filter.and(new EqualsFilter(searchProperties.getUsernameAttribute(), username));
98  
99              if (log.isDebugEnabled())
100             {
101                 log.debug("Doing initial search to complete authentication, username: '" + username + "', " +
102                     "base: '" + searchProperties.getBaseUserNamespace() + "' filter: '" + filter.encode() + "'");
103             }
104 
105             // we don't care about the results, only that this search works
106             authCtx.search(searchProperties.getBaseUserNamespace(), filter.encode(), ctls);
107         }
108         catch (AuthenticationException e)
109         {
110             if (log.isDebugEnabled())
111                 log.debug("LDAP authentication failed, user: '" + username + "', constructed DN: '" + userDN + "'", e);
112             return false;
113         }
114         catch (NamingException e) // will also catch AuthenticationException's
115         {
116             log.error("LDAP authentication error, user: '" + username + "', " +
117                 "constructed DN: '" + userDN + "', connectionProperties: " + connectionProperties, e);
118             return false;
119         }
120         catch (Throwable t)
121         {
122             log.error("Error occurred in LDAP authentication for username: " + username, t);
123             return false;
124         }
125         finally
126         {
127             try
128             {
129                 if (authCtx != null) authCtx.close();
130             }
131             catch (Exception e)
132             {
133                 log.warn("Exception closing LDAP connection, possible resource leak", e);
134             }
135 
136             if (UtilTimerStack.isActive())
137                 UtilTimerStack.pop(this.getClass().getName() + "_authenticate__" + username);
138         }
139 
140         return true;
141     }
142 
143     /**
144      * Gets the repository which the authenticator belongs to
145      */
146     public RepositoryIdentifier getRepository()
147     {
148         return repositoryIdentifier;
149     }
150 }