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.PropsUtil;
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, PropsUtil.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.hasMore()) {
170                 if (_log.isDebugEnabled()) {
171                     _log.debug("Search filter returned at least one result");
172                 }
173 
174                 SearchResult result = enu.next();
175 
176                 String fullUserDN = PortalLDAPUtil.getNameInNamespace(
177                     companyId, result);
178 
179                 Attributes attrs = PortalLDAPUtil.getAttributes(
180                     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, PropsUtil.LDAP_ERROR_USER_LOCKOUT))
192                                 != -1) {
193 
194                         throw new UserLockoutException();
195                     }
196                     else if (errorMessage.indexOf(PrefsPropsUtil.getString(
197                         companyId, PropsUtil.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         catch (Exception e) {
239             _log.error("Problem accessing LDAP server: " + e.getMessage());
240 
241             if (authenticateRequired(
242                     companyId, userId, emailAddress, FAILURE) == FAILURE) {
243 
244                 throw e;
245             }
246         }
247         finally {
248             ctx.close();
249         }
250 
251         return SUCCESS;
252     }
253 
254     protected LDAPAuthResult authenticate(
255             LdapContext ctx, long companyId, Attributes attrs, String userDN,
256             String password)
257         throws Exception {
258 
259         LDAPAuthResult ldapAuthResult = new LDAPAuthResult();
260 
261         // Check passwords by either doing a comparison between the passwords or
262         // by binding to the LDAP server. If using LDAP password policies, bind
263         // auth method must be used in order to get the result control codes.
264 
265         String authMethod = PrefsPropsUtil.getString(
266             companyId, PropsUtil.LDAP_AUTH_METHOD);
267 
268         if (authMethod.equals(AUTH_METHOD_BIND)) {
269             try {
270                 Hashtable<String, String> env =
271                     (Hashtable<String, String>)ctx.getEnvironment();
272 
273                 env.put(Context.SECURITY_PRINCIPAL, userDN);
274                 env.put(Context.SECURITY_CREDENTIALS, password);
275                 env.put(Context.REFERRAL, "follow");
276 
277                 ctx = new InitialLdapContext(env, null);
278 
279                 // Get LDAP bind results
280 
281                 Control[] responseControls =  ctx.getResponseControls();
282 
283                 ldapAuthResult.setAuthenticated(true);
284                 ldapAuthResult.setResponseControl(responseControls);
285             }
286             catch (Exception e) {
287                 _log.error(
288                     "Failed to bind to the LDAP server with userDN " + userDN +
289                         " and password " + password + ": " + e.getMessage());
290 
291                 ldapAuthResult.setAuthenticated(false);
292                 ldapAuthResult.setErrorMessage(e.getMessage());
293             }
294         }
295         else if (authMethod.equals(AUTH_METHOD_PASSWORD_COMPARE)) {
296             Attribute userPassword = attrs.get("userPassword");
297 
298             if (userPassword != null) {
299                 String ldapPassword = new String((byte[])userPassword.get());
300 
301                 String encryptedPassword = password;
302 
303                 String algorithm = PrefsPropsUtil.getString(
304                     companyId,
305                     PropsUtil.LDAP_AUTH_PASSWORD_ENCRYPTION_ALGORITHM);
306 
307                 if (Validator.isNotNull(algorithm)) {
308                     encryptedPassword =
309                         "{" + algorithm + "}" +
310                             PwdEncryptor.encrypt(
311                                 algorithm, password, ldapPassword);
312                 }
313 
314                 if (ldapPassword.equals(encryptedPassword)) {
315                     ldapAuthResult.setAuthenticated(true);
316                 }
317                 else {
318                     ldapAuthResult.setAuthenticated(false);
319 
320                     if (_log.isWarnEnabled()) {
321                         _log.warn(
322                             "Passwords do not match for userDN " + userDN);
323                     }
324                 }
325             }
326         }
327 
328         return ldapAuthResult;
329     }
330 
331     protected int authenticateOmniadmin(
332             long companyId, String emailAddress, long userId)
333         throws Exception {
334 
335         // Only allow omniadmin if Liferay password checking is enabled
336 
337         if (PropsValues.AUTH_PIPELINE_ENABLE_LIFERAY_CHECK) {
338             if (userId > 0) {
339                 if (OmniadminUtil.isOmniadmin(userId)) {
340                     return SUCCESS;
341                 }
342             }
343             else if (Validator.isNotNull(emailAddress)) {
344                 try {
345                     User user = UserLocalServiceUtil.getUserByEmailAddress(
346                         companyId, emailAddress);
347 
348                     if (OmniadminUtil.isOmniadmin(user.getUserId())) {
349                         return SUCCESS;
350                     }
351                 }
352                 catch (NoSuchUserException nsue) {
353                 }
354             }
355         }
356 
357         return FAILURE;
358     }
359 
360     protected int authenticateRequired(
361             long companyId, long userId, String emailAddress, int failureCode)
362         throws Exception {
363 
364         if (PrefsPropsUtil.getBoolean(
365                 companyId, PropsUtil.LDAP_AUTH_REQUIRED)) {
366 
367             return failureCode;
368         }
369         else {
370             return SUCCESS;
371         }
372     }
373 
374     private static Log _log = LogFactoryUtil.getLog(LDAPAuth.class);
375 
376 }