瀏覽代碼

support for bogus user forgotten password policy

Jason Rivard 7 年之前
父節點
當前提交
bfa3c6dca5

+ 0 - 57
server/src/build/assembly/release-bundle.xml

@@ -1,57 +0,0 @@
-<!--
-  ~ Password Management Servlets (PWM)
-  ~ http://www.pwm-project.org
-  ~
-  ~ Copyright (c) 2006-2009 Novell, Inc.
-  ~ Copyright (c) 2009-2018 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
-  -->
-
-<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
-
-    <id>release-bundle</id>
-    <formats>
-        <format>zip</format>
-    </formats>
-    <includeBaseDirectory>false</includeBaseDirectory>
-    <files>
-        <file>
-            <source>target/${project.build.finalName}.war</source>
-            <destName>pwm.war</destName>
-        </file>
-        <file>
-            <source>${project.basedir}/../LICENSE</source>
-        </file>
-        <file>
-            <source>${project.basedir}/pom.xml</source>
-        </file>
-    </files>
-    <fileSets>
-        <fileSet>
-            <directory>${project.basedir}/src</directory>
-            <includes>
-                <include>*/**</include>
-            </includes>
-        </fileSet>
-        <fileSet>
-            <directory>${project.basedir}/supplemental</directory>
-            <includes>
-                <include>*/**</include>
-            </includes>
-        </fileSet>
-    </fileSets>
-</assembly>

