/*
* 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.ChaiOperationException;
import com.novell.ldapchai.exception.ChaiUnavailableException;
import com.novell.ldapchai.exception.ChaiValidationException;
import password.pwm.AppProperty;
import password.pwm.PwmApplication;
import password.pwm.PwmConstants;
import password.pwm.VerificationMethodSystem;
import password.pwm.bean.PasswordStatus;
import password.pwm.bean.SessionLabel;
import password.pwm.bean.UserIdentity;
import password.pwm.bean.UserInfoBean;
import password.pwm.config.ActionConfiguration;
import password.pwm.config.Configuration;
import password.pwm.config.FormConfiguration;
import password.pwm.config.FormUtility;
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.config.profile.ProfileType;
import password.pwm.config.profile.ProfileUtility;
import password.pwm.error.ErrorInformation;
import password.pwm.error.PwmDataValidationException;
import password.pwm.error.PwmError;
import password.pwm.error.PwmException;
import password.pwm.error.PwmOperationalException;
import password.pwm.error.PwmUnrecoverableException;
import password.pwm.http.HttpMethod;
import password.pwm.http.JspUrl;
import password.pwm.http.ProcessStatus;
import password.pwm.http.PwmHttpRequestWrapper;
import password.pwm.http.PwmRequest;
import password.pwm.http.PwmRequestAttribute;
import password.pwm.http.PwmSession;
import password.pwm.http.bean.ForgottenPasswordBean;
import password.pwm.http.servlet.AbstractPwmServlet;
import password.pwm.http.servlet.ControlledPwmServlet;
import password.pwm.http.servlet.PwmServletDefinition;
import password.pwm.http.servlet.oauth.OAuthForgottenPasswordResults;
import password.pwm.http.servlet.oauth.OAuthMachine;
import password.pwm.http.servlet.oauth.OAuthSettings;
import password.pwm.i18n.Message;
import password.pwm.ldap.LdapOperationsHelper;
import password.pwm.ldap.LdapUserDataReader;
import password.pwm.ldap.UserDataReader;
import password.pwm.ldap.auth.AuthenticationType;
import password.pwm.ldap.auth.AuthenticationUtility;
import password.pwm.ldap.auth.PwmAuthenticationSource;
import password.pwm.ldap.auth.SessionAuthenticator;
import password.pwm.ldap.search.SearchConfiguration;
import password.pwm.ldap.search.UserSearchEngine;
import password.pwm.svc.event.AuditEvent;
import password.pwm.svc.intruder.RecordType;
import password.pwm.svc.stats.Statistic;
import password.pwm.svc.stats.StatisticsManager;
import password.pwm.svc.token.TokenPayload;
import password.pwm.svc.token.TokenType;
import password.pwm.util.CaptchaUtility;
import password.pwm.util.LocaleHelper;
import password.pwm.util.PasswordData;
import password.pwm.util.PostChangePasswordAction;
import password.pwm.util.RandomPasswordGenerator;
import password.pwm.util.java.JavaHelper;
import password.pwm.util.java.JsonUtil;
import password.pwm.util.logging.PwmLogger;
import password.pwm.util.operations.ActionExecutor;
import password.pwm.util.operations.PasswordUtility;
import password.pwm.util.operations.cr.NMASCrOperator;
import password.pwm.util.operations.otp.OTPUserRecord;
import password.pwm.ws.server.RestResultBean;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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;
/**
* User interaction servlet for recovering user's password using secret question/answer
*
* @author Jason D. Rivard
*/
@WebServlet(
name="ForgottenPasswordServlet",
urlPatterns = {
PwmConstants.URL_PREFIX_PUBLIC + "/forgottenpassword",
PwmConstants.URL_PREFIX_PUBLIC + "/forgottenpassword/*",
PwmConstants.URL_PREFIX_PUBLIC + "/ForgottenPassword",
PwmConstants.URL_PREFIX_PUBLIC + "/ForgottenPassword/*",
}
)
public class ForgottenPasswordServlet extends ControlledPwmServlet {
// ------------------------------ FIELDS ------------------------------
private static final PwmLogger LOGGER = PwmLogger.forClass(ForgottenPasswordServlet.class);
public enum ForgottenPasswordAction implements AbstractPwmServlet.ProcessAction {
search(HttpMethod.POST),
checkResponses(HttpMethod.POST),
checkAttributes(HttpMethod.POST),
enterCode(HttpMethod.POST, HttpMethod.GET),
enterOtp(HttpMethod.POST),
reset(HttpMethod.POST),
actionChoice(HttpMethod.POST),
tokenChoice(HttpMethod.POST),
verificationChoice(HttpMethod.POST),
enterRemoteResponse(HttpMethod.POST),
oauthReturn(HttpMethod.GET),
resendToken(HttpMethod.POST),
;
private final Collection<HttpMethod> method;
ForgottenPasswordAction(final HttpMethod... method)
{
this.method = Collections.unmodifiableList(Arrays.asList(method));
}
public Collection<HttpMethod> permittedMethods()
{
return method;
}
}
@Override
public Class<? extends ProcessAction> getProcessActionsClass() {
return ForgottenPasswordAction.class;
}
public enum ActionChoice {
unlock,
resetPassword,
}
public enum TokenChoice {
email,
sms,
}
@Override
public ProcessStatus preProcessCheck(final PwmRequest pwmRequest) throws PwmUnrecoverableException, IOException, ServletException {
final PwmSession pwmSession = pwmRequest.getPwmSession();
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final Configuration config = pwmApplication.getConfig();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (!config.readSettingAsBoolean(PwmSetting.FORGOTTEN_PASSWORD_ENABLE)) {
pwmRequest.respondWithError(PwmError.ERROR_SERVICE_NOT_AVAILABLE.toInfo());
return ProcessStatus.Halt;
}
if (pwmSession.isAuthenticated()) {
pwmRequest.respondWithError(PwmError.ERROR_USERAUTHENTICATED.toInfo());
return ProcessStatus.Halt;
}
if (forgottenPasswordBean.getUserIdentity() != null) {
pwmApplication.getIntruderManager().convenience().checkUserIdentity(forgottenPasswordBean.getUserIdentity());
}
checkForLocaleSwitch(pwmRequest, forgottenPasswordBean);
final ProcessAction action = this.readProcessAction(pwmRequest);
// convert a url command like /public/newuser/12321321 to redirect with a process action.
if (action == null) {
if (pwmRequest.convertURLtokenCommand()) {
return ProcessStatus.Halt;
}
}
return ProcessStatus.Continue;
}
private static ForgottenPasswordBean forgottenPasswordBean(final PwmRequest pwmRequest) throws PwmUnrecoverableException {
return pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, ForgottenPasswordBean.class);
}
private static void clearForgottenPasswordBean(final PwmRequest pwmRequest) throws PwmUnrecoverableException {
pwmRequest.getPwmApplication().getSessionStateService().clearBean(pwmRequest, ForgottenPasswordBean.class);
}
@ActionHandler(action = "actionChoice")
private ProcessStatus processActionChoice(final PwmRequest pwmRequest)
throws PwmUnrecoverableException, ServletException, IOException, ChaiUnavailableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final boolean resendEnabled = Boolean.parseBoolean(pwmRequest.getConfig().readAppProperty(AppProperty.TOKEN_RESEND_ENABLED));
if (resendEnabled) {
// clear token dest info in case we got here from a user 'go-back' request
forgottenPasswordBean.getProgress().clearTokenSentStatus();
}
if (forgottenPasswordBean.getProgress().isAllPassed()) {
final String choice = pwmRequest.readParameterAsString("choice");
final ActionChoice actionChoice = JavaHelper.readEnumFromString(ActionChoice.class, null, choice);
if (actionChoice != null) {
switch (actionChoice) {
case unlock:
this.executeUnlock(pwmRequest);
break;
case resetPassword:
this.executeResetPassword(pwmRequest);
break;
default:
JavaHelper.unhandledSwitchStatement(actionChoice);
}
}
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "reset")
private ProcessStatus processReset(final PwmRequest pwmRequest)
throws IOException, PwmUnrecoverableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
clearForgottenPasswordBean(pwmRequest);
if (forgottenPasswordBean.getUserIdentity() == null) {
pwmRequest.sendRedirectToContinue();
return ProcessStatus.Halt;
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "tokenChoice")
private ProcessStatus processTokenChoice(final PwmRequest pwmRequest)
throws PwmUnrecoverableException, ServletException, IOException, ChaiUnavailableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (forgottenPasswordBean.getProgress().getTokenSendChoice() == MessageSendMethod.CHOICE_SMS_EMAIL) {
final String choice = pwmRequest.readParameterAsString("choice");
final TokenChoice tokenChoice = JavaHelper.readEnumFromString(TokenChoice.class, null, choice);
if (tokenChoice != null) {
switch (tokenChoice) {
case email:
forgottenPasswordBean.getProgress().setTokenSendChoice(MessageSendMethod.EMAILONLY);
break;
case sms:
forgottenPasswordBean.getProgress().setTokenSendChoice(MessageSendMethod.SMSONLY);
break;
default:
JavaHelper.unhandledSwitchStatement(tokenChoice);
}
}
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "verificationChoice")
private ProcessStatus processVerificationChoice(final PwmRequest pwmRequest)
throws PwmUnrecoverableException, ServletException, IOException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final String requestedChoiceStr = pwmRequest.readParameterAsString("choice");
final LinkedHashSet<IdentityVerificationMethod> remainingAvailableOptionalMethods = new LinkedHashSet<>(
ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods(pwmRequest, forgottenPasswordBean)
);
pwmRequest.setAttribute(PwmRequestAttribute.AvailableAuthMethods, remainingAvailableOptionalMethods);
IdentityVerificationMethod requestedChoice = null;
if (requestedChoiceStr != null && !requestedChoiceStr.isEmpty()) {
try {
requestedChoice = IdentityVerificationMethod.valueOf(requestedChoiceStr);
} catch (IllegalArgumentException e) {
final String errorMsg = "unknown verification method requested";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,errorMsg);
setLastError(pwmRequest, errorInformation);
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_METHOD_CHOICE);
return ProcessStatus.Halt;
}
}
if (remainingAvailableOptionalMethods.contains(requestedChoice)) {
forgottenPasswordBean.getProgress().setInProgressVerificationMethod(requestedChoice);
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordOptionalPageView,"true");
forwardUserBasedOnRecoveryMethod(pwmRequest, requestedChoice);
return ProcessStatus.Continue;
} else if (requestedChoice != null) {
final String errorMsg = "requested verification method is not available at this time";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,errorMsg);
setLastError(pwmRequest, errorInformation);
}
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_METHOD_CHOICE);
return ProcessStatus.Halt;
}
@ActionHandler(action = "search")
private ProcessStatus processSearch(final PwmRequest pwmRequest)
throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
{
final PwmSession pwmSession = pwmRequest.getPwmSession();
final Locale userLocale = pwmRequest.getLocale();
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final String contextParam = pwmRequest.readParameterAsString(PwmConstants.PARAM_CONTEXT);
final String ldapProfile = pwmRequest.readParameterAsString(PwmConstants.PARAM_LDAP_PROFILE);
// clear the bean
clearForgottenPasswordBean(pwmRequest);
if (!CaptchaUtility.verifyReCaptcha(pwmRequest)) {
final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_BAD_CAPTCHA_RESPONSE);
LOGGER.debug(pwmRequest, errorInfo);
setLastError(pwmRequest, errorInfo);
return ProcessStatus.Continue;
}
final List<FormConfiguration> forgottenPasswordForm = pwmApplication.getConfig().readSettingAsForm(
PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM);
Map<FormConfiguration, String> formValues = new LinkedHashMap<>();
try {
//read the values from the request
formValues = FormUtility.readFormValuesFromRequest(pwmRequest, forgottenPasswordForm, userLocale);
// check for intruder search values
pwmApplication.getIntruderManager().convenience().checkAttributes(formValues);
// see if the values meet the configured form requirements.
FormUtility.validateFormValues(pwmRequest.getConfig(), formValues, userLocale);
final String searchFilter;
{
final String configuredSearchFilter = pwmApplication.getConfig().readSettingAsString(PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FILTER);
if (configuredSearchFilter == null || configuredSearchFilter.isEmpty()) {
searchFilter = FormUtility.ldapSearchFilterForForm(pwmApplication, forgottenPasswordForm);
LOGGER.trace(pwmSession,"auto generated ldap search filter: " + searchFilter);
} else {
searchFilter = configuredSearchFilter;
}
}
// convert the username field to an identity
final UserIdentity userIdentity;
{
final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
.filter(searchFilter)
.formValues(formValues)
.contexts(Collections.singletonList(contextParam))
.ldapProfile(ldapProfile)
.build();
userIdentity = userSearchEngine.performSingleUserSearch(searchConfiguration, pwmRequest.getSessionLabel());
}
if (userIdentity == null) {
pwmApplication.getIntruderManager().convenience().markAddressAndSession(pwmSession);
pwmApplication.getStatisticsManager().incrementValue(Statistic.RECOVERY_FAILURES);
setLastError(pwmRequest, PwmError.ERROR_CANT_MATCH_USER.toInfo());
return ProcessStatus.Continue;
}
AuthenticationUtility.checkIfUserEligibleToAuthentication(pwmApplication, userIdentity);
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
initForgottenPasswordBean(pwmRequest, userIdentity, forgottenPasswordBean);
// clear intruder search values
pwmApplication.getIntruderManager().convenience().clearAttributes(formValues);
} catch (PwmOperationalException e) {
final ErrorInformation errorInfo = new ErrorInformation(PwmError.ERROR_RESPONSES_NORESPONSES,e.getErrorInformation().getDetailedErrorMsg(),e.getErrorInformation().getFieldValues());
pwmApplication.getIntruderManager().mark(RecordType.ADDRESS, pwmSession.getSessionStateBean().getSrcAddress(), pwmRequest.getSessionLabel());
pwmApplication.getIntruderManager().convenience().markAttributes(formValues, pwmSession);
LOGGER.debug(pwmSession,errorInfo.toDebugStr());
setLastError(pwmRequest, errorInfo);
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "enterCode")
private ProcessStatus processEnterCode(final PwmRequest pwmRequest)
throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final String userEnteredCode = pwmRequest.readParameterAsString(PwmConstants.PARAM_TOKEN);
ErrorInformation errorInformation = null;
try {
final TokenPayload tokenPayload = pwmRequest.getPwmApplication().getTokenService().processUserEnteredCode(
pwmRequest.getPwmSession(),
forgottenPasswordBean.getUserIdentity() == null ? null : forgottenPasswordBean.getUserIdentity(),
TokenType.FORGOTTEN_PW,
userEnteredCode
);
if (tokenPayload != null) {
// token correct
if (forgottenPasswordBean.getUserIdentity() == null) {
// clean session, user supplied token (clicked email, etc) and this is first request
initForgottenPasswordBean(
pwmRequest,
tokenPayload.getUserIdentity(),
forgottenPasswordBean
);
}
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.TOKEN);
StatisticsManager.incrementStat(pwmRequest.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED);
}
} catch (PwmOperationalException e) {
final String errorMsg = "token incorrect: " + e.getMessage();
errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT,errorMsg);
}
if (!forgottenPasswordBean.getProgress().getSatisfiedMethods().contains(IdentityVerificationMethod.TOKEN)) {
if (errorInformation == null) {
errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT);
}
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "enterRemoteResponse")
private ProcessStatus processEnterRemoteResponse(final PwmRequest pwmRequest)
throws PwmUnrecoverableException, IOException, ServletException
{
final String PREFIX = "remote-";
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final VerificationMethodSystem remoteRecoveryMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
final Map<String,String> remoteResponses = new LinkedHashMap<>();
{
final Map<String,String> inputMap = pwmRequest.readParametersAsMap();
for (final String name : inputMap.keySet()) {
if (name != null && name.startsWith(PREFIX)) {
final String strippedName = name.substring(PREFIX.length(), name.length());
final String value = inputMap.get(name);
remoteResponses.put(strippedName, value);
}
}
}
final ErrorInformation errorInformation = remoteRecoveryMethod.respondToPrompts(remoteResponses);
if (remoteRecoveryMethod.getVerificationState() == VerificationMethodSystem.VerificationState.COMPLETE) {
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.REMOTE_RESPONSES);
}
if (remoteRecoveryMethod.getVerificationState() == VerificationMethodSystem.VerificationState.FAILED) {
forgottenPasswordBean.getProgress().setRemoteRecoveryMethod(null);
pwmRequest.respondWithError(errorInformation,true);
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
LOGGER.debug(pwmRequest, "unsuccessful remote response verification input: " + errorInformation.toDebugStr());
return ProcessStatus.Continue;
}
if (errorInformation != null) {
setLastError(pwmRequest, errorInformation);
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "enterOtp")
private ProcessStatus processEnterOtpToken(final PwmRequest pwmRequest)
throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final String userEnteredCode = pwmRequest.readParameterAsString(PwmConstants.PARAM_TOKEN);
LOGGER.debug(pwmRequest, String.format("entered OTP: %s", userEnteredCode));
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
final OTPUserRecord otpUserRecord = userInfoBean.getOtpUserRecord();
final boolean otpPassed;
if (otpUserRecord != null) {
LOGGER.info(pwmRequest, "checking entered OTP");
try {
// forces service to use proxy account to update (write) updated otp record if necessary.
otpPassed = pwmRequest.getPwmApplication().getOtpService().validateToken(
null,
forgottenPasswordBean.getUserIdentity(),
otpUserRecord,
userEnteredCode,
true
);
if (otpPassed) {
StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_PASSED);
LOGGER.debug(pwmRequest, "one time password validation has been passed");
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.OTP);
} else {
StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_FAILED);
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_OTP_TOKEN));
}
} catch (PwmOperationalException e) {
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_OTP_TOKEN,e.getErrorInformation().toDebugStr()));
}
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "oauthReturn")
private ProcessStatus processOAuthReturn(final PwmRequest pwmRequest)
throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (forgottenPasswordBean.getProgress().getInProgressVerificationMethod() != IdentityVerificationMethod.OAUTH) {
LOGGER.debug(pwmRequest, "oauth return detected, however current session did not issue an oauth request; will restart forgotten password sequence");
pwmRequest.getPwmApplication().getSessionStateService().clearBean(pwmRequest, ForgottenPasswordBean.class);
pwmRequest.sendRedirect(PwmServletDefinition.ForgottenPassword);
return ProcessStatus.Halt;
}
if (forgottenPasswordBean.getUserIdentity() == null) {
LOGGER.debug(pwmRequest, "oauth return detected, however current session does not have a user identity stored; will restart forgotten password sequence");
pwmRequest.getPwmApplication().getSessionStateService().clearBean(pwmRequest, ForgottenPasswordBean.class);
pwmRequest.sendRedirect(PwmServletDefinition.ForgottenPassword);
return ProcessStatus.Halt;
}
final String encryptedResult = pwmRequest.readParameterAsString(PwmConstants.PARAM_RECOVERY_OAUTH_RESULT, PwmHttpRequestWrapper.Flag.BypassValidation);
final OAuthForgottenPasswordResults results = pwmRequest.getPwmApplication().getSecureService().decryptObject(encryptedResult, OAuthForgottenPasswordResults.class);
LOGGER.trace(pwmRequest, "received ");
final String userDNfromOAuth = results.getUsername();
if (userDNfromOAuth == null || userDNfromOAuth.isEmpty()) {
final String errorMsg = "oauth server coderesolver endpoint did not return a username value";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
final UserIdentity oauthUserIdentity;
{
final UserSearchEngine userSearchEngine =pwmRequest.getPwmApplication().getUserSearchEngine();
try {
oauthUserIdentity = userSearchEngine.resolveUsername(userDNfromOAuth, null, null, pwmRequest.getSessionLabel());
} catch (PwmOperationalException e) {
final String errorMsg = "unexpected error searching for oauth supplied username in ldap; error: " + e.getMessage() ;
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
}
final boolean userMatch;
{
final UserIdentity userIdentityInBean = forgottenPasswordBean.getUserIdentity();
userMatch = userIdentityInBean != null && userIdentityInBean.equals(oauthUserIdentity);
}
if (userMatch) {
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.OAUTH);
} else {
final String errorMsg = "oauth server username does not match previously identified user";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
return ProcessStatus.Continue;
}
@ActionHandler(action = "checkResponses")
private ProcessStatus processCheckResponses(final PwmRequest pwmRequest)
throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException
{
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (forgottenPasswordBean.getUserIdentity() == null) {
return ProcessStatus.Continue;
}
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet(pwmRequest, forgottenPasswordBean);
if (responseSet == null) {
final String errorMsg = "attempt to check responses, but responses are not loaded into session bean";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
try {
// read the supplied responses from the user
final Map<Challenge, String> crMap = ForgottenPasswordUtil.readResponsesFromHttpRequest(
pwmRequest,
forgottenPasswordBean.getPresentableChallengeSet()
);
final boolean responsesPassed;
try {
responsesPassed = responseSet.test(crMap);
} catch (ChaiUnavailableException e) {
if (e.getCause() instanceof PwmUnrecoverableException) {
throw (PwmUnrecoverableException)e.getCause();
}
throw e;
}
// special case for nmas, clear out existing challenges and input fields.
if (!responsesPassed && responseSet instanceof NMASCrOperator.NMASCRResponseSet) {
forgottenPasswordBean.setPresentableChallengeSet(responseSet.getPresentableChallengeSet());
}
if (responsesPassed) {
LOGGER.debug(pwmRequest, "user '" + userIdentity + "' has supplied correct responses");
} else {
final String errorMsg = "incorrect response to one or more challenges";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE, errorMsg);
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
return ProcessStatus.Continue;
}
} catch (ChaiValidationException e) {
LOGGER.debug(pwmRequest, "chai validation error checking user responses: " + e.getMessage());
final ErrorInformation errorInformation = new ErrorInformation(PwmError.forChaiError(e.getErrorCode()));
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
return ProcessStatus.Continue;
}
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.CHALLENGE_RESPONSES);
return ProcessStatus.Continue;
}
@ActionHandler(action = "resendToken")
private ProcessStatus processResendToken(final PwmRequest pwmRequest)
throws PwmUnrecoverableException, IOException
{
{
final boolean resendEnabled = Boolean.parseBoolean(pwmRequest.getConfig().readAppProperty(AppProperty.TOKEN_RESEND_ENABLED));
if (!resendEnabled) {
final String errorMsg = "token resend is not enabled";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
}
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (!forgottenPasswordBean.getProgress().isTokenSent()) {
final String errorMsg = "attempt to resend token, but initial token has not yet been sent";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_SERVICE_NOT_AVAILABLE, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
}
{
LOGGER.trace(pwmRequest, "preparing to send a new token to user");
final long delayTime = Long.parseLong(pwmRequest.getConfig().readAppProperty(AppProperty.TOKEN_RESEND_DELAY_MS));
JavaHelper.pause(delayTime);
}
{
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getProgress().getTokenSendChoice();
ForgottenPasswordUtil.initializeAndSendToken(pwmRequest, userInfoBean, tokenSendMethod);
}
final RestResultBean restResultBean = new RestResultBean();
restResultBean.setSuccessMessage(LocaleHelper.getLocalizedMessage(Message.Success_TokenResend, pwmRequest));
pwmRequest.outputJsonResult(restResultBean);
return ProcessStatus.Halt;
}
@ActionHandler(action = "checkAttributes")
private ProcessStatus processCheckAttributes(final PwmRequest pwmRequest)
throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException
{
//final SessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (forgottenPasswordBean.getUserIdentity() == null) {
return ProcessStatus.Continue;
}
final UserIdentity userIdentity =forgottenPasswordBean.getUserIdentity();
try { // check attributes
final ChaiUser theUser = pwmRequest.getPwmApplication().getProxiedChaiUser(userIdentity);
final Locale userLocale = pwmRequest.getLocale();
final List<FormConfiguration> requiredAttributesForm = forgottenPasswordBean.getAttributeForm();
if (requiredAttributesForm.isEmpty()) {
return ProcessStatus.Continue;
}
final Map<FormConfiguration,String> formValues = FormUtility.readFormValuesFromRequest(
pwmRequest, requiredAttributesForm, userLocale);
for (final FormConfiguration paramConfig : formValues.keySet()) {
final String attrName = paramConfig.getName();
try {
if (theUser.compareStringAttribute(attrName, formValues.get(paramConfig))) {
LOGGER.trace(pwmRequest, "successful validation of ldap attribute value for '" + attrName + "'");
} else {
throw new PwmDataValidationException(new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE, "incorrect value for '" + attrName + "'", new String[]{attrName}));
}
} catch (ChaiOperationException e) {
LOGGER.error(pwmRequest, "error during param validation of '" + attrName + "', error: " + e.getMessage());
throw new PwmDataValidationException(new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE, "ldap error testing value for '" + attrName + "'", new String[]{attrName}));
}
}
forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.ATTRIBUTES);
} catch (PwmDataValidationException e) {
handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE,e.getErrorInformation().toDebugStr()));
}
return ProcessStatus.Continue;
}
@Override
protected void nextStep(final PwmRequest pwmRequest)
throws IOException, ServletException, PwmUnrecoverableException, ChaiUnavailableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final Configuration config = pwmRequest.getConfig();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
// check for identified user;
if (forgottenPasswordBean.getUserIdentity() == null) {
pwmRequest.addFormInfoToRequestAttr(PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM,false,false);
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_SEARCH);
return;
}
final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get(forgottenPasswordBean.getForgottenPasswordProfileID());
{
final Map<String, ForgottenPasswordProfile> profileIDList = pwmRequest.getConfig().getForgottenPasswordProfiles();
final String profileDebugMsg = forgottenPasswordProfile != null && profileIDList != null && profileIDList.size() > 1
? " profile=" + forgottenPasswordProfile.getIdentifier() + ", "
: "";
LOGGER.trace(pwmRequest, "entering forgotten password progress engine: "
+ profileDebugMsg
+ "flags=" + JsonUtil.serialize(recoveryFlags) + ", "
+ "progress=" + JsonUtil.serialize(progress));
}
if (forgottenPasswordProfile == null) {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_NO_PROFILE_ASSIGNED));
}
// check for previous authentication
if (recoveryFlags.getRequiredAuthMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH) || recoveryFlags.getOptionalAuthMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH)) {
if (!progress.getSatisfiedMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH)) {
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
final String userGuid = LdapOperationsHelper.readLdapGuidValue(pwmApplication, pwmRequest.getSessionLabel(), userIdentity, true);
if (ForgottenPasswordUtil.checkAuthRecord(pwmRequest, userGuid)) {
LOGGER.debug(pwmRequest, "marking " + IdentityVerificationMethod.PREVIOUS_AUTH + " method as satisfied");
progress.getSatisfiedMethods().add(IdentityVerificationMethod.PREVIOUS_AUTH);
}
}
}
// dispatch required auth methods.
for (final IdentityVerificationMethod method : recoveryFlags.getRequiredAuthMethods()) {
if (!progress.getSatisfiedMethods().contains(method)) {
forwardUserBasedOnRecoveryMethod(pwmRequest, method);
return;
}
}
// redirect if an verification method is in progress
if (progress.getInProgressVerificationMethod() != null) {
if (progress.getSatisfiedMethods().contains(progress.getInProgressVerificationMethod())) {
progress.setInProgressVerificationMethod(null);
} else {
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordOptionalPageView,"true");
forwardUserBasedOnRecoveryMethod(pwmRequest, progress.getInProgressVerificationMethod());
return;
}
}
// check if more optional methods required
if (recoveryFlags.getMinimumOptionalAuthMethods() > 0) {
final Set<IdentityVerificationMethod> satisfiedOptionalMethods = ForgottenPasswordUtil.figureSatisfiedOptionalAuthMethods(recoveryFlags,progress);
if (satisfiedOptionalMethods.size() < recoveryFlags.getMinimumOptionalAuthMethods()) {
final Set<IdentityVerificationMethod> remainingAvailableOptionalMethods = ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods(pwmRequest, forgottenPasswordBean);
if (remainingAvailableOptionalMethods.isEmpty()) {
final String errorMsg = "additional optional verification methods are needed, however all available optional verification methods have been satisified by user";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
LOGGER.error(pwmRequest, errorInformation);
throw new PwmUnrecoverableException(errorInformation);
} else {
if (remainingAvailableOptionalMethods.size() == 1) {
final IdentityVerificationMethod remainingMethod = remainingAvailableOptionalMethods.iterator().next();
LOGGER.debug(pwmRequest, "only 1 remaining available optional verification method, will redirect to " + remainingMethod.toString());
forwardUserBasedOnRecoveryMethod(pwmRequest, remainingMethod);
progress.setInProgressVerificationMethod(remainingMethod);
return;
}
}
processVerificationChoice(pwmRequest);
return;
}
}
if (progress.getSatisfiedMethods().isEmpty()) {
final String errorMsg = "forgotten password recovery sequence completed, but user has not actually satisfied any verification methods";
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
LOGGER.error(pwmRequest, errorInformation);
throw new PwmUnrecoverableException(errorInformation);
}
if (!forgottenPasswordBean.getProgress().isAllPassed()) {
forgottenPasswordBean.getProgress().setAllPassed(true);
StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_SUCCESSES);
}
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
try {
final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean(PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME);
if (enforceFromForgotten) {
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(forgottenPasswordBean.getUserIdentity());
PasswordUtility.checkIfPasswordWithinMinimumLifetime(
theUser,
pwmRequest.getSessionLabel(),
userInfoBean.getPasswordPolicy(),
userInfoBean.getPasswordLastModifiedTime(),
userInfoBean.getPasswordState()
);
}
} catch (PwmOperationalException e) {
throw new PwmUnrecoverableException(e.getErrorInformation());
}
LOGGER.trace(pwmRequest, "all recovery checks passed, proceeding to configured recovery action");
final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction(config, forgottenPasswordBean);
if (recoveryAction == RecoveryAction.SENDNEWPW || recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE) {
processSendNewPassword(pwmRequest);
return;
}
if (forgottenPasswordProfile.readSettingAsBoolean(PwmSetting.RECOVERY_ALLOW_UNLOCK)) {
final PasswordStatus passwordStatus = userInfoBean.getPasswordState();
if (!passwordStatus.isExpired() && !passwordStatus.isPreExpired()) {
try {
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(forgottenPasswordBean.getUserIdentity());
if (theUser.isPasswordLocked()) {
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_ACTION_CHOICE);
return;
}
} catch (ChaiOperationException e) {
LOGGER.error(pwmRequest, "chai operation error checking user lock status: " + e.getMessage());
}
}
}
this.executeResetPassword(pwmRequest);
}
private void executeUnlock(final PwmRequest pwmRequest)
throws IOException, ServletException, ChaiUnavailableException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
try {
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
theUser.unlockPassword();
// mark the event log
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
pwmApplication.getAuditManager().submit(AuditEvent.UNLOCK_PASSWORD, userInfoBean, pwmSession);
ForgottenPasswordUtil.sendUnlockNoticeEmail(pwmRequest, forgottenPasswordBean);
pwmRequest.getPwmResponse().forwardToSuccessPage(Message.Success_UnlockAccount);
} catch (ChaiOperationException e) {
final String errorMsg = "unable to unlock user " + userIdentity + " error: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNLOCK_FAILURE,errorMsg);
LOGGER.error(pwmSession, errorInformation.toDebugStr());
pwmRequest.respondWithError(errorInformation, true);
} finally {
clearForgottenPasswordBean(pwmRequest);
}
}
private void executeResetPassword(final PwmRequest pwmRequest)
throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
if (!forgottenPasswordBean.getProgress().isAllPassed()) {
return;
}
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userIdentity);
try { // try unlocking user
theUser.unlockPassword();
LOGGER.trace(pwmSession, "unlock account succeeded");
} catch (ChaiOperationException e) {
final String errorMsg = "unable to unlock user " + theUser.getEntryDN() + " error: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNLOCK_FAILURE,errorMsg);
LOGGER.error(pwmSession, errorInformation.toDebugStr());
}
try {
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
pwmApplication,
pwmSession,
PwmAuthenticationSource.FORGOTTEN_PASSWORD
);
sessionAuthenticator.authUserWithUnknownPassword(userIdentity,AuthenticationType.AUTH_FROM_PUBLIC_MODULE);
pwmSession.getLoginInfoBean().getAuthFlags().add(AuthenticationType.AUTH_FROM_PUBLIC_MODULE);
LOGGER.info(pwmSession, "user successfully supplied password recovery responses, forward to change password page: " + theUser.getEntryDN());
// mark the event log
pwmApplication.getAuditManager().submit(AuditEvent.RECOVER_PASSWORD, pwmSession.getUserInfoBean(),
pwmSession);
// add the post-forgotten password actions
addPostChangeAction(pwmRequest, userIdentity);
// mark user as requiring a new password.
pwmSession.getUserInfoBean().setRequiresNewPassword(true);
// redirect user to change password screen.
pwmRequest.sendRedirect(PwmServletDefinition.PublicChangePassword.servletUrlName());
} catch (PwmUnrecoverableException e) {
LOGGER.warn(pwmSession,
"unexpected error authenticating during forgotten password recovery process user: " + e.getMessage());
pwmRequest.respondWithError(e.getErrorInformation());
} finally {
clearForgottenPasswordBean(pwmRequest);
}
}
private static void processSendNewPassword(final PwmRequest pwmRequest)
throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final PwmSession pwmSession = pwmRequest.getPwmSession();
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get(forgottenPasswordBean.getForgottenPasswordProfileID());
final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction(pwmApplication.getConfig(), forgottenPasswordBean);
LOGGER.trace(pwmRequest,"beginning process to send new password to user");
if (!forgottenPasswordBean.getProgress().isAllPassed()) {
return;
}
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
final ChaiUser theUser = pwmRequest.getPwmApplication().getProxiedChaiUser(userIdentity);
try { // try unlocking user
theUser.unlockPassword();
LOGGER.trace(pwmRequest, "unlock account succeeded");
} catch (ChaiOperationException e) {
final String errorMsg = "unable to unlock user " + theUser.getEntryDN() + " error: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNLOCK_FAILURE,errorMsg);
LOGGER.error(pwmRequest.getPwmSession(), errorInformation.toDebugStr());
pwmRequest.respondWithError(errorInformation);
return;
}
try {
/*
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
pwmApplication,
pwmSession,
PwmAuthenticationSource.FORGOTTEN_PASSWORD
);
sessionAuthenticator.authUserWithUnknownPassword(userIdentity,AuthenticationType.AUTH_FROM_PUBLIC_MODULE);
*/
pwmSession.getLoginInfoBean().setAuthenticated(true);
pwmSession.getLoginInfoBean().getAuthFlags().add(AuthenticationType.AUTH_FROM_PUBLIC_MODULE);
pwmSession.getLoginInfoBean().setUserIdentity(userIdentity);
LOGGER.info(pwmRequest, "user successfully supplied password recovery responses, emailing new password to: " + theUser.getEntryDN());
// add post change actions
addPostChangeAction(pwmRequest, userIdentity);
// create newpassword
final PasswordData newPassword = RandomPasswordGenerator.createRandomPassword(pwmSession, pwmApplication);
// set the password
LOGGER.trace(pwmRequest.getPwmSession(), "setting user password to system generated random value");
PasswordUtility.setActorPassword(pwmSession, pwmApplication, newPassword);
if (recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE) {
LOGGER.debug(pwmSession, "marking user password as expired");
theUser.expirePassword();
}
// mark the event log
pwmApplication.getAuditManager().submit(AuditEvent.RECOVER_PASSWORD, pwmSession.getUserInfoBean(), pwmSession);
final MessageSendMethod messageSendMethod = forgottenPasswordProfile.readSettingAsEnum(PwmSetting.RECOVERY_SENDNEWPW_METHOD,MessageSendMethod.class);
// send email or SMS
final String toAddress = PasswordUtility.sendNewPassword(
pwmSession.getUserInfoBean(),
pwmApplication,
pwmSession.getSessionManager().getMacroMachine(pwmApplication),
newPassword,
pwmSession.getSessionStateBean().getLocale(),
messageSendMethod
);
pwmRequest.getPwmResponse().forwardToSuccessPage(Message.Success_PasswordSend, toAddress);
} catch (PwmException e) {
LOGGER.warn(pwmSession,"unexpected error setting new password during recovery process for user: " + e.getMessage());
pwmRequest.respondWithError(e.getErrorInformation());
} catch (ChaiOperationException e) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN,"unexpected ldap error while processing recovery action " + recoveryAction + ", error: " + e.getMessage());
LOGGER.warn(pwmSession,errorInformation.toDebugStr());
pwmRequest.respondWithError(errorInformation);
} finally {
clearForgottenPasswordBean(pwmRequest);
pwmSession.unauthenticateUser(pwmRequest);
pwmSession.getSessionStateBean().setPasswordModified(false);
}
}
private static List<FormConfiguration> figureAttributeForm(
final PwmApplication pwmApplication,
final ForgottenPasswordProfile forgottenPasswordProfile,
final SessionLabel sessionLabel,
final UserIdentity userIdentity
)
throws ChaiUnavailableException, PwmOperationalException, PwmUnrecoverableException
{
final List<FormConfiguration> requiredAttributesForm = forgottenPasswordProfile.readSettingAsForm(PwmSetting.RECOVERY_ATTRIBUTE_FORM);
if (requiredAttributesForm.isEmpty()) {
return requiredAttributesForm;
}
final UserDataReader userDataReader = LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity);
final List<FormConfiguration> returnList = new ArrayList<>();
for (final FormConfiguration formItem : requiredAttributesForm) {
if (formItem.isRequired()) {
returnList.add(formItem);
} else {
try {
final String currentValue = userDataReader.readStringAttribute(formItem.getName());
if (currentValue != null && currentValue.length() > 0) {
returnList.add(formItem);
} else {
LOGGER.trace(sessionLabel, "excluding optional required attribute(" + formItem.getName() + "), user has no value");
}
} catch (ChaiOperationException e) {
throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, "unexpected error reading value for attribute " + formItem.getName()));
}
}
}
if (returnList.isEmpty()) {
throw new PwmOperationalException(new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, "user has no values for any optional attribute"));
}
return returnList;
}
private static void addPostChangeAction(
final PwmRequest pwmRequest,
final UserIdentity userIdentity
)
{
final PostChangePasswordAction postAction = new PostChangePasswordAction() {
@Override
public String getLabel() {
return "Forgotten Password Post Actions";
}
@Override
public boolean doAction(final PwmSession pwmSession, final String newPassword)
throws PwmUnrecoverableException {
try {
{ // execute configured actions
final ChaiUser proxiedUser = pwmRequest.getPwmApplication().getProxiedChaiUser(userIdentity);
LOGGER.debug(pwmSession, "executing post-forgotten password configured actions to user " + proxiedUser.getEntryDN());
final List<ActionConfiguration> configValues = pwmRequest.getConfig().readSettingAsAction(PwmSetting.FORGOTTEN_USER_POST_ACTIONS);
final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings(pwmRequest.getPwmApplication(),userIdentity)
.setMacroMachine(pwmSession.getSessionManager().getMacroMachine(pwmRequest.getPwmApplication()))
.setExpandPwmMacros(true)
.createActionExecutor();
actionExecutor.executeActions(configValues, pwmSession);
}
} catch (PwmOperationalException e) {
final ErrorInformation info = new ErrorInformation(PwmError.ERROR_UNKNOWN, e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues());
final PwmUnrecoverableException newException = new PwmUnrecoverableException(info);
newException.initCause(e);
throw newException;
} catch (ChaiUnavailableException e) {
final String errorMsg = "unable to reach ldap server while writing post-forgotten password attributes: " + e.getMessage();
final ErrorInformation info = new ErrorInformation(PwmError.ERROR_ACTIVATION_FAILURE, errorMsg);
final PwmUnrecoverableException newException = new PwmUnrecoverableException(info);
newException.initCause(e);
throw newException;
}
return true;
}
};
pwmRequest.getPwmSession().getUserSessionDataCacheBean().addPostChangePasswordActions("forgottenPasswordPostActions", postAction);
}
private static void initForgottenPasswordBean(
final PwmRequest pwmRequest,
final UserIdentity userIdentity,
final ForgottenPasswordBean forgottenPasswordBean
)
throws PwmUnrecoverableException, PwmOperationalException
{
final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
final Locale locale = pwmRequest.getLocale();
final SessionLabel sessionLabel = pwmRequest.getSessionLabel();
forgottenPasswordBean.setUserIdentity(userIdentity);
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
final String forgottenProfileID = ProfileUtility.discoverProfileIDforUser(pwmApplication, sessionLabel, userIdentity, ProfileType.ForgottenPassword);
if (forgottenProfileID == null || forgottenProfileID.isEmpty()) {
throw new PwmUnrecoverableException(PwmError.ERROR_NO_PROFILE_ASSIGNED.toInfo());
}
forgottenPasswordBean.setForgottenPasswordProfileID(forgottenProfileID);
final ForgottenPasswordProfile forgottenPasswordProfile = pwmApplication.getConfig().getForgottenPasswordProfiles().get(forgottenProfileID);
final ForgottenPasswordBean.RecoveryFlags recoveryFlags = calculateRecoveryFlags(
pwmApplication,
forgottenProfileID
);
final ChallengeSet challengeSet;
if (recoveryFlags.getRequiredAuthMethods().contains(IdentityVerificationMethod.CHALLENGE_RESPONSES)
|| recoveryFlags.getOptionalAuthMethods().contains(IdentityVerificationMethod.CHALLENGE_RESPONSES)) {
final ResponseSet responseSet;
try {
final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userInfoBean.getUserIdentity());
responseSet = pwmApplication.getCrService().readUserResponseSet(
sessionLabel,
userInfoBean.getUserIdentity(),
theUser
);
challengeSet = responseSet == null ? null : responseSet.getPresentableChallengeSet();
} catch (ChaiValidationException e) {
final String errorMsg = "unable to determine presentable challengeSet for stored responses: " + e.getMessage();
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_NO_CHALLENGES, errorMsg);
throw new PwmUnrecoverableException(errorInformation);
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
} else {
challengeSet = null;
}
if (!recoveryFlags.isAllowWhenLdapIntruderLocked()) {
try {
final ChaiUser chaiUser = pwmApplication.getProxiedChaiUser(userInfoBean.getUserIdentity());
if (chaiUser.isPasswordLocked()) {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_INTRUDER_LDAP));
}
} catch (ChaiOperationException e) {
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN,
"error checking user '" + userInfoBean.getUserIdentity() + "' ldap intruder lock status: " + e.getMessage());
LOGGER.error(sessionLabel, errorInformation);
throw new PwmUnrecoverableException(errorInformation);
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
}
final List<FormConfiguration> attributeForm;
try {
attributeForm = figureAttributeForm(pwmApplication, forgottenPasswordProfile, sessionLabel, userIdentity);
} catch (ChaiUnavailableException e) {
throw new PwmUnrecoverableException(PwmError.forChaiError(e.getErrorCode()));
}
forgottenPasswordBean.setUserLocale(locale);
forgottenPasswordBean.setPresentableChallengeSet(challengeSet);
forgottenPasswordBean.setAttributeForm(attributeForm);
forgottenPasswordBean.setRecoveryFlags(recoveryFlags);
forgottenPasswordBean.setProgress(new ForgottenPasswordBean.Progress());
for (final IdentityVerificationMethod recoveryVerificationMethods : recoveryFlags.getRequiredAuthMethods()) {
ForgottenPasswordUtil.verifyRequirementsForAuthMethod(pwmRequest, forgottenPasswordBean, recoveryVerificationMethods);
}
}
private static ForgottenPasswordBean.RecoveryFlags calculateRecoveryFlags(
final PwmApplication pwmApplication,
final String forgottenPasswordProfileID
) {
final Configuration config = pwmApplication.getConfig();
final ForgottenPasswordProfile forgottenPasswordProfile = config.getForgottenPasswordProfiles().get(forgottenPasswordProfileID);
final MessageSendMethod tokenSendMethod = config.getForgottenPasswordProfiles().get(forgottenPasswordProfileID).readSettingAsEnum(PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class);
final Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods = forgottenPasswordProfile.requiredRecoveryAuthenticationMethods();
final Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods = forgottenPasswordProfile.optionalRecoveryAuthenticationMethods();
final int minimumOptionalRecoveryAuthMethods = forgottenPasswordProfile.getMinOptionalRequired();
final boolean allowWhenLdapIntruderLocked = forgottenPasswordProfile.readSettingAsBoolean(PwmSetting.RECOVERY_ALLOW_WHEN_LOCKED);
return new ForgottenPasswordBean.RecoveryFlags(
requiredRecoveryVerificationMethods,
optionalRecoveryVerificationMethods,
minimumOptionalRecoveryAuthMethods,
allowWhenLdapIntruderLocked,
tokenSendMethod
);
}
private void handleUserVerificationBadAttempt(
final PwmRequest pwmRequest,
final ForgottenPasswordBean forgottenPasswordBean,
final ErrorInformation errorInformation
)
throws PwmUnrecoverableException
{
LOGGER.debug(pwmRequest, errorInformation);
setLastError(pwmRequest, errorInformation);
final UserIdentity userIdentity = forgottenPasswordBean == null
? null
: forgottenPasswordBean.getUserIdentity();
if (userIdentity != null) {
final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
pwmRequest.getPwmApplication(),
pwmRequest.getPwmSession(),
PwmAuthenticationSource.FORGOTTEN_PASSWORD
);
sessionAuthenticator.simulateBadPassword(userIdentity);
pwmRequest.getPwmApplication().getIntruderManager().convenience().markUserIdentity(userIdentity,
pwmRequest.getPwmSession());
pwmRequest.getPwmApplication().getIntruderManager().convenience().markAddressAndSession(
pwmRequest.getPwmSession());
}
StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_FAILURES);
}
private void checkForLocaleSwitch(final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean)
throws PwmUnrecoverableException, IOException, ServletException
{
if (forgottenPasswordBean.getUserIdentity() == null || forgottenPasswordBean.getUserLocale() == null) {
return;
}
if (forgottenPasswordBean.getUserLocale().equals(pwmRequest.getLocale())) {
return;
}
LOGGER.debug(pwmRequest, "user initiated forgotten password recovery using '" + forgottenPasswordBean.getUserLocale() + "' locale, but current request locale is now '"
+ pwmRequest.getLocale() + "', thus, the user progress will be restart and user data will be re-read using current locale");
try {
initForgottenPasswordBean(
pwmRequest,
forgottenPasswordBean.getUserIdentity(),
forgottenPasswordBean
);
} catch (PwmOperationalException e) {
clearForgottenPasswordBean(pwmRequest);
final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, "unexpected error while re-loading user data due to locale change: " + e.getErrorInformation().toDebugStr());
LOGGER.error(pwmRequest, errorInformation.toDebugStr());
setLastError(pwmRequest, errorInformation);
}
}
private void forwardUserBasedOnRecoveryMethod(
final PwmRequest pwmRequest,
final IdentityVerificationMethod method
)
throws ServletException, PwmUnrecoverableException, IOException
{
LOGGER.debug(pwmRequest,"attempting to forward request to handle verification method " + method.toString());
final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
ForgottenPasswordUtil.verifyRequirementsForAuthMethod(pwmRequest,forgottenPasswordBean,method);
switch (method) {
case PREVIOUS_AUTH: {
throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"previous authentication is required, but user has not previously authenticated"));
}
case ATTRIBUTES: {
pwmRequest.addFormInfoToRequestAttr(forgottenPasswordBean.getAttributeForm(), Collections.emptyMap(), false, false);
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_ATTRIBUTES);
}
break;
case CHALLENGE_RESPONSES: {
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordChallengeSet, forgottenPasswordBean.getPresentableChallengeSet());
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_RESPONSES);
}
break;
case OTP: {
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordUserInfo, userInfoBean);
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_ENTER_OTP);
}
break;
case TOKEN: {
final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
if (progress.getTokenSendChoice() == null) {
progress.setTokenSendChoice(ForgottenPasswordUtil.figureTokenSendPreference(pwmRequest, forgottenPasswordBean));
}
if (progress.getTokenSendChoice() == MessageSendMethod.CHOICE_SMS_EMAIL) {
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE);
return;
}
if (!progress.isTokenSent()) {
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
final String destAddress = ForgottenPasswordUtil.initializeAndSendToken(pwmRequest, userInfoBean, progress.getTokenSendChoice());
progress.setTokenSentAddress(destAddress);
progress.setTokenSent(true);
}
if (!progress.getSatisfiedMethods().contains(IdentityVerificationMethod.TOKEN)) {
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_ENTER_TOKEN);
return;
}
}
break;
case REMOTE_RESPONSES: {
final UserInfoBean userInfoBean = ForgottenPasswordUtil.readUserInfoBean(pwmRequest, forgottenPasswordBean);
final VerificationMethodSystem remoteMethod;
if (forgottenPasswordBean.getProgress().getRemoteRecoveryMethod() == null) {
remoteMethod = new RemoteVerificationMethod();
remoteMethod.init(
pwmRequest.getPwmApplication(),
userInfoBean,
pwmRequest.getSessionLabel(),
pwmRequest.getLocale()
);
forgottenPasswordBean.getProgress().setRemoteRecoveryMethod(remoteMethod);
} else {
remoteMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
}
final List<VerificationMethodSystem.UserPrompt> prompts = remoteMethod.getCurrentPrompts();
final String displayInstructions = remoteMethod.getCurrentDisplayInstructions();
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordPrompts, new ArrayList<>(prompts));
pwmRequest.setAttribute(PwmRequestAttribute.ForgottenPasswordInstructions, displayInstructions);
pwmRequest.forwardToJsp(JspUrl.RECOVER_PASSWORD_REMOTE);
}
break;
case OAUTH:
forgottenPasswordBean.getProgress().setInProgressVerificationMethod(IdentityVerificationMethod.OAUTH);
final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get(forgottenPasswordBean.getForgottenPasswordProfileID());
final OAuthSettings oAuthSettings = OAuthSettings.forForgottenPassword(forgottenPasswordProfile);
final OAuthMachine oAuthMachine = new OAuthMachine(oAuthSettings);
pwmRequest.getPwmApplication().getSessionStateService().saveSessionBeans(pwmRequest);
final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
oAuthMachine.redirectUserToOAuthServer(pwmRequest, null, userIdentity, forgottenPasswordProfile.getIdentifier());
break;
default:
throw new UnsupportedOperationException("unexpected method during forward: " + method.toString());
}
}
}