1   /**
2    * Copyright (c) 2000-2008 Liferay, Inc. All rights reserved.
3    *
4    * Permission is hereby granted, free of charge, to any person obtaining a copy
5    * of this software and associated documentation files (the "Software"), to deal
6    * in the Software without restriction, including without limitation the rights
7    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    * copies of the Software, and to permit persons to whom the Software is
9    * furnished to do so, subject to the following conditions:
10   *
11   * The above copyright notice and this permission notice shall be included in
12   * all copies or substantial portions of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.security.auth;
24  
25  import com.liferay.portal.NoSuchUserException;
26  import com.liferay.portal.PasswordExpiredException;
27  import com.liferay.portal.UserLockoutException;
28  import com.liferay.portal.kernel.log.Log;
29  import com.liferay.portal.kernel.log.LogFactoryUtil;
30  import com.liferay.portal.kernel.util.StringPool;
31  import com.liferay.portal.kernel.util.Validator;
32  import com.liferay.portal.model.User;
33  import com.liferay.portal.security.ldap.PortalLDAPUtil;
34  import com.liferay.portal.security.pwd.PwdEncryptor;
35  import com.liferay.portal.service.UserLocalServiceUtil;
36  import com.liferay.portal.util.PrefsPropsUtil;
37  import com.liferay.portal.util.PropsKeys;
38  import com.liferay.portal.util.PropsValues;
39  import com.liferay.portlet.admin.util.OmniadminUtil;
40  
41  import java.util.Hashtable;
42  import java.util.Map;
43  
44  import javax.naming.Context;
45  import javax.naming.NamingEnumeration;
46  import javax.naming.directory.Attribute;
47  import javax.naming.directory.Attributes;
48  import javax.naming.directory.SearchControls;
49  import javax.naming.directory.SearchResult;
50  import javax.naming.ldap.Control;
51  import javax.naming.ldap.InitialLdapContext;
52  import javax.naming.ldap.LdapContext;
53  
54  /**
55   * <a href="LDAPAuth.java.html"><b><i>View Source</i></b></a>
56   *
57   * @author Brian Wing Shun Chan
58   * @author Scott Lee
59   *
60   */
61  public class LDAPAuth implements Authenticator {
62  
63      public static final String AUTH_METHOD_BIND = "bind";
64  
65      public static final String AUTH_METHOD_PASSWORD_COMPARE =
66          "password-compare";
67  
68      public static final String RESULT_PASSWORD_RESET =
69          "2.16.840.1.113730.3.4.4";
70  
71      public static final String RESULT_PASSWORD_EXP_WARNING =
72          "2.16.840.1.113730.3.4.5";
73  
74      public int authenticateByEmailAddress(
75              long companyId, String emailAddress, String password,
76              Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
77          throws AuthException {
78  
79          try {
80              return authenticate(
81                  companyId, emailAddress, StringPool.BLANK, 0, password);
82          }
83          catch (Exception e) {
84              _log.error(e, e);
85  
86              throw new AuthException(e);
87          }
88      }
89  
90      public int authenticateByScreenName(
91              long companyId, String screenName, String password,
92              Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
93          throws AuthException {
94  
95          try {
96              return authenticate(
97                  companyId, StringPool.BLANK, screenName, 0, password);
98          }
99          catch (Exception e) {
100             _log.error(e, e);
101 
102             throw new AuthException(e);
103         }
104     }
105 
106     public int authenticateByUserId(
107             long companyId, long userId, String password,
108             Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
109         throws AuthException {
110 
111         try {
112             return authenticate(
113                 companyId, StringPool.BLANK, StringPool.BLANK, userId,
114                 password);
115         }
116         catch (Exception e) {
117             _log.error(e, e);
118 
119             throw new AuthException(e);
120         }
121     }
122 
123     protected int authenticate(
124             long companyId, String emailAddress, String screenName, long userId,
125             String password)
126         throws Exception {
127 
128         if (!PortalLDAPUtil.isAuthEnabled(companyId)) {
129             if (_log.isDebugEnabled()) {
130                 _log.debug("Authenticator is not enabled");
131             }
132 
133             return SUCCESS;
134         }
135 
136         if (_log.isDebugEnabled()) {
137             _log.debug("Authenticator is enabled");
138         }
139 
140         // Make exceptions for omniadmins so that if they break the LDAP
141         // configuration, they can still login to fix the problem
142 
143         if (authenticateOmniadmin(companyId, emailAddress, userId) == SUCCESS) {
144             return SUCCESS;
145         }
146 
147         String baseDN = PrefsPropsUtil.getString(
148             companyId, PropsKeys.LDAP_BASE_DN);
149 
150         LdapContext ctx = PortalLDAPUtil.getContext(companyId);
151 
152         if (ctx == null) {
153             return authenticateRequired(
154                 companyId, userId, emailAddress, FAILURE);
155         }
156 
157         //  Process LDAP auth search filter
158 
159         String filter = PortalLDAPUtil.getAuthSearchFilter(
160             companyId, emailAddress, screenName, String.valueOf(userId));
161 
162         try {
163             SearchControls cons = new SearchControls(
164                 SearchControls.SUBTREE_SCOPE, 1, 0, null, false, false);
165 
166             NamingEnumeration<SearchResult> enu = ctx.search(
167                 baseDN, filter, cons);
168 
169             if (enu.hasMoreElements()) {
170                 if (_log.isDebugEnabled()) {
171                     _log.debug("Search filter returned at least one result");
172                 }
173 
174                 SearchResult result = enu.nextElement();
175 
176                 String fullUserDN = PortalLDAPUtil.getNameInNamespace(
177                     companyId, result);
178 
179                 Attributes attrs = PortalLDAPUtil.getUserAttributes(
180                     companyId, ctx, fullUserDN);
181 
182                 LDAPAuthResult ldapAuthResult = authenticate(
183                     ctx, companyId, attrs, fullUserDN, password);
184 
185                 // Process LDAP failure codes
186 
187                 String errorMessage = ldapAuthResult.getErrorMessage();
188 
189                 if (errorMessage != null) {
190                     if (errorMessage.indexOf(PrefsPropsUtil.getString(
191                             companyId, PropsKeys.LDAP_ERROR_USER_LOCKOUT))
192                                 != -1) {
193 
194                         throw new UserLockoutException();
195                     }
196                     else if (errorMessage.indexOf(PrefsPropsUtil.getString(
197                         companyId, PropsKeys.LDAP_ERROR_PASSWORD_EXPIRED))
198                             != -1) {
199 
200                         throw new PasswordExpiredException();
201                     }
202                 }
203 
204                 if (!ldapAuthResult.isAuthenticated()) {
205                     return authenticateRequired(
206                         companyId, userId, emailAddress, FAILURE);
207                 }
208 
209                 // Get user or create from LDAP
210 
211                 User user = PortalLDAPUtil.importLDAPUser(
212                     companyId, ctx, attrs, password, true);
213 
214                 // Process LDAP success codes
215 
216                 String resultCode = ldapAuthResult.getResponseControl();
217 
218                 if (resultCode.equals(LDAPAuth.RESULT_PASSWORD_RESET)) {
219                     UserLocalServiceUtil.updatePasswordReset(
220                         user.getUserId(), true);
221                 }
222                 else if (
223                     resultCode.equals(LDAPAuth.RESULT_PASSWORD_EXP_WARNING)) {
224 
225                     UserLocalServiceUtil.updatePasswordReset(
226                         user.getUserId(), true);
227                 }
228             }
229             else {
230                 if (_log.isDebugEnabled()) {
231                     _log.debug("Search filter did not return any results");
232                 }
233 
234                 return authenticateRequired(
235                     companyId, userId, emailAddress, DNE);
236             }
237 
238             enu.close();
239         }
240         catch (Exception e) {
241             _log.error("Problem accessing LDAP server: " + e.getMessage());
242 
243             if (authenticateRequired(
244                     companyId, userId, emailAddress, FAILURE) == FAILURE) {
245 
246                 throw e;
247             }
248         }
249         finally {
250             if (ctx != null) {
251                 ctx.close();
252             }
253         }
254 
255         return SUCCESS;
256     }
257 
258     protected LDAPAuthResult authenticate(
259             LdapContext ctx, long companyId, Attributes attrs, String userDN,
260             String password)
261         throws Exception {
262 
263         LDAPAuthResult ldapAuthResult = new LDAPAuthResult();
264 
265         // Check passwords by either doing a comparison between the passwords or
266         // by binding to the LDAP server. If using LDAP password policies, bind
267         // auth method must be used in order to get the result control codes.
268 
269         String authMethod = PrefsPropsUtil.getString(
270             companyId, PropsKeys.LDAP_AUTH_METHOD);
271         InitialLdapContext innerCtx = null;
272 
273         if (authMethod.equals(AUTH_METHOD_BIND)) {
274             try {
275                 Hashtable<String, Object> env =
276                     (Hashtable<String, Object>)ctx.getEnvironment();
277 
278                 env.put(Context.SECURITY_PRINCIPAL, userDN);
279                 env.put(Context.SECURITY_CREDENTIALS, password);
280                 env.put(
281                     Context.REFERRAL,
282                     PrefsPropsUtil.getString(
283                         companyId, PropsKeys.LDAP_REFERRAL));
284 
285                 // Do not use pooling because principal changes
286 
287                 env.put("com.sun.jndi.ldap.connect.pool", "false");
288 
289                 innerCtx = new InitialLdapContext(env, null);
290 
291                 // Get LDAP bind results
292 
293                 Control[] responseControls =  innerCtx.getResponseControls();
294 
295                 ldapAuthResult.setAuthenticated(true);
296                 ldapAuthResult.setResponseControl(responseControls);
297             }
298             catch (Exception e) {
299                 if (_log.isDebugEnabled()) {
300                     _log.debug(
301                         "Failed to bind to the LDAP server with userDN "
302                             + userDN + " and password " + password);
303                 }
304 
305                 _log.error(
306                     "Failed to bind to the LDAP server: " + e.getMessage());
307 
308                 ldapAuthResult.setAuthenticated(false);
309                 ldapAuthResult.setErrorMessage(e.getMessage());
310             }
311             finally {
312                 if (innerCtx != null) {
313                     innerCtx.close();
314                 }
315             }
316         }
317         else if (authMethod.equals(AUTH_METHOD_PASSWORD_COMPARE)) {
318             Attribute userPassword = attrs.get("userPassword");
319 
320             if (userPassword != null) {
321                 String ldapPassword = new String((byte[])userPassword.get());
322 
323                 String encryptedPassword = password;
324 
325                 String algorithm = PrefsPropsUtil.getString(
326                     companyId,
327                     PropsKeys.LDAP_AUTH_PASSWORD_ENCRYPTION_ALGORITHM);
328 
329                 if (Validator.isNotNull(algorithm)) {
330                     encryptedPassword =
331                         "{" + algorithm + "}" +
332                             PwdEncryptor.encrypt(
333                                 algorithm, password, ldapPassword);
334                 }
335 
336                 if (ldapPassword.equals(encryptedPassword)) {
337                     ldapAuthResult.setAuthenticated(true);
338                 }
339                 else {
340                     ldapAuthResult.setAuthenticated(false);
341 
342                     if (_log.isWarnEnabled()) {
343                         _log.warn(
344                             "Passwords do not match for userDN " + userDN);
345                     }
346                 }
347             }
348         }
349 
350         return ldapAuthResult;
351     }
352 
353     protected int authenticateOmniadmin(
354             long companyId, String emailAddress, long userId)
355         throws Exception {
356 
357         // Only allow omniadmin if Liferay password checking is enabled
358 
359         if (PropsValues.AUTH_PIPELINE_ENABLE_LIFERAY_CHECK) {
360             if (userId > 0) {
361                 if (OmniadminUtil.isOmniadmin(userId)) {
362                     return SUCCESS;
363                 }
364             }
365             else if (Validator.isNotNull(emailAddress)) {
366                 try {
367                     User user = UserLocalServiceUtil.getUserByEmailAddress(
368                         companyId, emailAddress);
369 
370                     if (OmniadminUtil.isOmniadmin(user.getUserId())) {
371                         return SUCCESS;
372                     }
373                 }
374                 catch (NoSuchUserException nsue) {
375                 }
376             }
377         }
378 
379         return FAILURE;
380     }
381 
382     protected int authenticateRequired(
383             long companyId, long userId, String emailAddress, int failureCode)
384         throws Exception {
385 
386         if (PrefsPropsUtil.getBoolean(
387                 companyId, PropsKeys.LDAP_AUTH_REQUIRED)) {
388 
389             return failureCode;
390         }
391         else {
392             return SUCCESS;
393         }
394     }
395 
396     private static Log _log = LogFactoryUtil.getLog(LDAPAuth.class);
397 
398 }