/* * 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.http.servlet.forgottenpw; import com.novell.ldapchai.ChaiUser; import com.novell.ldapchai.cr.Challenge; import com.novell.ldapchai.cr.ChallengeSet; import com.novell.ldapchai.cr.ResponseSet; import com.novell.ldapchai.exception.ChaiUnavailableException; import com.novell.ldapchai.exception.ChaiValidationException; import com.novell.ldapchai.provider.ChaiProvider; import password.pwm.AppProperty; import password.pwm.PwmApplication; import password.pwm.PwmConstants; import password.pwm.bean.EmailItemBean; import password.pwm.bean.UserIdentity; import password.pwm.bean.UserInfoBean; import password.pwm.config.Configuration; import password.pwm.config.FormConfiguration; import password.pwm.config.PwmSetting; import password.pwm.config.option.IdentityVerificationMethod; import password.pwm.config.option.MessageSendMethod; import password.pwm.config.option.RecoveryAction; import password.pwm.config.profile.ForgottenPasswordProfile; import password.pwm.error.ErrorInformation; import password.pwm.error.PwmError; import password.pwm.error.PwmOperationalException; import password.pwm.error.PwmUnrecoverableException; import password.pwm.http.PwmRequest; import password.pwm.http.bean.ForgottenPasswordBean; import password.pwm.http.filter.AuthenticationFilter; import password.pwm.ldap.LdapUserDataReader; import password.pwm.ldap.UserStatusReader; import password.pwm.svc.stats.Statistic; import password.pwm.svc.stats.StatisticsManager; import password.pwm.svc.token.TokenPayload; import password.pwm.svc.token.TokenService; import password.pwm.svc.token.TokenType; import password.pwm.util.java.JavaHelper; import password.pwm.util.logging.PwmLogger; import password.pwm.util.macro.MacroMachine; import password.pwm.util.operations.PasswordUtility; import password.pwm.ws.client.rest.RestTokenDataClient; import javax.servlet.ServletException; import java.io.IOException; import java.time.Instant; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; class ForgottenPasswordUtil { private static final PwmLogger LOGGER = PwmLogger.forClass(ForgottenPasswordUtil.class); static Set<IdentityVerificationMethod> figureRemainingAvailableOptionalAuthMethods( final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean ) { final ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags(); final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress(); final Set<IdentityVerificationMethod> result = new LinkedHashSet<>(); result.addAll(recoveryFlags.getOptionalAuthMethods()); result.removeAll(progress.getSatisfiedMethods()); for (final IdentityVerificationMethod recoveryVerificationMethods : new LinkedHashSet<>(result)) { try { verifyRequirementsForAuthMethod(pwmRequest, forgottenPasswordBean, recoveryVerificationMethods); } catch (PwmUnrecoverableException e) { result.remove(recoveryVerificationMethods); } } return Collections.unmodifiableSet(result); } public static RecoveryAction getRecoveryAction(final Configuration configuration, final ForgottenPasswordBean forgottenPasswordBean) { final ForgottenPasswordProfile forgottenPasswordProfile = configuration.getForgottenPasswordProfiles().get(forgottenPasswordBean.getForgottenPasswordProfileID()); return forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_ACTION, RecoveryAction.class); } static Set<IdentityVerificationMethod> figureSatisfiedOptionalAuthMethods( final ForgottenPasswordBean.RecoveryFlags recoveryFlags, final ForgottenPasswordBean.Progress progress) { final Set<IdentityVerificationMethod> result = new LinkedHashSet<>(); result.addAll(recoveryFlags.getOptionalAuthMethods()); result.retainAll(progress.getSatisfiedMethods()); return Collections.unmodifiableSet(result); } static UserInfoBean readUserInfoBean(final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean) throws PwmUnrecoverableException { if (forgottenPasswordBean.getUserIdentity() == null) { return null; } final String CACHE_KEY = "ForgottenPassword-UserInfoCache"; final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity(); { final UserInfoBean beanInRequest = (UserInfoBean)pwmRequest.getHttpServletRequest().getSession().getAttribute(CACHE_KEY); if (beanInRequest != null) { if (userIdentity.equals(beanInRequest.getUserIdentity())) { LOGGER.trace(pwmRequest, "using request cached UserInfoBean"); return beanInRequest; } else { LOGGER.trace(pwmRequest, "request cached userInfoBean is not for current user, clearing."); pwmRequest.getHttpServletRequest().getSession().setAttribute(CACHE_KEY, null); } } } final ChaiProvider chaiProvider = pwmRequest.getPwmApplication().getProxyChaiProvider(userIdentity.getLdapProfileID()); final UserStatusReader userStatusReader = new UserStatusReader(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel()); final UserInfoBean userInfoBean = new UserInfoBean(); userStatusReader.populateUserInfoBean( userInfoBean, pwmRequest.getLocale(), userIdentity, chaiProvider ); pwmRequest.getHttpServletRequest().getSession().setAttribute(CACHE_KEY, userInfoBean); return userInfoBean; } static ResponseSet readResponseSet(final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean) throws PwmUnrecoverableException { if (forgottenPasswordBean.getUserIdentity() == null) { return null; } final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity(); final ResponseSet responseSet; try { final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity); responseSet = pwmApplication.getCrService().readUserResponseSet( pwmRequest.getSessionLabel(), userIdentity, theUser ); } catch (ChaiUnavailableException e) { throw PwmUnrecoverableException.fromChaiException(e); } return responseSet; } static void sendUnlockNoticeEmail( final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean ) throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException { final PwmApplication pwmApplication = pwmRequest.getPwmApplication(); final Configuration config = pwmRequest.getConfig(); final Locale locale = pwmRequest.getLocale(); final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity(); final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_UNLOCK, locale); if (configuredEmailSetting == null) { LOGGER.debug(pwmRequest, "skipping send unlock notice email for '" + userIdentity + "' no email configured"); return; } final UserInfoBean userInfoBean = readUserInfoBean(pwmRequest, forgottenPasswordBean); final MacroMachine macroMachine = new MacroMachine( pwmApplication, pwmRequest.getSessionLabel(), userInfoBean, null, LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity) ); pwmApplication.getEmailQueue().submitEmail( configuredEmailSetting, userInfoBean, macroMachine ); } static boolean checkAuthRecord(final PwmRequest pwmRequest, final String userGuid) throws PwmUnrecoverableException { if (userGuid == null || userGuid.isEmpty()) { return false; } try { final String cookieName = pwmRequest.getConfig().readAppProperty(AppProperty.HTTP_COOKIE_AUTHRECORD_NAME); if (cookieName == null || cookieName.isEmpty()) { LOGGER.trace(pwmRequest, "skipping auth record cookie read, cookie name parameter is blank"); return false; } final AuthenticationFilter.AuthRecord authRecord = pwmRequest.readEncryptedCookie(cookieName, AuthenticationFilter.AuthRecord.class); if (authRecord != null) { if (authRecord.getGuid() != null && !authRecord.getGuid().isEmpty() && authRecord.getGuid().equals(userGuid)) { LOGGER.debug(pwmRequest, "auth record cookie validated"); return true; } } } catch (Exception e) { LOGGER.error(pwmRequest, "unexpected error while examining cookie auth record: " + e.getMessage()); } return false; } static MessageSendMethod figureTokenSendPreference( final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean ) throws PwmUnrecoverableException { final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean); final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod(); if (tokenSendMethod == null || tokenSendMethod.equals(MessageSendMethod.NONE)) { return MessageSendMethod.NONE; } if (!tokenSendMethod.equals(MessageSendMethod.CHOICE_SMS_EMAIL)) { return tokenSendMethod; } final String emailAddress = userInfoBean.getUserEmailAddress(); final String smsAddress = userInfoBean.getUserSmsNumber(); final boolean hasEmail = emailAddress != null && emailAddress.length() > 1; final boolean hasSms = smsAddress != null && smsAddress.length() > 1; if (hasEmail && hasSms) { return MessageSendMethod.CHOICE_SMS_EMAIL; } else if (hasEmail) { LOGGER.debug(pwmRequest, "though token send method is " + MessageSendMethod.CHOICE_SMS_EMAIL + ", no sms address is available for user so defaulting to email method"); return MessageSendMethod.EMAILONLY; } else if (hasSms) { LOGGER.debug(pwmRequest, "though token send method is " + MessageSendMethod.CHOICE_SMS_EMAIL + ", no email address is available for user so defaulting to sms method"); return MessageSendMethod.SMSONLY; } throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT)); } static void verifyRequirementsForAuthMethod( final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean, final IdentityVerificationMethod recoveryVerificationMethods ) throws PwmUnrecoverableException { switch (recoveryVerificationMethods) { case TOKEN: { final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod(); if (tokenSendMethod == null || tokenSendMethod == MessageSendMethod.NONE) { final String errorMsg = "user is required to complete token validation, yet there is not a token send method configured"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG, errorMsg); throw new PwmUnrecoverableException(errorInformation); } } break; case ATTRIBUTES: { final List<FormConfiguration> formConfiguration = forgottenPasswordBean.getAttributeForm(); if (formConfiguration == null || formConfiguration.isEmpty()) { final String errorMsg = "user is required to complete LDAP attribute check, yet there are no LDAP attribute form items configured"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG, errorMsg); throw new PwmUnrecoverableException(errorInformation); } } break; case OTP: { final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean); if (userInfoBean.getOtpUserRecord() == null) { final String errorMsg = "could not find a one time password configuration for " + userInfoBean.getUserIdentity(); final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_NO_OTP_CONFIGURATION, errorMsg); throw new PwmUnrecoverableException(errorInformation); } } break; case CHALLENGE_RESPONSES: { final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean); final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet(pwmRequest, forgottenPasswordBean); if (responseSet == null) { final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES); throw new PwmUnrecoverableException(errorInformation); } final ChallengeSet challengeSet = userInfoBean.getChallengeProfile().getChallengeSet(); try { if (responseSet.meetsChallengeSetRequirements(challengeSet)) { if (challengeSet.getRequiredChallenges().isEmpty() && (challengeSet.getMinRandomRequired() <= 0)) { final String errorMsg = "configured challenge set policy for " + userInfoBean.getUserIdentity().toString() + " is empty, user not qualified to recover password"; final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, errorMsg); throw new PwmUnrecoverableException(errorInformation); } } } catch (ChaiValidationException e) { final String errorMsg = "stored response set for user '" + userInfoBean.getUserIdentity() + "' do not meet current challenge set requirements: " + e.getLocalizedMessage(); final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES, errorMsg); throw new PwmUnrecoverableException(errorInformation); } } break; default: // continue, assume no data requirements for method. break; } } static Map<Challenge, String> readResponsesFromHttpRequest( final PwmRequest req, final ChallengeSet challengeSet ) throws ChaiValidationException, ChaiUnavailableException, PwmUnrecoverableException { final Map<Challenge, String> responses = new LinkedHashMap<>(); int counter = 0; for (final Challenge loopChallenge : challengeSet.getChallenges()) { counter++; final String answer = req.readParameterAsString(PwmConstants.PARAM_RESPONSE_PREFIX + counter); responses.put(loopChallenge, answer.length() > 0 ? answer : ""); } return responses; } static String initializeAndSendToken( final PwmRequest pwmRequest, final UserInfoBean userInfoBean, final MessageSendMethod tokenSendMethod ) throws PwmUnrecoverableException { final Configuration config = pwmRequest.getConfig(); final UserIdentity userIdentity = userInfoBean.getUserIdentity(); final Map<String,String> tokenMapData = new LinkedHashMap<>(); try { final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userIdentity ); if (userLastPasswordChange != null) { final String userChangeString = JavaHelper.toIsoDate(userLastPasswordChange); tokenMapData.put(PwmConstants.TOKEN_KEY_PWD_CHG_DATE, userChangeString); } } catch (ChaiUnavailableException e) { LOGGER.error(pwmRequest, "unexpected error reading user's last password change time"); } final EmailItemBean emailItemBean = config.readSettingAsEmail(PwmSetting.EMAIL_CHALLENGE_TOKEN, pwmRequest.getLocale()); final MacroMachine macroMachine = MacroMachine.forUser(pwmRequest, userIdentity); final RestTokenDataClient.TokenDestinationData inputDestinationData = new RestTokenDataClient.TokenDestinationData( macroMachine.expandMacros(emailItemBean.getTo()), userInfoBean.getUserSmsNumber(), null ); final RestTokenDataClient restTokenDataClient = new RestTokenDataClient(pwmRequest.getPwmApplication()); final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString( pwmRequest.getSessionLabel(), inputDestinationData, userIdentity, pwmRequest.getLocale()); final String tokenDestinationAddress = outputDestrestTokenDataClient.getDisplayValue(); final Set<String> destinationValues = new LinkedHashSet<>(); if (outputDestrestTokenDataClient.getEmail() != null) { destinationValues.add(outputDestrestTokenDataClient.getEmail()); } if (outputDestrestTokenDataClient.getSms() != null) { destinationValues.add(outputDestrestTokenDataClient.getSms()); } final String tokenKey; final TokenPayload tokenPayload; try { tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(TokenType.FORGOTTEN_PW, tokenMapData, userIdentity, destinationValues); tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken(tokenPayload, pwmRequest.getSessionLabel()); } catch (PwmOperationalException e) { throw new PwmUnrecoverableException(e.getErrorInformation()); } final String smsMessage = config.readSettingAsLocalizedString(PwmSetting.SMS_CHALLENGE_TOKEN_TEXT, pwmRequest.getLocale()); TokenService.TokenSender.sendToken( pwmRequest.getPwmApplication(), userInfoBean, macroMachine, emailItemBean, tokenSendMethod, outputDestrestTokenDataClient.getEmail(), outputDestrestTokenDataClient.getSms(), smsMessage, tokenKey ); StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_TOKENS_SENT); return tokenDestinationAddress; } }