+ 8 - 4
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -726,6 +726,8 @@ public enum PwmSetting
             "recovery.postActions", PwmSettingSyntax.ACTION, PwmSettingCategory.RECOVERY_SETTINGS ),
     CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME(
             "challenge.enforceMinimumPasswordLifetime", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
+    RECOVERY_BOGUS_USER_ENABLE(
+            "recovery.bogus.user.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
 
     // recovery profile
     RECOVERY_PROFILE_LIST(
@@ -736,18 +738,20 @@ public enum PwmSetting
             "recovery.verificationMethods", PwmSettingSyntax.VERIFICATION_METHOD, PwmSettingCategory.RECOVERY_DEF ),
     RECOVERY_TOKEN_SEND_METHOD(
             "challenge.token.sendMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_DEF ),
-    RECOVERY_ALLOW_UNLOCK(
-            "challenge.allowUnlock", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_DEF ),
     RECOVERY_ACTION(
             "recovery.action", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_DEF ),
     RECOVERY_SENDNEWPW_METHOD(
             "recovery.sendNewPassword.sendMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_DEF ),
     RECOVERY_ATTRIBUTE_FORM(
             "challenge.requiredAttributes", PwmSettingSyntax.FORM, PwmSettingCategory.RECOVERY_DEF ),
+
+    // recover options
+    RECOVERY_ALLOW_UNLOCK(
+            "challenge.allowUnlock", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
     RECOVERY_ALLOW_WHEN_LOCKED(
-            "recovery.allowWhenLocked", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_DEF ),
+            "recovery.allowWhenLocked", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
     TOKEN_RESEND_ENABLE(
-            "recovery.token.resend.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_DEF ),
+            "recovery.token.resend.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
 
     // recovery oauth
     RECOVERY_OAUTH_ID_LOGIN_URL(

+ 1 - 0
server/src/main/java/password/pwm/config/PwmSettingCategory.java

@@ -140,6 +140,7 @@ public enum PwmSettingCategory
     RECOVERY_PROFILE( RECOVERY ),
 
     RECOVERY_DEF( RECOVERY_PROFILE ),
+    RECOVERY_OPTIONS( RECOVERY_PROFILE ),
     RECOVERY_OAUTH( RECOVERY_PROFILE ),
 
     FORGOTTEN_USERNAME( MODULES_PUBLIC ),

+ 30 - 75
server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

@@ -22,6 +22,9 @@
 
 package password.pwm.config.value.data;
 
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
 import lombok.Getter;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
@@ -53,6 +56,8 @@ import java.util.regex.PatternSyntaxException;
  * @author Jason D. Rivard
  */
 @Getter
+@Builder
+@AllArgsConstructor( access = AccessLevel.PRIVATE )
 public class FormConfiguration implements Serializable
 {
 
@@ -80,24 +85,49 @@ public class FormConfiguration implements Serializable
     {
         ldap,
         remote,
+        bogus,
     }
 
+    @Builder.Default
     private String name = "";
+
+    @Builder.Default
     private int minimumLength;
+
+    @Builder.Default
     private int maximumLength;
+
+    @Builder.Default
     private Type type = Type.text;
+
+    @Builder.Default
     private Source source = Source.ldap;
+
     private boolean required;
     private boolean confirmationRequired;
     private boolean readonly;
     private boolean unique;
     private boolean multivalue;
+
+    @Builder.Default
     private Map<String, String> labels = Collections.singletonMap( "", "" );
+
+    @Builder.Default
     private Map<String, String> regexErrors = Collections.singletonMap( "", "" );
+
+    @Builder.Default
     private Map<String, String> description = Collections.singletonMap( "", "" );
+
+    @Builder.Default
     private String regex = "";
+
+    @Builder.Default
     private String placeholder = "";
+
+    @Builder.Default
     private String javascript = "";
+
+    @Builder.Default
     private Map<String, String> selectOptions = Collections.emptyMap();
 
 
@@ -220,21 +250,11 @@ public class FormConfiguration implements Serializable
         regexErrors = Collections.singletonMap( "", "" );
     }
 
-    public String getName( )
-    {
-        return name;
-    }
-
     public String getLabel( final Locale locale )
     {
         return LocaleHelper.resolveStringKeyLocaleMap( locale, labels );
     }
 
-    public Map<String, String> getLabelLocaleMap( )
-    {
-        return Collections.unmodifiableMap( this.labels );
-    }
-
     public String getRegexError( final Locale locale )
     {
         return LocaleHelper.resolveStringKeyLocaleMap( locale, regexErrors );
@@ -245,71 +265,6 @@ public class FormConfiguration implements Serializable
         return LocaleHelper.resolveStringKeyLocaleMap( locale, description );
     }
 
-    public Map<String, String> getLabelDescriptionLocaleMap( )
-    {
-        return Collections.unmodifiableMap( this.description );
-    }
-
-    public int getMaximumLength( )
-    {
-        return maximumLength;
-    }
-
-    public int getMinimumLength( )
-    {
-        return minimumLength;
-    }
-
-    public Type getType( )
-    {
-        return type;
-    }
-
-    public boolean isConfirmationRequired( )
-    {
-        return confirmationRequired;
-    }
-
-    public boolean isRequired( )
-    {
-        return required;
-    }
-
-    public boolean isReadonly( )
-    {
-        return readonly;
-    }
-
-    public boolean isUnique( )
-    {
-        return unique;
-    }
-
-    public boolean isMultivalue( )
-    {
-        return multivalue;
-    }
-
-    public String getRegex( )
-    {
-        return regex;
-    }
-
-    public String getPlaceholder( )
-    {
-        return placeholder;
-    }
-
-    public String getJavascript( )
-    {
-        return javascript;
-    }
-
-    public Map<String, String> getSelectOptions( )
-    {
-        return Collections.unmodifiableMap( selectOptions );
-    }
-
     public boolean equals( final Object o )
     {
         if ( this == o )

+ 2 - 0
server/src/main/java/password/pwm/error/PwmError.java

@@ -297,6 +297,8 @@ public enum PwmError
             5085, "Error_EmailSendFailure", null, ErrorFlag.Permanent ),
     ERROR_PASSWORD_ONLY_BAD(
             5089, "Error_PasswordOnlyBad", null ),
+    ERROR_RECOVERY_SEQUENCE_INCOMPLETE(
+            5090, "Error_RecoverySequenceIncomplete", null ),
 
     ERROR_REMOTE_ERROR_VALUE(
             6000, "Error_RemoteErrorValue", null, ErrorFlag.Permanent ),

+ 7 - 0
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -39,6 +39,7 @@ import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -66,9 +67,15 @@ public class ForgottenPasswordBean extends PwmSessionBean
     @SerializedName( "f" )
     private RecoveryFlags recoveryFlags = new RecoveryFlags();
 
+    @SerializedName( "b" )
+    private boolean bogusUser;
+
     @SerializedName( "fp" )
     private String forgottenPasswordProfileID;
 
+    @SerializedName( "lf" )
+    private Map<String, String> userSearchValues;
+
     @Data
     public static class Progress implements Serializable
     {

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/ControlledPwmServlet.java

@@ -133,7 +133,7 @@ public abstract class ControlledPwmServlet extends AbstractPwmServlet implements
                 final String msg = "unexpected error during action handler for '"
                         + this.getClass().getName()
                         + ":" + action + "', error: " + cause.getMessage();
-                LOGGER.error( pwmRequest, msg );
+                LOGGER.error( pwmRequest, msg, e.getCause() );
                 throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_UNKNOWN, msg ) );
             }
             LOGGER.error( "uncaused invocation error: " + e.getMessage(), e );

+ 81 - 213
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -24,7 +24,6 @@ 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;
@@ -35,7 +34,6 @@ import password.pwm.PwmConstants;
 import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.LoginInfoBean;
 import password.pwm.bean.PasswordStatus;
-import password.pwm.bean.SessionLabel;
 import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
@@ -44,8 +42,6 @@ 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.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
@@ -77,7 +73,6 @@ 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;
@@ -92,6 +87,7 @@ 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.util.secure.PwmRandom;
 import password.pwm.ws.server.RestResultBean;
 
 import javax.servlet.ServletException;
@@ -380,6 +376,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final String contextParam = pwmRequest.readParameterAsString( PwmConstants.PARAM_CONTEXT );
         final String ldapProfile = pwmRequest.readParameterAsString( PwmConstants.PARAM_LDAP_PROFILE );
 
+        final boolean bogusUserModeEnabled = pwmRequest.getConfig().readSettingAsBoolean( PwmSetting.RECOVERY_BOGUS_USER_ENABLE );
+
         // clear the bean
         clearForgottenPasswordBean( pwmRequest );
 
@@ -440,31 +438,42 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             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;
+                throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER ) );
             }
 
             AuthenticationUtility.checkIfUserEligibleToAuthentication( pwmApplication, userIdentity );
 
             final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-            initForgottenPasswordBean( pwmRequest, userIdentity, forgottenPasswordBean );
+            ForgottenPasswordUtil.initForgottenPasswordBean( pwmRequest, userIdentity, forgottenPasswordBean );
 
             // clear intruder search values
             pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
+
+            return ProcessStatus.Continue;
         }
         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 );
