/* * Password Management Servlets (PWM) * http://www.pwm-project.org * * Copyright (c) 2006-2009 Novell, Inc. * Copyright (c) 2009-2017 The PWM Project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package password.pwm.ldap.auth; import com.novell.ldapchai.ChaiConstant; import com.novell.ldapchai.ChaiFactory; import com.novell.ldapchai.ChaiUser; import com.novell.ldapchai.exception.ChaiError; import com.novell.ldapchai.exception.ChaiException; import com.novell.ldapchai.exception.ChaiOperationException; import com.novell.ldapchai.exception.ChaiUnavailableException; import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException; import com.novell.ldapchai.impl.oracleds.entry.OracleDSEntries; import com.novell.ldapchai.provider.ChaiProvider; import com.novell.ldapchai.provider.ChaiSetting; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.SessionLabel; import password.pwm.bean.UserIdentity; import password.pwm.config.PwmSetting; import password.pwm.config.profile.LdapProfile; import password.pwm.config.profile.PwmPasswordPolicy; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.ldap.LdapOperationsHelper; import password.pwm.svc.event.AuditEvent; import password.pwm.svc.event.AuditRecord; import password.pwm.svc.event.AuditRecordFactory; import password.pwm.svc.intruder.IntruderManager; import password.pwm.svc.intruder.RecordType; import password.pwm.svc.stats.Statistic; import password.pwm.svc.stats.StatisticsManager; import password.pwm.util.PasswordData; import password.pwm.util.RandomPasswordGenerator; import password.pwm.util.java.JavaHelper; import password.pwm.util.java.TimeDuration; import password.pwm.util.logging.PwmLogLevel; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroMachine; import password.pwm.util.operations.PasswordUtility; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Set; class LDAPAuthenticationRequest implements AuthenticationRequest { private static final PwmLogger LOGGER = PwmLogger.forClass(LDAPAuthenticationRequest.class); private static final String ORACLE_ATTR_PW_ALLOW_CHG_TIME = "passwordAllowChangeTime"; private final PwmApplication pwmApplication; private final SessionLabel sessionLabel; private final UserIdentity userIdentity; private final AuthenticationType requestedAuthType; private final PwmAuthenticationSource authenticationSource; private ChaiProvider userProvider; private AuthenticationStrategy strategy = AuthenticationStrategy.BIND; private Date startTime; private static int counter = 0; private int operationNumber = 0; LDAPAuthenticationRequest( final PwmApplication pwmApplication, final SessionLabel sessionLabel, final UserIdentity userIdentity, final AuthenticationType requestedAuthType, final PwmAuthenticationSource authenticationSource ) { this.pwmApplication = pwmApplication; this.sessionLabel = sessionLabel; this.userIdentity = userIdentity; this.requestedAuthType = requestedAuthType; this.authenticationSource = authenticationSource; this.operationNumber = counter++; } static AuthenticationRequest createLDAPAuthenticationRequest( final PwmApplication pwmApplication, final SessionLabel sessionLabel, final UserIdentity userIdentity, final AuthenticationType requestedAuthType, final PwmAuthenticationSource authenticationSource ) { return new LDAPAuthenticationRequest(pwmApplication, sessionLabel, userIdentity, requestedAuthType, authenticationSource); } @Override public AuthenticationResult authUsingUnknownPw() throws ChaiUnavailableException, PwmUnrecoverableException { initialize(); log(PwmLogLevel.TRACE, "beginning authentication using unknown password procedure"); PasswordData userPassword = null; final boolean configAlwaysUseProxy = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_USE_PROXY_FOR_FORGOTTEN); if (configAlwaysUseProxy) { strategy = AuthenticationStrategy.ADMIN_PROXY; } else { userPassword = learnUserPassword(); if (userPassword != null) { strategy = AuthenticationStrategy.READ_THEN_BIND; } else { userPassword = setTempUserPassword(); if (userPassword != null) { strategy = AuthenticationStrategy.WRITE_THEN_BIND; } } if (userPassword == null) { throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"no available unknown-pw authentication method")); } } try { return authenticateUserImpl(userPassword); } catch (PwmOperationalException e) { if (strategy == AuthenticationStrategy.READ_THEN_BIND) { final String errorStr = "unable to authenticate with password read from directory, check proxy rights, ldap logs; error: " + e.getMessage(); throw new PwmUnrecoverableException( new ErrorInformation(PwmError.ERROR_BAD_SESSION_PASSWORD, errorStr)); } else if (strategy == AuthenticationStrategy.WRITE_THEN_BIND) { final String errorStr = "unable to authenticate with temporary password, check proxy rights, ldap logs; error: " + e.getMessage(); throw new PwmUnrecoverableException( new ErrorInformation(PwmError.ERROR_BAD_SESSION_PASSWORD, errorStr)); } throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"unable to authenticate via authWithUnknownPw method: " + e.getMessage())); } } @Override public AuthenticationResult authenticateUser(final PasswordData password) throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException { initialize(); return authenticateUserImpl(password); } private AuthenticationResult authenticateUserImpl( final PasswordData password ) throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException { if (startTime == null) { startTime = new Date(); } log(PwmLogLevel.DEBUG, "preparing to authenticate user using authenticationType=" + this.requestedAuthType + " using strategy " + this.strategy); final StatisticsManager statisticsManager = pwmApplication.getStatisticsManager(); final IntruderManager intruderManager = pwmApplication.getIntruderManager(); intruderManager.convenience().checkUserIdentity(userIdentity); intruderManager.check(RecordType.ADDRESS, sessionLabel.getSrcAddress()); boolean allowBindAsUser = true; if (strategy == AuthenticationStrategy.ADMIN_PROXY) { allowBindAsUser = false; } if (allowBindAsUser) { try { testCredentials(userIdentity, password); } catch (PwmOperationalException e) { boolean permitAuthDespiteError = false; final ChaiProvider.DIRECTORY_VENDOR vendor = pwmApplication.getProxyChaiProvider( userIdentity.getLdapProfileID()).getDirectoryVendor(); if (PwmError.PASSWORD_NEW_PASSWORD_REQUIRED == e.getError()) { if (vendor == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) { if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_REQUIRE_NEW_PWD)) { log(PwmLogLevel.INFO, "auth bind failed, but will allow login due to 'must change password on next login AD error', error: " + e.getErrorInformation().toDebugStr()); allowBindAsUser = false; permitAuthDespiteError = true; } } else if (vendor == ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS) { if (pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.ORACLE_DS_ALLOW_AUTH_REQUIRE_NEW_PWD)) { log(PwmLogLevel.INFO, "auth bind failed, but will allow login due to 'pwdReset' user attribute, error: " + e.getErrorInformation().toDebugStr()); allowBindAsUser = false; permitAuthDespiteError = true; } } } else if (PwmError.PASSWORD_EXPIRED == e.getError()) { // handle ad case where password is expired if (vendor == ChaiProvider.DIRECTORY_VENDOR.MICROSOFT_ACTIVE_DIRECTORY) { if (pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_REQUIRE_NEW_PWD)) { if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_ALLOW_AUTH_EXPIRED)) { throw e; } log(PwmLogLevel.INFO, "auth bind failed, but will allow login due to 'password expired AD error', error: " + e.getErrorInformation().toDebugStr()); allowBindAsUser = false; permitAuthDespiteError = true; } } } if (!permitAuthDespiteError) { // auth failed, presumably due to wrong password. statisticsManager.incrementValue(Statistic.AUTHENTICATION_FAILURES); throw e; } } } else { // verify user is not account disabled AuthenticationUtility.checkIfUserEligibleToAuthentication(pwmApplication, userIdentity); } statisticsManager.incrementValue(Statistic.AUTHENTICATIONS); statisticsManager.updateEps(Statistic.EpsType.AUTHENTICATION, 1); statisticsManager.updateAverageValue(Statistic.AVG_AUTHENTICATION_TIME, TimeDuration.fromCurrent(startTime).getTotalMilliseconds()); final AuthenticationType returnAuthType; if (!allowBindAsUser) { returnAuthType = AuthenticationType.AUTH_BIND_INHIBIT; } else { if (requestedAuthType == null) { returnAuthType = AuthenticationType.AUTHENTICATED; } else { if (requestedAuthType == AuthenticationType.AUTH_WITHOUT_PASSWORD) { returnAuthType = AuthenticationType.AUTHENTICATED; } else if (requestedAuthType == AuthenticationType.AUTH_FROM_PUBLIC_MODULE) { returnAuthType = AuthenticationType.AUTH_FROM_PUBLIC_MODULE; } else { returnAuthType = requestedAuthType; } } } final boolean useProxy = determineIfLdapProxyNeeded(returnAuthType, password); final ChaiProvider returnProvider = useProxy ? makeProxyProvider() : userProvider; final AuthenticationResult authenticationResult = new AuthenticationResult(returnProvider, returnAuthType, password); final StringBuilder debugMsg = new StringBuilder(); debugMsg.append("successful ldap authentication for ").append(userIdentity); debugMsg.append(" (").append(TimeDuration.fromCurrent(startTime).asCompactString()).append(")"); debugMsg.append(" type: ").append(returnAuthType).append(", using strategy ").append(strategy); debugMsg.append(", using proxy connection: ").append(useProxy); debugMsg.append(", returning bind dn: ").append(returnProvider == null ? "none" : returnProvider.getChaiConfiguration().getSetting(ChaiSetting.BIND_DN)); log(PwmLogLevel.INFO, debugMsg); final MacroMachine macroMachine = MacroMachine.forUser(pwmApplication, PwmConstants.DEFAULT_LOCALE, sessionLabel, userIdentity); final AuditRecord auditRecord = new AuditRecordFactory(pwmApplication, macroMachine).createUserAuditRecord( AuditEvent.AUTHENTICATE, this.userIdentity, makeAuditLogMessage(returnAuthType), sessionLabel.getSrcAddress(), sessionLabel.getSrcHostname() ); pwmApplication.getAuditManager().submit(auditRecord); pwmApplication.getSessionTrackService().addRecentLogin(userIdentity); return authenticationResult; } private void initialize() { if (startTime != null) { throw new IllegalStateException("AuthenticationRequest can not be used more than once"); } startTime = new Date(); } private void testCredentials( final UserIdentity userIdentity, final PasswordData password ) throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException { log(PwmLogLevel.TRACE, "beginning testCredentials process"); if (userIdentity == null || userIdentity.getUserDN() == null || userIdentity.getUserDN().length() < 1) { final String errorMsg = "attempt to authenticate with null userDN"; log(PwmLogLevel.DEBUG, errorMsg); throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_WRONGPASSWORD,errorMsg)); } if (password == null) { final String errorMsg = "attempt to authenticate with null password"; log(PwmLogLevel.DEBUG, errorMsg); throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_WRONGPASSWORD,errorMsg)); } //try authenticating the user using a normal ldap BIND operation. log(PwmLogLevel.TRACE, "attempting authentication using ldap BIND"); boolean bindSucceeded = false; try { //read a provider using the user's DN and password. userProvider = LdapOperationsHelper.createChaiProvider( sessionLabel, userIdentity.getLdapProfile(pwmApplication.getConfig()), pwmApplication.getConfig(), userIdentity.getUserDN(), password ); //issue a read operation to trigger a bind. userProvider.readStringAttribute(userIdentity.getUserDN(), ChaiConstant.ATTR_LDAP_OBJECTCLASS); bindSucceeded = true; } catch (ChaiException e) { if (e.getErrorCode() != null && e.getErrorCode() == ChaiError.INTRUDER_LOCKOUT) { final String errorMsg = "intruder lockout detected for user " + userIdentity + " marking session as locked out: " + e.getMessage(); final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INTRUDER_LDAP, errorMsg); log(PwmLogLevel.WARN, errorInformation.toDebugStr()); throw new PwmUnrecoverableException(errorInformation); } final PwmError pwmError = PwmError.forChaiError(e.getErrorCode()); final ErrorInformation errorInformation; if (pwmError != null && PwmError.ERROR_UNKNOWN != pwmError) { errorInformation = new ErrorInformation(pwmError, e.getMessage()); } else { errorInformation = new ErrorInformation(PwmError.ERROR_WRONGPASSWORD, "ldap error during password check: " + e.getMessage()); } log(PwmLogLevel.DEBUG, errorInformation.toDebugStr()); throw new PwmOperationalException(errorInformation); } finally { if (!bindSucceeded && userProvider != null){ try { userProvider.close(); userProvider = null; } catch (Throwable e) { log(PwmLogLevel.ERROR, "unexpected error closing invalid ldap connection after failed login attempt: " + e.getMessage()); } } } } private PasswordData learnUserPassword() throws ChaiUnavailableException, PwmUnrecoverableException { log(PwmLogLevel.TRACE, "beginning auth processes for user with unknown password"); return LdapOperationsHelper.readLdapPassword(pwmApplication, sessionLabel, userIdentity); } private PasswordData setTempUserPassword( ) throws ChaiUnavailableException, ImpossiblePasswordPolicyException, PwmUnrecoverableException { final boolean configAlwaysUseProxy = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_USE_PROXY_FOR_FORGOTTEN); final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider(userIdentity.getLdapProfileID()); final ChaiUser chaiUser = ChaiFactory.createChaiUser(userIdentity.getUserDN(), chaiProvider); // try setting a random password on the account to authenticate. if (!configAlwaysUseProxy && requestedAuthType == AuthenticationType.AUTH_FROM_PUBLIC_MODULE) { log(PwmLogLevel.DEBUG, "attempting to set temporary random password"); final PwmPasswordPolicy passwordPolicy = PasswordUtility.readPasswordPolicyForUser( pwmApplication, sessionLabel, userIdentity, chaiUser, PwmConstants.DEFAULT_LOCALE ); // create random password for user final RandomPasswordGenerator.RandomGeneratorConfig randomGeneratorConfig = new RandomPasswordGenerator.RandomGeneratorConfig(); randomGeneratorConfig.setSeedlistPhrases(RandomPasswordGenerator.DEFAULT_SEED_PHRASES); randomGeneratorConfig.setPasswordPolicy(passwordPolicy); final PasswordData currentPass = RandomPasswordGenerator.createRandomPassword(sessionLabel, randomGeneratorConfig, pwmApplication); try { final String oracleDS_PrePasswordAllowChangeTime = oraclePreTemporaryPwHandler(chaiProvider, chaiUser); // write the random password for the user. chaiUser.setPassword(currentPass.getStringValue()); oraclePostTemporaryPwHandler(chaiProvider, chaiUser, oracleDS_PrePasswordAllowChangeTime); log(PwmLogLevel.INFO, "user " + userIdentity + " password has been set to random value to use for user authentication"); } catch (ChaiOperationException e) { final String errorStr = "error setting random password for user " + userIdentity + " " + e.getMessage(); log(PwmLogLevel.ERROR, errorStr); throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_BAD_SESSION_PASSWORD, errorStr)); } return currentPass; } return null; } private String oraclePreTemporaryPwHandler( final ChaiProvider chaiProvider, final ChaiUser chaiUser ) throws PwmUnrecoverableException, ChaiUnavailableException, ChaiOperationException { if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.ORACLE_DS_ENABLE_MANIP_ALLOWCHANGETIME)) { return null; } if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) { return null; } // oracle DS special case: passwordAllowChangeTime handler final String oracleDS_PrePasswordAllowChangeTime = chaiProvider.readStringAttribute( chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME); log(PwmLogLevel.TRACE,"read OracleDS value of passwordAllowChangeTime value=" + oracleDS_PrePasswordAllowChangeTime); if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) { final Date date = OracleDSEntries.convertZuluToDate(oracleDS_PrePasswordAllowChangeTime); final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME); if (enforceFromForgotten) { if (new Date().before(date)) { final String errorMsg = "change not permitted until " + JavaHelper.toIsoDate( date); throw new PwmUnrecoverableException( new ErrorInformation(PwmError.PASSWORD_TOO_SOON, errorMsg)); } } } return oracleDS_PrePasswordAllowChangeTime; } private void oraclePostTemporaryPwHandler( final ChaiProvider chaiProvider, final ChaiUser chaiUser, final String oracleDS_PrePasswordAllowChangeTime ) throws ChaiUnavailableException, ChaiOperationException { if (!pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.ORACLE_DS_ENABLE_MANIP_ALLOWCHANGETIME)) { return; } // oracle DS special case: passwordAllowChangeTime handler if (ChaiProvider.DIRECTORY_VENDOR.ORACLE_DS != chaiUser.getChaiProvider().getDirectoryVendor()) { return; } if (oracleDS_PrePasswordAllowChangeTime != null && !oracleDS_PrePasswordAllowChangeTime.isEmpty()) { // write back the original pre-password allow change time. final Set<String> values = new HashSet<>( Collections.singletonList(oracleDS_PrePasswordAllowChangeTime)); chaiProvider.writeStringAttribute(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME, values, true); log(PwmLogLevel.TRACE,"re-wrote passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + ", value=" + oracleDS_PrePasswordAllowChangeTime); } else { final String oracleDS_PostPasswordAllowChangeTime = chaiProvider.readStringAttribute( chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME); if (oracleDS_PostPasswordAllowChangeTime != null && !oracleDS_PostPasswordAllowChangeTime.isEmpty()) { final boolean PostTempUseCurrentTime = Boolean.parseBoolean(pwmApplication.getConfig().readAppProperty(AppProperty.LDAP_ORACLE_POST_TEMPPW_USE_CURRENT_TIME)); if (PostTempUseCurrentTime) { log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will replace with current time value"); final String newTimeValue = OracleDSEntries.convertDateToZulu(new Date()); final Set<String> values = new HashSet<>(Collections.singletonList(newTimeValue)); chaiProvider.writeStringAttribute(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME, values, true); log(PwmLogLevel.TRACE, "wrote attribute value '" + newTimeValue + "' for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN()); } else { // password allow change time has appeared, but wasn't present previously, so delete it. log(PwmLogLevel.TRACE, "a new value for passwordAllowChangeTime attribute to user " + chaiUser.getEntryDN() + " has appeared, will remove"); chaiProvider.deleteStringAttributeValue(chaiUser.getEntryDN(), ORACLE_ATTR_PW_ALLOW_CHG_TIME, oracleDS_PostPasswordAllowChangeTime); log(PwmLogLevel.TRACE, "deleted attribute value for passwordAllowChangeTime attribute on user " + chaiUser.getEntryDN()); } } } } private boolean determineIfLdapProxyNeeded(final AuthenticationType authenticationType, final PasswordData userPassword) throws ChaiUnavailableException, PwmUnrecoverableException { if (userProvider != null) { return false; } final boolean authIsBindInhibit = authenticationType == AuthenticationType.AUTH_BIND_INHIBIT; final boolean authIsFromForgottenPw = authenticationType == AuthenticationType.AUTH_FROM_PUBLIC_MODULE; final boolean alwaysUseProxyIsEnabled = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.AD_USE_PROXY_FOR_FORGOTTEN); final boolean passwordNotPresent = userPassword == null; return authIsBindInhibit || authIsFromForgottenPw && (alwaysUseProxyIsEnabled || passwordNotPresent); } private ChaiProvider makeProxyProvider() throws ChaiUnavailableException, PwmUnrecoverableException { final LdapProfile profile = pwmApplication.getConfig().getLdapProfiles().get(userIdentity.getLdapProfileID()); final String proxyDN = profile.readSettingAsString(PwmSetting.LDAP_PROXY_USER_DN); final PasswordData proxyPassword = profile.readSettingAsPassword(PwmSetting.LDAP_PROXY_USER_PASSWORD); return LdapOperationsHelper.createChaiProvider(sessionLabel, profile, pwmApplication.getConfig(), proxyDN, proxyPassword); } private void log(final PwmLogLevel level, final CharSequence message) { LOGGER.log(level, sessionLabel,"authID=" + operationNumber + ", " + message); } private String makeAuditLogMessage(final AuthenticationType authenticationType) { return "type=" + authenticationType.toString() + ", " + "source=" + (authenticationSource == null ? "null" : authenticationSource.toString()); } }