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