+            if ( e.getError() != PwmError.ERROR_CANT_MATCH_USER || !bogusUserModeEnabled )
+            {
+                final ErrorInformation errorInfo = new ErrorInformation(
+                        PwmError.ERROR_RESPONSES_NORESPONSES,
+                        e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
+                );
+                pwmApplication.getStatisticsManager().incrementValue( Statistic.RECOVERY_FAILURES );
+
+                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+                pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+
+                LOGGER.debug( pwmSession, errorInfo.toDebugStr() );
+                setLastError( pwmRequest, errorInfo );
+                return ProcessStatus.Continue;
+            }
+        }
 
-            LOGGER.debug( pwmSession, errorInfo.toDebugStr() );
-            setLastError( pwmRequest, errorInfo );
+        if ( bogusUserModeEnabled )
+        {
+            ForgottenPasswordUtil.initBogusForgottenPasswordBean( pwmRequest );
+            forgottenPasswordBean( pwmRequest ).setUserSearchValues( FormUtility.asStringMap( formValues ) );
         }
 
         return ProcessStatus.Continue;
@@ -492,7 +501,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                 if ( forgottenPasswordBean.getUserIdentity() == null )
                 {
                     // clean session, user supplied token (clicked email, etc) and this is first request
-                    initForgottenPasswordBean(
+                    ForgottenPasswordUtil.initForgottenPasswordBean(
                             pwmRequest,
                             tokenPayload.getUserIdentity(),
                             forgottenPasswordBean
@@ -808,6 +817,32 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         //final SessionStateBean ssBean = pwmRequest.getPwmSession().getSessionStateBean();
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
 
+        if ( forgottenPasswordBean.isBogusUser() )
+        {
+            final FormConfiguration formConfiguration = forgottenPasswordBean.getAttributeForm().iterator().next();
+
+            // add a bit of jitter to pretend like we're checking a data source
+            JavaHelper.pause( 300 + PwmRandom.getInstance().nextInt( 700 ) );
+
+            if ( forgottenPasswordBean.getUserSearchValues() != null )
+            {
+                final List<FormConfiguration> formConfigurations = pwmRequest.getConfig().readSettingAsForm( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM );
+                final Map<FormConfiguration, String> formMap = FormUtility.asFormConfigurationMap( formConfigurations, forgottenPasswordBean.getUserSearchValues() );
+                pwmRequest.getPwmApplication().getIntruderManager().convenience().markAttributes( formMap, pwmRequest.getPwmSession() );
+            }
+
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE,
+                    "incorrect value for attribute '" + formConfiguration.getName() + "'", new String[]
+                    {
+                            formConfiguration.getLabel( pwmRequest.getLocale() ),
+                    }
+            );
+
+            forgottenPasswordBean.getProgress().setInProgressVerificationMethod( IdentityVerificationMethod.ATTRIBUTES );
+            setLastError( pwmRequest, errorInformation );
+            return ProcessStatus.Continue;
+        }
+
         if ( forgottenPasswordBean.getUserIdentity() == null )
         {
             return ProcessStatus.Continue;
@@ -832,8 +867,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
             {
-                final FormConfiguration paramConfig = entry.getKey();
-                final String attrName = paramConfig.getName();
+                final FormConfiguration formConfiguration = entry.getKey();
+                final String attrName = formConfiguration.getName();
 
                 try
                 {
@@ -845,7 +880,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                     {
                         throw new PwmDataValidationException( new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE, "incorrect value for '" + attrName + "'", new String[]
                                 {
-                                        attrName,
+                                        formConfiguration.getLabel( pwmRequest.getLocale() ),
                                 }
                         ) );
                     }
@@ -855,7 +890,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                     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,
+                                    formConfiguration.getLabel( pwmRequest.getLocale() ),
                             }
                     ) );
                 }
