001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.security.auth;
016    
017    import com.liferay.portal.NoSuchUserException;
018    import com.liferay.portal.PasswordExpiredException;
019    import com.liferay.portal.UserLockoutException;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.util.AutoResetThreadLocal;
023    import com.liferay.portal.kernel.util.GetterUtil;
024    import com.liferay.portal.kernel.util.MapUtil;
025    import com.liferay.portal.kernel.util.PropsKeys;
026    import com.liferay.portal.kernel.util.StringBundler;
027    import com.liferay.portal.kernel.util.StringPool;
028    import com.liferay.portal.kernel.util.StringUtil;
029    import com.liferay.portal.kernel.util.Validator;
030    import com.liferay.portal.model.User;
031    import com.liferay.portal.security.ldap.LDAPSettingsUtil;
032    import com.liferay.portal.security.ldap.PortalLDAPImporterUtil;
033    import com.liferay.portal.security.ldap.PortalLDAPUtil;
034    import com.liferay.portal.security.pwd.PasswordEncryptorUtil;
035    import com.liferay.portal.service.UserLocalServiceUtil;
036    import com.liferay.portal.util.PrefsPropsUtil;
037    import com.liferay.portal.util.PropsValues;
038    import com.liferay.portlet.admin.util.OmniadminUtil;
039    
040    import java.util.HashMap;
041    import java.util.Hashtable;
042    import java.util.Map;
043    import java.util.Properties;
044    
045    import javax.naming.Context;
046    import javax.naming.NamingEnumeration;
047    import javax.naming.directory.Attribute;
048    import javax.naming.directory.Attributes;
049    import javax.naming.directory.SearchControls;
050    import javax.naming.directory.SearchResult;
051    import javax.naming.ldap.Control;
052    import javax.naming.ldap.InitialLdapContext;
053    import javax.naming.ldap.LdapContext;
054    
055    /**
056     * @author Brian Wing Shun Chan
057     * @author Scott Lee
058     * @author Josef Sustacek
059     */
060    public class LDAPAuth implements Authenticator {
061    
062            public static final String AUTH_METHOD_BIND = "bind";
063    
064            public static final String AUTH_METHOD_PASSWORD_COMPARE =
065                    "password-compare";
066    
067            public static final String RESULT_PASSWORD_EXP_WARNING =
068                    "2.16.840.1.113730.3.4.5";
069    
070            public static final String RESULT_PASSWORD_RESET =
071                    "2.16.840.1.113730.3.4.4";
072    
073            @Override
074            public int authenticateByEmailAddress(
075                            long companyId, String emailAddress, String password,
076                            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
077                    throws AuthException {
078    
079                    try {
080                            return authenticate(
081                                    companyId, emailAddress, StringPool.BLANK, 0, password);
082                    }
083                    catch (Exception e) {
084                            _log.error(e, e);
085    
086                            throw new AuthException(e);
087                    }
088            }
089    
090            @Override
091            public int authenticateByScreenName(
092                            long companyId, String screenName, String password,
093                            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
094                    throws AuthException {
095    
096                    try {
097                            return authenticate(
098                                    companyId, StringPool.BLANK, screenName, 0, password);
099                    }
100                    catch (Exception e) {
101                            _log.error(e, e);
102    
103                            throw new AuthException(e);
104                    }
105            }
106    
107            @Override
108            public int authenticateByUserId(
109                            long companyId, long userId, String password,
110                            Map<String, String[]> headerMap, Map<String, String[]> parameterMap)
111                    throws AuthException {
112    
113                    try {
114                            return authenticate(
115                                    companyId, StringPool.BLANK, StringPool.BLANK, userId,
116                                    password);
117                    }
118                    catch (Exception e) {
119                            _log.error(e, e);
120    
121                            throw new AuthException(e);
122                    }
123            }
124    
125            protected LDAPAuthResult authenticate(
126                            LdapContext ctx, long companyId, Attributes attributes,
127                            String userDN, String password)
128                    throws Exception {
129    
130                    LDAPAuthResult ldapAuthResult = null;
131    
132                    // Check passwords by either doing a comparison between the passwords or
133                    // by binding to the LDAP server. If using LDAP password policies, bind
134                    // auth method must be used in order to get the result control codes.
135    
136                    String authMethod = PrefsPropsUtil.getString(
137                            companyId, PropsKeys.LDAP_AUTH_METHOD);
138    
139                    if (authMethod.equals(AUTH_METHOD_BIND)) {
140                            Hashtable<String, Object> env =
141                                    (Hashtable<String, Object>)ctx.getEnvironment();
142    
143                            env.put(Context.SECURITY_PRINCIPAL, userDN);
144                            env.put(Context.SECURITY_CREDENTIALS, password);
145                            env.put(
146                                    Context.REFERRAL,
147                                    PrefsPropsUtil.getString(companyId, PropsKeys.LDAP_REFERRAL));
148    
149                            // Do not use pooling because principal changes
150    
151                            env.put("com.sun.jndi.ldap.connect.pool", "false");
152    
153                            ldapAuthResult = getFailedLDAPAuthResult(env);
154    
155                            if (ldapAuthResult != null) {
156                                    return ldapAuthResult;
157                            }
158    
159                            ldapAuthResult = new LDAPAuthResult();
160    
161                            InitialLdapContext initialLdapContext = null;
162    
163                            try {
164                                    initialLdapContext = new InitialLdapContext(env, null);
165    
166                                    // Get LDAP bind results
167    
168                                    Control[] responseControls =
169                                            initialLdapContext.getResponseControls();
170    
171                                    ldapAuthResult.setAuthenticated(true);
172                                    ldapAuthResult.setResponseControl(responseControls);
173                            }
174                            catch (Exception e) {
175                                    if (_log.isDebugEnabled()) {
176                                            _log.debug(
177                                                    "Failed to bind to the LDAP server with userDN " +
178                                                            userDN + " and password " + password,
179                                                    e);
180                                    }
181    
182                                    ldapAuthResult.setAuthenticated(false);
183                                    ldapAuthResult.setErrorMessage(e.getMessage());
184    
185                                    setFailedLDAPAuthResult(env, ldapAuthResult);
186                            }
187                            finally {
188                                    if (initialLdapContext != null) {
189                                            initialLdapContext.close();
190                                    }
191                            }
192                    }
193                    else if (authMethod.equals(AUTH_METHOD_PASSWORD_COMPARE)) {
194                            ldapAuthResult = new LDAPAuthResult();
195    
196                            Attribute userPassword = attributes.get("userPassword");
197    
198                            if (userPassword != null) {
199                                    String ldapPassword = new String((byte[])userPassword.get());
200    
201                                    String encryptedPassword = password;
202    
203                                    String algorithm = PrefsPropsUtil.getString(
204                                            companyId,
205                                            PropsKeys.LDAP_AUTH_PASSWORD_ENCRYPTION_ALGORITHM);
206    
207                                    if (Validator.isNotNull(algorithm)) {
208                                            encryptedPassword = PasswordEncryptorUtil.encrypt(
209                                                    algorithm, password, ldapPassword);
210                                    }
211    
212                                    if (ldapPassword.equals(encryptedPassword)) {
213                                            ldapAuthResult.setAuthenticated(true);
214                                    }
215                                    else {
216                                            ldapAuthResult.setAuthenticated(false);
217    
218                                            if (_log.isDebugEnabled()) {
219                                                    _log.debug(
220                                                            "Passwords do not match for userDN " + userDN);
221                                            }
222                                    }
223                            }
224                    }
225    
226                    return ldapAuthResult;
227            }
228    
229            protected int authenticate(
230                            long ldapServerId, long companyId, String emailAddress,
231                            String screenName, long userId, String password)
232                    throws Exception {
233    
234                    String postfix = LDAPSettingsUtil.getPropertyPostfix(ldapServerId);
235    
236                    LdapContext ldapContext = PortalLDAPUtil.getContext(
237                            ldapServerId, companyId);
238    
239                    if (ldapContext == null) {
240                            return FAILURE;
241                    }
242    
243                    NamingEnumeration<SearchResult> enu = null;
244    
245                    try {
246                            String baseDN = PrefsPropsUtil.getString(
247                                    companyId, PropsKeys.LDAP_BASE_DN + postfix);
248    
249                            //  Process LDAP auth search filter
250    
251                            String filter = LDAPSettingsUtil.getAuthSearchFilter(
252                                    ldapServerId, companyId, emailAddress, screenName,
253                                    String.valueOf(userId));
254    
255                            Properties userMappings = LDAPSettingsUtil.getUserMappings(
256                                    ldapServerId, companyId);
257    
258                            String userMappingsScreenName = GetterUtil.getString(
259                                    userMappings.getProperty("screenName"));
260    
261                            userMappingsScreenName = StringUtil.toLowerCase(
262                                    userMappingsScreenName);
263    
264                            SearchControls searchControls = new SearchControls(
265                                    SearchControls.SUBTREE_SCOPE, 1, 0,
266                                    new String[] {userMappingsScreenName}, false, false);
267    
268                            enu = ldapContext.search(baseDN, filter, searchControls);
269    
270                            if (enu.hasMoreElements()) {
271                                    if (_log.isDebugEnabled()) {
272                                            _log.debug("Search filter returned at least one result");
273                                    }
274    
275                                    SearchResult result = enu.nextElement();
276    
277                                    String fullUserDN = PortalLDAPUtil.getNameInNamespace(
278                                            ldapServerId, companyId, result);
279    
280                                    Attributes attributes = PortalLDAPUtil.getUserAttributes(
281                                            ldapServerId, companyId, ldapContext, fullUserDN);
282    
283                                    LDAPAuthResult ldapAuthResult = authenticate(
284                                            ldapContext, companyId, attributes, fullUserDN, password);
285    
286                                    // Process LDAP failure codes
287    
288                                    String errorMessage = ldapAuthResult.getErrorMessage();
289    
290                                    if (errorMessage != null) {
291                                            int pos = errorMessage.indexOf(
292                                                    PrefsPropsUtil.getString(
293                                                            companyId, PropsKeys.LDAP_ERROR_USER_LOCKOUT));
294    
295                                            if (pos != -1) {
296                                                    throw new UserLockoutException();
297                                            }
298    
299                                            pos = errorMessage.indexOf(
300                                                    PrefsPropsUtil.getString(
301                                                            companyId, PropsKeys.LDAP_ERROR_PASSWORD_EXPIRED));
302    
303                                            if (pos != -1) {
304                                                    throw new PasswordExpiredException();
305                                            }
306                                    }
307    
308                                    if (!ldapAuthResult.isAuthenticated()) {
309                                            return FAILURE;
310                                    }
311    
312                                    // Get user or create from LDAP
313    
314                                    User user = PortalLDAPImporterUtil.importLDAPUser(
315                                            ldapServerId, companyId, ldapContext, attributes, password);
316    
317                                    // Process LDAP success codes
318    
319                                    String resultCode = ldapAuthResult.getResponseControl();
320    
321                                    if (resultCode.equals(LDAPAuth.RESULT_PASSWORD_RESET)) {
322                                            UserLocalServiceUtil.updatePasswordReset(
323                                                    user.getUserId(), true);
324                                    }
325                            }
326                            else {
327                                    if (_log.isDebugEnabled()) {
328                                            _log.debug("Search filter did not return any results");
329                                    }
330    
331                                    return DNE;
332                            }
333                    }
334                    catch (Exception e) {
335                            if (e instanceof PasswordExpiredException ||
336                                    e instanceof UserLockoutException) {
337    
338                                    throw e;
339                            }
340    
341                            _log.error("Problem accessing LDAP server", e);
342    
343                            return FAILURE;
344                    }
345                    finally {
346                            if (enu != null) {
347                                    enu.close();
348                            }
349    
350                            if (ldapContext != null) {
351                                    ldapContext.close();
352                            }
353                    }
354    
355                    return SUCCESS;
356            }
357    
358            protected int authenticate(
359                            long companyId, String emailAddress, String screenName, long userId,
360                            String password)
361                    throws Exception {
362    
363                    if (!AuthSettingsUtil.isLDAPAuthEnabled(companyId)) {
364                            if (_log.isDebugEnabled()) {
365                                    _log.debug("Authenticator is not enabled");
366                            }
367    
368                            return SUCCESS;
369                    }
370    
371                    if (_log.isDebugEnabled()) {
372                            _log.debug("Authenticator is enabled");
373                    }
374    
375                    int preferredLDAPServerResult = authenticateAgainstPreferredLDAPServer(
376                            companyId, emailAddress, screenName, userId, password);
377    
378                    if (preferredLDAPServerResult == SUCCESS) {
379                            if (PrefsPropsUtil.getBoolean(
380                                            companyId, PropsKeys.LDAP_IMPORT_USER_PASSWORD_ENABLED)) {
381    
382                                    return preferredLDAPServerResult;
383                            }
384    
385                            return Authenticator.SKIP_LIFERAY_CHECK;
386                    }
387    
388                    long[] ldapServerIds = StringUtil.split(
389                            PrefsPropsUtil.getString(companyId, "ldap.server.ids"), 0L);
390    
391                    for (long ldapServerId : ldapServerIds) {
392                            int result = authenticate(
393                                    ldapServerId, companyId, emailAddress, screenName, userId,
394                                    password);
395    
396                            if (result == SUCCESS) {
397                                    if (PrefsPropsUtil.getBoolean(
398                                                    companyId,
399                                                    PropsKeys.LDAP_IMPORT_USER_PASSWORD_ENABLED)) {
400    
401                                            return result;
402                                    }
403    
404                                    return Authenticator.SKIP_LIFERAY_CHECK;
405                            }
406                    }
407    
408                    for (int ldapServerId = 0;; ldapServerId++) {
409                            String postfix = LDAPSettingsUtil.getPropertyPostfix(ldapServerId);
410    
411                            String providerUrl = PrefsPropsUtil.getString(
412                                    companyId, PropsKeys.LDAP_BASE_PROVIDER_URL + postfix);
413    
414                            if (Validator.isNull(providerUrl)) {
415                                    break;
416                            }
417    
418                            int result = authenticate(
419                                    ldapServerId, companyId, emailAddress, screenName, userId,
420                                    password);
421    
422                            if (result == SUCCESS) {
423                                    if (PrefsPropsUtil.getBoolean(
424                                                    companyId,
425                                                    PropsKeys.LDAP_IMPORT_USER_PASSWORD_ENABLED)) {
426    
427                                            return result;
428                                    }
429    
430                                    return Authenticator.SKIP_LIFERAY_CHECK;
431                            }
432                    }
433    
434                    return authenticateRequired(
435                            companyId, userId, emailAddress, screenName, true, FAILURE);
436            }
437    
438            protected int authenticateAgainstPreferredLDAPServer(
439                            long companyId, String emailAddress, String screenName, long userId,
440                            String password)
441                    throws Exception {
442    
443                    int result = DNE;
444    
445                    User user = null;
446    
447                    try {
448                            if (userId > 0) {
449                                    user = UserLocalServiceUtil.getUserById(companyId, userId);
450                            }
451                            else if (Validator.isNotNull(emailAddress)) {
452                                    user = UserLocalServiceUtil.getUserByEmailAddress(
453                                            companyId, emailAddress);
454                            }
455                            else if (Validator.isNotNull(screenName)) {
456                                    user = UserLocalServiceUtil.getUserByScreenName(
457                                            companyId, screenName);
458                            }
459                            else {
460                                    if (_log.isDebugEnabled()) {
461                                            _log.debug("Unable to get preferred LDAP server");
462                                    }
463    
464                                    return result;
465                            }
466                    }
467                    catch (NoSuchUserException nsue) {
468                            if (_log.isDebugEnabled()) {
469                                    _log.debug("Unable to get preferred LDAP server", nsue);
470                            }
471    
472                            return result;
473                    }
474    
475                    long ldapServerId = user.getLdapServerId();
476    
477                    if (ldapServerId < 0) {
478                            return result;
479                    }
480    
481                    String postfix = LDAPSettingsUtil.getPropertyPostfix(ldapServerId);
482    
483                    String providerUrl = PrefsPropsUtil.getString(
484                            user.getCompanyId(), PropsKeys.LDAP_BASE_PROVIDER_URL + postfix);
485    
486                    if (Validator.isNull(providerUrl)) {
487                            return result;
488                    }
489    
490                    if (_log.isDebugEnabled()) {
491                            _log.debug(
492                                    "Using LDAP server ID " + ldapServerId +
493                                            " to authenticate user " + user.getUserId());
494                    }
495    
496                    result = authenticate(
497                            ldapServerId, companyId, emailAddress, screenName, userId,
498                            password);
499    
500                    return result;
501            }
502    
503            protected int authenticateOmniadmin(
504                            long companyId, String emailAddress, String screenName, long userId)
505                    throws Exception {
506    
507                    // Only allow omniadmin if Liferay password checking is enabled
508    
509                    if (!PropsValues.AUTH_PIPELINE_ENABLE_LIFERAY_CHECK) {
510                            return FAILURE;
511                    }
512    
513                    if (userId > 0) {
514                            if (OmniadminUtil.isOmniadmin(userId)) {
515                                    return SUCCESS;
516                            }
517                    }
518                    else if (Validator.isNotNull(emailAddress)) {
519                            User user = UserLocalServiceUtil.fetchUserByEmailAddress(
520                                    companyId, emailAddress);
521    
522                            if (user != null) {
523                                    if (OmniadminUtil.isOmniadmin(user)) {
524                                            return SUCCESS;
525                                    }
526                            }
527                    }
528                    else if (Validator.isNotNull(screenName)) {
529                            User user = UserLocalServiceUtil.fetchUserByScreenName(
530                                    companyId, screenName);
531    
532                            if (user != null) {
533                                    if (OmniadminUtil.isOmniadmin(user)) {
534                                            return SUCCESS;
535                                    }
536                            }
537                    }
538    
539                    return FAILURE;
540            }
541    
542            protected int authenticateRequired(
543                            long companyId, long userId, String emailAddress, String screenName,
544                            boolean allowOmniadmin, int failureCode)
545                    throws Exception {
546    
547                    // Make exceptions for omniadmins so that if they break the LDAP
548                    // configuration, they can still login to fix the problem
549    
550                    if (allowOmniadmin &&
551                            (authenticateOmniadmin(
552                                    companyId, emailAddress, screenName, userId) == SUCCESS)) {
553    
554                            return SUCCESS;
555                    }
556    
557                    if (PrefsPropsUtil.getBoolean(
558                                    companyId, PropsKeys.LDAP_AUTH_REQUIRED)) {
559    
560                            return failureCode;
561                    }
562                    else {
563                            return SUCCESS;
564                    }
565            }
566    
567            protected LDAPAuthResult getFailedLDAPAuthResult(Map<String, Object> env) {
568                    Map<String, LDAPAuthResult> failedLDAPAuthResults =
569                            _failedLDAPAuthResults.get();
570    
571                    String cacheKey = getKey(env);
572    
573                    return failedLDAPAuthResults.get(cacheKey);
574            }
575    
576            protected String getKey(Map<String, Object> env) {
577                    StringBundler sb = new StringBundler(5);
578    
579                    sb.append(MapUtil.getString(env, Context.PROVIDER_URL));
580                    sb.append(StringPool.POUND);
581                    sb.append(MapUtil.getString(env, Context.SECURITY_PRINCIPAL));
582                    sb.append(StringPool.POUND);
583                    sb.append(MapUtil.getString(env, Context.SECURITY_CREDENTIALS));
584    
585                    return sb.toString();
586            }
587    
588            protected void setFailedLDAPAuthResult(
589                    Map<String, Object> env, LDAPAuthResult ldapAuthResult) {
590    
591                    Map<String, LDAPAuthResult> failedLDAPAuthResults =
592                            _failedLDAPAuthResults.get();
593    
594                    String cacheKey = getKey(env);
595    
596                    if (failedLDAPAuthResults.containsKey(cacheKey)) {
597                            return;
598                    }
599    
600                    failedLDAPAuthResults.put(cacheKey, ldapAuthResult);
601            }
602    
603            private static Log _log = LogFactoryUtil.getLog(LDAPAuth.class);
604    
605            private ThreadLocal<Map<String, LDAPAuthResult>>
606                    _failedLDAPAuthResults =
607                            new AutoResetThreadLocal<Map<String, LDAPAuthResult>>(
608                                    LDAPAuth.class + "._failedLDAPAuthResultCache",
609                                    new HashMap<String, LDAPAuthResult>());
610    
611    }