@@ -885,7 +920,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
 
         // check for identified user;
-        if ( forgottenPasswordBean.getUserIdentity() == null )
+        if ( forgottenPasswordBean.getUserIdentity() == null && !forgottenPasswordBean.isBogusUser() )
         {
             pwmRequest.addFormInfoToRequestAttr( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM, false, false );
             pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_SEARCH );
@@ -989,11 +1024,26 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         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 );
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_RECOVERY_SEQUENCE_INCOMPLETE, errorMsg );
             LOGGER.error( pwmRequest, errorInformation );
+            clearForgottenPasswordBean( pwmRequest );
             throw new PwmUnrecoverableException( errorInformation );
         }
 
+        {
+            final int satisfiedMethods = progress.getSatisfiedMethods().size();
+            final int totalMethodsNeeded = recoveryFlags.getRequiredAuthMethods().size() + recoveryFlags.getMinimumOptionalAuthMethods();
+            if ( satisfiedMethods < totalMethodsNeeded )
+            {
+                final String errorMsg = "forgotten password recovery sequence completed " + satisfiedMethods + " methods, "
+                        + " but policy requires a total of " + totalMethodsNeeded + " methods";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_RECOVERY_SEQUENCE_INCOMPLETE, errorMsg );
+                LOGGER.error( pwmRequest, errorInformation );
+                clearForgottenPasswordBean( pwmRequest );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+        }
+
         if ( !forgottenPasswordBean.getProgress().isAllPassed() )
         {
             forgottenPasswordBean.getProgress().setAllPassed( true );
@@ -1157,57 +1207,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     }
 
 
-    private static List<FormConfiguration> figureAttributeForm(
-            final ForgottenPasswordProfile forgottenPasswordProfile,
-            final ForgottenPasswordBean forgottenPasswordBean,
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmOperationalException, PwmUnrecoverableException
-    {
-        final List<FormConfiguration> requiredAttributesForm = forgottenPasswordProfile.readSettingAsForm( PwmSetting.RECOVERY_ATTRIBUTE_FORM );
-        if ( requiredAttributesForm.isEmpty() )
-        {
-            return requiredAttributesForm;
-        }
-
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        final List<FormConfiguration> returnList = new ArrayList<>();
-        for ( final FormConfiguration formItem : requiredAttributesForm )
-        {
-            if ( formItem.isRequired() )
-            {
-                returnList.add( formItem );
-            }
-            else
-            {
-                try
-                {
-                    final String currentValue = userInfo.readStringAttribute( formItem.getName() );
-                    if ( currentValue != null && currentValue.length() > 0 )
-                    {
-                        returnList.add( formItem );
-                    }
-                    else
-                    {
-                        LOGGER.trace( pwmRequest, "excluding optional required attribute(" + formItem.getName() + "), user has no value" );
-                    }
-                }
-                catch ( PwmUnrecoverableException 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;
-    }
-
     static void addPostChangeAction(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity
@@ -1282,140 +1281,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     }
 
 
-    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 UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( 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 = forgottenPasswordProfile( pwmRequest );
-
-        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( userInfo.getUserIdentity() );
-                responseSet = pwmApplication.getCrService().readUserResponseSet(
-                        sessionLabel,
-                        userInfo.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( userInfo.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 '" + userInfo.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( forgottenPasswordProfile, forgottenPasswordBean, pwmRequest, 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(
-                allowWhenLdapIntruderLocked,
-                requiredRecoveryVerificationMethods,
-                optionalRecoveryVerificationMethods,
-                minimumOptionalRecoveryAuthMethods,
-                tokenSendMethod
-        );
-    }
-
     private void handleUserVerificationBadAttempt(
             final PwmRequest pwmRequest,
             final ForgottenPasswordBean forgottenPasswordBean,
@@ -1429,6 +1294,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final UserIdentity userIdentity = forgottenPasswordBean == null
                 ? null
                 : forgottenPasswordBean.getUserIdentity();
+
         if ( userIdentity != null )
         {
             final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator(
@@ -1439,9 +1305,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             sessionAuthenticator.simulateBadPassword( userIdentity );
             pwmRequest.getPwmApplication().getIntruderManager().convenience().markUserIdentity( userIdentity,
                     pwmRequest.getPwmSession() );
-            pwmRequest.getPwmApplication().getIntruderManager().convenience().markAddressAndSession(
-                    pwmRequest.getPwmSession() );
         }
+
+        pwmRequest.getPwmApplication().getIntruderManager().convenience().markAddressAndSession(
+                pwmRequest.getPwmSession() );
+
         StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_FAILURES );
     }
 
@@ -1463,7 +1331,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         try
         {
-            initForgottenPasswordBean(
+            ForgottenPasswordUtil.initForgottenPasswordBean(
                     pwmRequest,
                     forgottenPasswordBean.getUserIdentity(),
                     forgottenPasswordBean

+ 242 - 0
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -34,6 +34,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.SessionLabel;
 import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
@@ -42,6 +43,8 @@ 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.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
@@ -75,6 +78,7 @@ import password.pwm.ws.client.rest.RestTokenDataClient;
 import javax.servlet.ServletException;
 import java.io.IOException;
 import java.time.Instant;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -686,4 +690,242 @@ class ForgottenPasswordUtil
             pwmRequest.getPwmSession().getSessionStateBean().setPasswordModified( false );
         }
     }
+
+    static void initBogusForgottenPasswordBean( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final ForgottenPasswordBean forgottenPasswordBean = ForgottenPasswordServlet.forgottenPasswordBean( pwmRequest );
+        forgottenPasswordBean.setUserIdentity( null );
+        forgottenPasswordBean.setPresentableChallengeSet( null );
+
+
+        final List<Challenge> challengeList = new ArrayList<>( );
+        {
+            final String firstProfile = pwmRequest.getConfig().getChallengeProfileIDs().iterator().next();
+            final ChallengeSet challengeSet = pwmRequest.getConfig().getChallengeProfile( firstProfile, PwmConstants.DEFAULT_LOCALE ).getChallengeSet();
+            challengeList.addAll( challengeSet.getRequiredChallenges() );
+            for ( int i = 0; i < challengeSet.getMinRandomRequired(); i++ )
+            {
+                challengeList.add( challengeSet.getRandomChallenges().get( i ) );
+            }
+        }
+
+        final List<FormConfiguration> formData = new ArrayList<>(  );
+        {
+            int counter = 0;
+            for ( Challenge challenge: challengeList )
+            {
+                final FormConfiguration formConfiguration = FormConfiguration.builder()
+                        .name( "challenge" + counter++ )
+                        .type( FormConfiguration.Type.text )
+                        .labels( Collections.singletonMap( "", challenge.getChallengeText() ) )
+                        .minimumLength( challenge.getMinLength() )
+                        .maximumLength( challenge.getMaxLength() )
+                        .source( FormConfiguration.Source.bogus )
+                        .build();
+                formData.add( formConfiguration );
+            }
+        }
+        forgottenPasswordBean.setAttributeForm( formData );
+        forgottenPasswordBean.setBogusUser( true );
+        {
+            final String profileID = pwmRequest.getConfig().getForgottenPasswordProfiles().keySet().iterator().next();
+            forgottenPasswordBean.setForgottenPasswordProfileID( profileID  );
+        }
+
+        final ForgottenPasswordBean.RecoveryFlags recoveryFlags = new ForgottenPasswordBean.RecoveryFlags(
+                false,
+                Collections.singleton( IdentityVerificationMethod.ATTRIBUTES ),
+                Collections.emptySet(),
+                0,
+                MessageSendMethod.NONE
+        );
+
+        forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
+    }
+
+    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 UserInfo userInfo = readUserInfo( 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 = ForgottenPasswordServlet.forgottenPasswordProfile( pwmRequest );
+
+        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( userInfo.getUserIdentity() );
+                responseSet = pwmApplication.getCrService().readUserResponseSet(
+                        sessionLabel,
+                        userInfo.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( userInfo.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 '" + userInfo.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( forgottenPasswordProfile, forgottenPasswordBean, pwmRequest, 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() )
+        {
+            verifyRequirementsForAuthMethod( pwmRequest, forgottenPasswordBean, recoveryVerificationMethods );
+        }
+    }
+
+    static List<FormConfiguration> figureAttributeForm(
+            final ForgottenPasswordProfile forgottenPasswordProfile,
+            final ForgottenPasswordBean forgottenPasswordBean,
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity
+    )
+            throws ChaiUnavailableException, PwmOperationalException, PwmUnrecoverableException
+    {
+        final List<FormConfiguration> requiredAttributesForm = forgottenPasswordProfile.readSettingAsForm( PwmSetting.RECOVERY_ATTRIBUTE_FORM );
+        if ( requiredAttributesForm.isEmpty() )
+        {
+            return requiredAttributesForm;
+        }
+
+        final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
+        final List<FormConfiguration> returnList = new ArrayList<>();
+        for ( final FormConfiguration formItem : requiredAttributesForm )
+        {
+            if ( formItem.isRequired() )
+            {
+                returnList.add( formItem );
+            }
+            else
+            {
+                try
+                {
+                    final String currentValue = userInfo.readStringAttribute( formItem.getName() );
+                    if ( currentValue != null && currentValue.length() > 0 )
+                    {
+                        returnList.add( formItem );
+                    }
+                    else
+                    {
+                        LOGGER.trace( pwmRequest, "excluding optional required attribute(" + formItem.getName() + "), user has no value" );
+                    }
+                }
+                catch ( PwmUnrecoverableException 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;
+    }
+
+    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(
+                allowWhenLdapIntruderLocked,
+                requiredRecoveryVerificationMethods,
+                optionalRecoveryVerificationMethods,
+                minimumOptionalRecoveryAuthMethods,
+                tokenSendMethod
+        );
+    }
 }

+ 4 - 1
server/src/main/java/password/pwm/util/form/FormUtility.java

@@ -191,7 +191,10 @@ public class FormUtility
         return returnObj;
     }
 
-    public static Map<FormConfiguration, String> asFormConfigurationMap( final List<FormConfiguration> formConfigurations, final Map<String, String> values )
+    public static Map<FormConfiguration, String> asFormConfigurationMap(
+            final List<FormConfiguration> formConfigurations,
+            final Map<String, String> values
+    )
     {
         final Map<FormConfiguration, String> returnMap = new LinkedHashMap<>();
         for ( final FormConfiguration formConfiguration : formConfigurations )

+ 7 - 0
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -2525,6 +2525,11 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="false" key="recovery.bogus.user.enable" level="2">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="false" key="recovery.token.resend.enable" level="1" required="true">
         <default>
             <value>true</value>
@@ -3968,6 +3973,8 @@
     </category>
     <category hidden="false" key="RECOVERY_SETTINGS">
     </category>
+    <category hidden="false" key="RECOVERY_OPTIONS">
+    </category>
     <category hidden="false" key="RECOVERY_PROFILE" profiles="true">
         <profile setting="recovery.profile.list"/>
     </category>

+ 1 - 0
server/src/main/resources/password/pwm/i18n/Error.properties

@@ -159,6 +159,7 @@ Error_EnvironmentError=An error with the application environment has prevented t
 Error_ApplicationNotRunning=This functionality is not available until the application configuration is restricted.
 Error_EmailSendFailure=Error sending email item %1%, error: %2%
 Error_PasswordOnlyBad=Password incorrect.  Please try again.
+Error_RecoverySequenceIncomplete=A problem occurred during the forgotten password sequence, please try again.
 
 Error_RemoteErrorValue=Remote Error: %1%
 

+ 4 - 0
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -92,6 +92,7 @@ Category_Description_RECOVERY_DEF=Definition
 Category_Description_RECOVERY_OAUTH=OAuth
 Category_Description_RECOVERY=Policies for forgotten password configuration.
 Category_Description_RECOVERY_PROFILE=Policies for forgotten password configuration.
+Category_Description_RECOVERY_OPTIONS=Options for forgotten password configuration.
 Category_Description_RECOVERY_SETTINGS=Settings for forgotten password configuration.
 Category_Description_REPORTING=Options to enable and configure reporting.
 Category_Description_REST_CLIENT=
@@ -188,6 +189,7 @@ Category_Label_RECOVERY_DEF=Definition
 Category_Label_RECOVERY=Forgotten Password
 Category_Label_RECOVERY_OAUTH=OAuth
 Category_Label_RECOVERY_PROFILE=Profiles
+Category_Label_RECOVERY_OPTIONS=Options
 Category_Label_RECOVERY_SETTINGS=Settings
 Category_Label_REPORTING=Reporting
 Category_Label_REST_CLIENT=REST Clients
@@ -595,6 +597,7 @@ Setting_Description_pwm.selfURL=<p>The URL to this application, as seen by users
 Setting_Description_pwm.wordlist.location=Specify a word list file URL for dictionary checking to prevent users from using commonly used words as passwords.   Using word lists is an important part of password security.  Word lists are used by intruders to guess common passwords.   The default word list included contains commonly used English passwords.  <br/><br/>The first time a startup occurs with a new word list setting, it takes some time to compile the word list into a database.  See the status screen and logs for progress information.  The word list file format is one or more text files containing a single word per line, enclosed in a ZIP file.  The String <i>\!\#comment\:</i> at the beginning of a line indicates a comment. <br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
 Setting_Description_recovery.action=Add actions to take when the user completes the forgotten password process.
 Setting_Description_recovery.allowWhenLocked=Enable this option to allow users to use the forgotten password feature when the account is intruder locked in LDAP.  This feature is not available when a user is using NMAS stored responses.
+Setting_Description_recovery.bogus.user.enable=Enable this option to have forgotten password act as though invalid user searches are valid, and present such users with a bogus forgotten password policy.  This can help prevent username discovery.
 Setting_Description_recovery.enable=Enable this option to have the forgotten password recovery available to users.
 Setting_Description_recovery.form=Specify the form fields for the activate user module. @PwmAppName@ requires the users to enter each attribute. Ideally, @PwmAppName@ requires the users to enter some personal data that is not publicly known.
 Setting_Description_recovery.oauth.idserver.attributesUrl=Specify the web service URL provided by the identity server to return attribute data about the user.
@@ -1079,6 +1082,7 @@ Setting_Label_pwm.selfURL=Site URL
 Setting_Label_pwm.wordlist.location=Word List File URL
 Setting_Label_recovery.action=Forgotten Password Recovery Mode
 Setting_Label_recovery.allowWhenLocked=Allow Forgotten Password when Locked
+Setting_Label_recovery.bogus.user.enable=Enable Bogus User Policy
 Setting_Label_recovery.enable=Enable Forgotten Password
 Setting_Label_recovery.form=Forgotten Password User Search Form
 Setting_Label_recovery.oauth.idserver.attributesUrl=OAuth Profile Service URL