瀏覽代碼

add /forgottenpassword rest api

Jason Rivard 6 年之前
父節點
當前提交
f79e514c7b
共有 74 個文件被更改,包括 3202 次插入427 次删除
  1. 1 0
      build/checkstyle-suppression.xml
  2. 1 1
      server/pom.xml
  3. 2 0
      server/src/main/java/password/pwm/AppProperty.java
  4. 1 0
      server/src/main/java/password/pwm/PwmConstants.java
  5. 32 10
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  6. 8 5
      server/src/main/java/password/pwm/config/PwmSetting.java
  7. 41 11
      server/src/main/java/password/pwm/config/option/WebServiceUsage.java
  8. 17 0
      server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java
  9. 26 49
      server/src/main/java/password/pwm/cr/CrChallengeItemBean.java
  10. 98 4
      server/src/main/java/password/pwm/cr/CrChallengePolicyBean.java
  11. 45 0
      server/src/main/java/password/pwm/http/CommonValues.java
  12. 21 0
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  13. 8 0
      server/src/main/java/password/pwm/http/PwmRequest.java
  14. 8 2
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  15. 34 0
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordStage.java
  16. 1 1
      server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  17. 51 0
      server/src/main/java/password/pwm/http/servlet/PwmRequestID.java
  18. 4 4
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java
  19. 2 2
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  20. 31 33
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  21. 414 0
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStageProcessor.java
  22. 1116 0
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStateMachine.java
  23. 84 104
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  24. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserServlet.java
  25. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  26. 1 1
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java
  27. 1 1
      server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileUtil.java
  28. 1 0
      server/src/main/java/password/pwm/i18n/Display.java
  29. 12 4
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  30. 11 0
      server/src/main/java/password/pwm/ldap/UserInfoFactory.java
  31. 7 8
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  32. 11 1
      server/src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java
  33. 1 1
      server/src/main/java/password/pwm/ldap/search/SearchConfiguration.java
  34. 1 11
      server/src/main/java/password/pwm/svc/event/DatabaseUserHistory.java
  35. 2 2
      server/src/main/java/password/pwm/svc/intruder/IntruderManager.java
  36. 4 19
      server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java
  37. 19 20
      server/src/main/java/password/pwm/svc/token/TokenService.java
  38. 22 15
      server/src/main/java/password/pwm/svc/token/TokenUtil.java
  39. 10 0
      server/src/main/java/password/pwm/util/i18n/LocaleHelper.java
  40. 21 0
      server/src/main/java/password/pwm/util/java/JsonUtil.java
  41. 1 0
      server/src/main/java/password/pwm/util/logging/PwmLogManager.java
  42. 10 0
      server/src/main/java/password/pwm/util/macro/MacroMachine.java
  43. 129 0
      server/src/main/java/password/pwm/util/secure/BeanCryptoMachine.java
  44. 6 0
      server/src/main/java/password/pwm/util/secure/SecureService.java
  45. 41 0
      server/src/main/java/password/pwm/ws/server/PresentableForm.java
  46. 74 0
      server/src/main/java/password/pwm/ws/server/PresentableFormRow.java
  47. 6 16
      server/src/main/java/password/pwm/ws/server/RestAuthenticationProcessor.java
  48. 9 0
      server/src/main/java/password/pwm/ws/server/RestRequest.java
  49. 43 31
      server/src/main/java/password/pwm/ws/server/RestResultBean.java
  50. 34 16
      server/src/main/java/password/pwm/ws/server/RestServlet.java
  51. 0 2
      server/src/main/java/password/pwm/ws/server/RestWebServer.java
  52. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestChallengesServer.java
  53. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestCheckPasswordServer.java
  54. 168 0
      server/src/main/java/password/pwm/ws/server/rest/RestForgottenPasswordServer.java
  55. 1 8
      server/src/main/java/password/pwm/ws/server/rest/RestFormSigningServer.java
  56. 1 5
      server/src/main/java/password/pwm/ws/server/rest/RestHealthServer.java
  57. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java
  58. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java
  59. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java
  60. 1 5
      server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java
  61. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestStatusServer.java
  62. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java
  63. 1 1
      server/src/main/java/password/pwm/ws/server/rest/RestVerifyResponsesServer.java
  64. 2 0
      server/src/main/resources/password/pwm/AppProperty.properties
  65. 13 5
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  66. 1 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  67. 4 2
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  68. 1 0
      webapp/pom.xml
  69. 1 1
      webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp
  70. 4 4
      webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-responses.jsp
  71. 208 0
      webapp/src/main/webapp/public/examples/rest-client-example.js
  72. 61 0
      webapp/src/main/webapp/public/examples/rest-client-example.jsp
  73. 186 0
      webapp/src/main/webapp/public/reference/rest.jsp
  74. 17 13
      webapp/src/main/webapp/public/resources/js/changepassword.js

+ 1 - 0
build/checkstyle-suppression.xml

@@ -25,4 +25,5 @@
 
 <suppressions>
     <suppress files="XmlFactoryTest\.xml" checks="[a-zA-Z0-9]*"/>
+    <suppress files="rest.jsp" checks="FileLength"/>
 </suppressions>

+ 1 - 1
server/pom.xml

@@ -192,7 +192,7 @@
         <dependency>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
-            <version>0.7.4</version>
+            <version>0.7.5</version>
         </dependency>
         <dependency>
             <groupId>commons-net</groupId>

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

@@ -344,6 +344,8 @@ public enum AppProperty
     TOKEN_REMOVE_ON_CLAIM                           ( "token.removeOnClaim" ),
     TOKEN_VERIFY_PW_MODIFY_TIME                     ( "token.verifyPwModifyTime" ),
     TOKEN_STORAGE_MAX_KEY_LENGTH                    ( "token.storage.maxKeyLength" ),
+    REST_SERVER_FORGOTTEN_PW_TOKEN_DISPLAY          ( "rest.server.forgottenPW.token.display" ),
+    REST_SERVER_FORGOTTEN_PW_RULE_DELIMITER         ( "rest.server.forgottenPW.ruleDelimiter" ),
     TELEMETRY_SENDER_IMPLEMENTATION                 ( "telemetry.senderImplementation" ),
     TELEMETRY_SENDER_SETTINGS                       ( "telemetry.senderSettings" ),
     TELEMETRY_SEND_FREQUENCY_SECONDS                ( "telemetry.sendFrequencySeconds" ),

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

@@ -166,6 +166,7 @@ public abstract class PwmConstants
     public static final String PARAM_RECOVERY_OAUTH_RESULT = "roauthResults";
     public static final String PARAM_SIGNED_FORM = "signedForm";
     public static final String PARAM_USERKEY = "userKey";
+    public static final String PARAM_METHOD_CHOICE = "methodChoice";
 
 
     public static final String COOKIE_PERSISTENT_CONFIG_LOGIN = "CONFIG-AUTH";

+ 32 - 10
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -23,14 +23,19 @@
 package password.pwm.bean;
 
 import lombok.Builder;
+import lombok.Getter;
 import lombok.Value;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.Display;
+import password.pwm.i18n.PwmDisplayBundle;
 import password.pwm.ldap.UserInfo;
 import password.pwm.svc.token.TokenDestinationDisplayMasker;
+import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.secure.SecureService;
 
@@ -41,6 +46,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 
@@ -72,21 +78,21 @@ public class TokenDestinationItem implements Serializable
     private String value;
     private Type type;
 
+    @Getter
     public enum Type
     {
-        sms( MessageSendMethod.SMSONLY ),
-        email( MessageSendMethod.EMAILONLY ),;
+        sms( MessageSendMethod.SMSONLY, Display.Button_SMS, Display.Display_RecoverTokenSendChoiceEmail ),
+        email( MessageSendMethod.EMAILONLY, Display.Button_Email, Display.Display_RecoverTokenSendChoiceSMS ),;
 
         private MessageSendMethod messageSendMethod;
+        private PwmDisplayBundle buttonLocalization;
+        private PwmDisplayBundle displayLocalization;
 
-        Type( final MessageSendMethod messageSendMethod )
+        Type( final MessageSendMethod messageSendMethod, final PwmDisplayBundle buttonLocalization, final PwmDisplayBundle displayLocalization )
         {
+            this.buttonLocalization = buttonLocalization;
             this.messageSendMethod = messageSendMethod;
-        }
-
-        public MessageSendMethod getMessageSendMethod( )
-        {
-            return messageSendMethod;
+            this.displayLocalization = displayLocalization;
         }
     }
 
@@ -109,7 +115,7 @@ public class TokenDestinationItem implements Serializable
                         userInfo.getUserEmailAddress2(),
                         userInfo.getUserEmailAddress3(),
                 }
-                )
+        )
         {
             if ( !StringUtil.isEmpty( emailValue ) )
             {
@@ -130,7 +136,7 @@ public class TokenDestinationItem implements Serializable
                         userInfo.getUserSmsNumber2(),
                         userInfo.getUserSmsNumber3(),
                 }
-                )
+        )
         {
             if ( !StringUtil.isEmpty( smsValue ) )
             {
@@ -186,4 +192,20 @@ public class TokenDestinationItem implements Serializable
         }
         return returnList;
     }
+
+    public String longDisplay( final Locale locale, final Configuration configuration )
+    {
+        final Map<String, String> tokens = new HashMap<>();
+        tokens.put( "%LABEL%", LocaleHelper.getLocalizedMessage( locale, getType().getButtonLocalization(), configuration ) );
+        tokens.put( "%MESSAGE%", LocaleHelper.getLocalizedMessage( locale, getType().getDisplayLocalization(), configuration ) );
+        tokens.put( "%VALUE%", this.getDisplay() );
+
+        String output = configuration.readAppProperty( AppProperty.REST_SERVER_FORGOTTEN_PW_TOKEN_DISPLAY );
+        for ( final Map.Entry<String, String> entry : tokens.entrySet() )
+        {
+            output = output.replace( entry.getKey(), entry.getValue() );
+        }
+
+        return output;
+    }
 }

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

@@ -1189,17 +1189,16 @@ public enum PwmSetting
 
     ENABLE_EXTERNAL_WEBSERVICES(
             "external.webservices.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REST_SERVER ),
-    ENABLE_WEBSERVICES_READANSWERS(
-            "webservices.enableReadAnswers", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REST_SERVER ),
-    PUBLIC_HEALTH_STATS_WEBSERVICES(
-            "webservices.healthStats.makePublic", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REST_SERVER ),
+    WEBSERVICES_PUBLIC_ENABLE(
+            "webservices.public.enable", PwmSettingSyntax.OPTIONLIST, PwmSettingCategory.REST_SERVER ),
     WEBSERVICES_EXTERNAL_SECRET(
             "webservices.external.secrets", PwmSettingSyntax.NAMED_SECRET, PwmSettingCategory.REST_SERVER ),
     WEBSERVICES_QUERY_MATCH(
             "webservices.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REST_SERVER ),
     WEBSERVICES_THIRDPARTY_QUERY_MATCH(
             "webservices.thirdParty.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.REST_SERVER ),
-
+    ENABLE_WEBSERVICES_READANSWERS(
+            "webservices.enableReadAnswers", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REST_SERVER ),
 
     EXTERNAL_MACROS_DEST_TOKEN_URLS(
             "external.destToken.urls", PwmSettingSyntax.STRING, PwmSettingCategory.REST_CLIENT ),
@@ -1224,6 +1223,10 @@ public enum PwmSetting
 
     // deprecated.
 
+    // deprecated 2019-06-01
+    PUBLIC_HEALTH_STATS_WEBSERVICES(
+            "webservices.healthStats.makePublic", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.REST_SERVER ),
+
     // deprecated 2019-01-20
     PEOPLE_SEARCH_DISPLAY_NAME(
             "peopleSearch.displayName.user", PwmSettingSyntax.STRING, PwmSettingCategory.PEOPLE_SEARCH ),

+ 41 - 11
server/src/main/java/password/pwm/config/option/WebServiceUsage.java

@@ -22,17 +22,47 @@
 
 package password.pwm.config.option;
 
+import password.pwm.ws.server.RestAuthenticationType;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 public enum WebServiceUsage
 {
-    Challenges,
-    CheckPassword,
-    Health,
-    Profile,
-    RandomPassword,
-    SetPassword,
-    SigningForm,
-    Statistics,
-    Status,
-    VerifyOtp,
-    VerifyResponses,
+    Challenges( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    CheckPassword( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    ForgottenPassword( RestAuthenticationType.PUBLIC ),
+    Health( RestAuthenticationType.PUBLIC ),
+    Profile( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    RandomPassword( RestAuthenticationType.PUBLIC, RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    SetPassword( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    SigningForm( RestAuthenticationType.NAMED_SECRET ),
+    Statistics( RestAuthenticationType.PUBLIC, RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    Status( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    VerifyOtp( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),
+    VerifyResponses( RestAuthenticationType.NAMED_SECRET, RestAuthenticationType.LDAP ),;
+
+    private Set<RestAuthenticationType> type;
+
+    WebServiceUsage( final RestAuthenticationType... type )
+    {
+        this.type = type == null ? Collections.emptySet() : Collections.unmodifiableSet( new HashSet<>( Arrays.asList( type ) ) );
+    }
+
+    public Set<RestAuthenticationType> getTypes()
+    {
+        return type;
+    }
+
+    public static Set<WebServiceUsage> forType( final RestAuthenticationType type )
+    {
+        return Collections.unmodifiableSet(
+                Arrays.stream( WebServiceUsage.values() )
+                        .filter( webServiceUsage -> webServiceUsage.getTypes().contains( type ) )
+                        .collect( Collectors.toSet() )
+        );
+    }
 }

+ 17 - 0
server/src/main/java/password/pwm/config/stored/ConfigurationCleaner.java

@@ -27,6 +27,8 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.option.WebServiceUsage;
+import password.pwm.config.value.OptionListValue;
 import password.pwm.config.value.StringArrayValue;
 import password.pwm.config.value.StringValue;
 import password.pwm.error.PwmUnrecoverableException;
@@ -36,7 +38,9 @@ import password.pwm.util.logging.PwmLogger;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 
 class ConfigurationCleaner
 {
@@ -338,5 +342,18 @@ class ConfigurationCleaner
                 storedConfiguration.resetSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, actor );
             }
         }
+
+        if ( !storedConfiguration.isDefaultValue( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
+        {
+            LOGGER.warn( "converting deprecated non-default setting "
+                    + PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE )
+                    + " to replacement setting " + PwmSetting.WEBSERVICES_PUBLIC_ENABLE.toMenuLocationDebug( null, PwmConstants.DEFAULT_LOCALE ) );
+            final Set<String> existingValues = (Set<String>) storedConfiguration.readSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE ).toNativeObject();
+            final Set<String> newValues = new LinkedHashSet<>( existingValues );
+            newValues.add( WebServiceUsage.Health.name() );
+            newValues.add( WebServiceUsage.Statistics.name() );
+            storedConfiguration.writeSetting( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, null, new OptionListValue( newValues ), actor );
+            storedConfiguration.resetSetting( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES, null, actor );
+        }
     }
 }

+ 26 - 49
server/src/main/java/password/pwm/cr/CrChallengeItemBean.java

@@ -22,69 +22,46 @@
 
 package password.pwm.cr;
 
+import com.novell.ldapchai.cr.Challenge;
+import com.novell.ldapchai.cr.bean.ChallengeBean;
+import lombok.Builder;
+import lombok.Value;
+
 import java.io.Serializable;
 
-public class CrChallengeItemBean implements Serializable
+@Value
+@Builder
+public class CrChallengeItemBean implements Serializable, Challenge
 {
-    public String challengeText;
-    public int minLength;
-    public int maxLength;
-    public boolean adminDefined;
-    public boolean required;
-    public int maxQuestionCharsInAnswer;
-    public boolean enforceWordlist;
-
-    public CrChallengeItemBean(
-            final String challengeText,
-            final int minLength,
-            final int maxLength,
-            final boolean adminDefined,
-            final boolean required,
-            final int maxQuestionCharsInAnswer,
-            final boolean enforceWordlist
-    )
-    {
-        this.challengeText = challengeText;
-        this.minLength = minLength;
-        this.maxLength = maxLength;
-        this.adminDefined = adminDefined;
-        this.required = required;
-        this.maxQuestionCharsInAnswer = maxQuestionCharsInAnswer;
-        this.enforceWordlist = enforceWordlist;
-    }
-
-    public String getChallengeText( )
-    {
-        return challengeText;
-    }
-
-    public int getMinLength( )
-    {
-        return minLength;
-    }
+    private String challengeText;
+    private int minLength;
+    private int maxLength;
+    private boolean adminDefined;
+    private boolean required;
+    private int maxQuestionCharsInAnswer;
+    private boolean enforceWordlist;
 
-    public int getMaxLength( )
+    @Override
+    public boolean isLocked()
     {
-        return maxLength;
+        return true;
     }
 
-    public boolean isAdminDefined( )
+    @Override
+    public void lock()
     {
-        return adminDefined;
-    }
 
-    public boolean isRequired( )
-    {
-        return required;
     }
 
-    public int getMaxQuestionCharsInAnswer( )
+    @Override
+    public void setChallengeText( final String challengeText )
     {
-        return maxQuestionCharsInAnswer;
+        throw new IllegalStateException();
     }
 
-    public boolean isEnforceWordlist( )
+    @Override
+    public ChallengeBean asChallengeBean()
     {
-        return enforceWordlist;
+        throw new IllegalStateException();
     }
 }

+ 98 - 4
server/src/main/java/password/pwm/cr/CrChallengePolicyBean.java

@@ -22,14 +22,108 @@
 
 package password.pwm.cr;
 
+import com.novell.ldapchai.cr.Challenge;
+import com.novell.ldapchai.cr.ChallengeSet;
+import com.novell.ldapchai.cr.bean.ChallengeSetBean;
+import lombok.Builder;
+import lombok.Value;
+
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
 
-public class CrChallengePolicyBean implements Serializable
+@Value
+@Builder
+public class CrChallengePolicyBean implements Serializable, ChallengeSet
 {
-    private String locale;
-    private List<CrChallengeItemBean> challenges;
-    private List<CrChallengeItemBean> helpdeskChallenges;
+    private Locale locale;
+    private List<Challenge> challenges;
+    private List<Challenge> helpdeskChallenges;
     private int minRandomRequired;
     private int helpdeskMinRandomRequired;
+
+    @Override
+    public List<Challenge> getAdminDefinedChallenges()
+    {
+        return challenges.stream()
+                .filter( Challenge::isAdminDefined )
+                .collect( Collectors.toList() );
+    }
+
+    @Override
+    public List<String> getChallengeTexts()
+    {
+
+        final List<String> returnList = new ArrayList<>();
+        challenges.stream()
+                .forEach( challenge -> returnList.add( challenge.getChallengeText() ) );
+        return Collections.unmodifiableList( returnList );
+    }
+
+    @Override
+    public List<Challenge> getRandomChallenges()
+    {
+        return challenges.stream()
+                .filter( challenge -> !challenge.isRequired() )
+                .collect( Collectors.toList() );
+    }
+
+    @Override
+    public List<Challenge> getRequiredChallenges()
+    {
+        return challenges.stream()
+                .filter( Challenge::isRequired )
+                .collect( Collectors.toList() );
+    }
+
+    @Override
+    public List<Challenge> getUserDefinedChallenges()
+    {
+        return challenges.stream()
+                .filter( crChallengeItemBean -> !crChallengeItemBean.isAdminDefined() )
+                .collect( Collectors.toList() );
+    }
+
+    @Override
+    public int minimumResponses()
+    {
+        int mininimumResponses = 0;
+
+        mininimumResponses += getRequiredChallenges().size();
+        mininimumResponses += getMinRandomRequired();
+
+        return mininimumResponses;
+    }
+
+    @Override
+    public boolean isLocked()
+    {
+        return true;
+    }
+
+    @Override
+    public void lock()
+    {
+
+    }
+
+    @Override
+    public String getIdentifier()
+    {
+        return null;
+    }
+
+    public static CrChallengePolicyBean fromChallengeSet( final ChallengeSet challengeSet )
+    {
+        return null;
+    }
+
+    @Override
+    public ChallengeSetBean asChallengeSetBean()
+    {
+        return null;
+    }
 }

+ 45 - 0
server/src/main/java/password/pwm/http/CommonValues.java

@@ -0,0 +1,45 @@
+/*
+ * 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
+ */
+
+package password.pwm.http;
+
+import lombok.Value;
+import password.pwm.PwmApplication;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.http.servlet.PwmRequestID;
+
+import java.util.Locale;
+
+@Value
+public class CommonValues
+{
+    final PwmApplication pwmApplication;
+    final SessionLabel sessionLabel;
+    final Locale locale;
+    final PwmRequestID requestID;
+
+    public Configuration getConfig()
+    {
+        return pwmApplication.getConfig();
+    }
+}

+ 21 - 0
server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java

@@ -22,9 +22,11 @@
 
 package password.pwm.http;
 
+import com.google.gson.JsonParseException;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.ServletUtility;
@@ -544,5 +546,24 @@ public class PwmHttpRequestWrapper
 
         return sb.toString();
     }
+
+
+    public <T> T readBodyAsJsonObject( final Class<T> classOfT )
+            throws IOException, PwmUnrecoverableException
+    {
+        final String json = readRequestBodyAsString();
+        try
+        {
+            return JsonUtil.deserialize( json, classOfT );
+        }
+        catch ( Exception e )
+        {
+            if ( e instanceof JsonParseException )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_REST_INVOCATION_ERROR, "unable to parse json body: " + e.getCause().getMessage() );
+            }
+            throw e;
+        }
+    }
 }
 

+ 8 - 0
server/src/main/java/password/pwm/http/PwmRequest.java

@@ -42,6 +42,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.bean.ImmutableByteArray;
 import password.pwm.http.servlet.AbstractPwmServlet;
+import password.pwm.http.servlet.PwmRequestID;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.command.CommandServlet;
 import password.pwm.ldap.UserInfo;
@@ -75,6 +76,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
 
     private final PwmResponse pwmResponse;
     private final PwmURL pwmURL;
+    private final PwmRequestID pwmRequestID;
 
     private transient PwmApplication pwmApplication;
     private transient PwmSession pwmSession;
@@ -107,6 +109,7 @@ public class PwmRequest extends PwmHttpRequestWrapper
             throws PwmUnrecoverableException
     {
         super( httpServletRequest, pwmApplication.getConfig() );
+        this.pwmRequestID = PwmRequestID.next();
         this.pwmResponse = new PwmResponse( httpServletResponse, this, pwmApplication.getConfig() );
         this.pwmSession = pwmSession;
         this.pwmApplication = pwmApplication;
@@ -568,4 +571,9 @@ public class PwmRequest extends PwmHttpRequestWrapper
         return false;
     }
 
+    public CommonValues commonValues()
+    {
+        return new CommonValues( pwmApplication, this.getSessionLabel(), this.getLocale(), pwmRequestID );
+    }
+
 }

+ 8 - 2
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -23,7 +23,7 @@
 package password.pwm.http.bean;
 
 import com.google.gson.annotations.SerializedName;
-import com.novell.ldapchai.cr.ChallengeSet;
+import com.novell.ldapchai.cr.bean.ChallengeSetBean;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -32,6 +32,7 @@ import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.RecoveryAction;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.config.value.data.FormConfiguration;
 
@@ -50,12 +51,14 @@ import java.util.Set;
 @EqualsAndHashCode( callSuper = false )
 public class ForgottenPasswordBean extends PwmSessionBean
 {
+    @SerializedName( "pr" )
+    private String profile;
 
     @SerializedName( "u" )
     private UserIdentity userIdentity;
 
     @SerializedName( "pc" )
-    private ChallengeSet presentableChallengeSet;
+    private ChallengeSetBean presentableChallengeSet;
 
     @SerializedName( "l" )
     private Locale userLocale;
@@ -98,6 +101,9 @@ public class ForgottenPasswordBean extends PwmSessionBean
 
         private transient VerificationMethodSystem remoteRecoveryMethod;
 
+        @SerializedName( "ra" )
+        private RecoveryAction executedRecoveryAction;
+
         public void clearTokenSentStatus( )
         {
             this.setTokenSent( false );

+ 34 - 0
server/src/main/java/password/pwm/http/bean/ForgottenPasswordStage.java

@@ -0,0 +1,34 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.bean;
+
+public enum ForgottenPasswordStage
+{
+    IDENTIFICATION,
+    METHOD_CHOICE,
+    TOKEN_CHOICE,
+    VERIFICATION,
+    ACTION_CHOICE,
+    NEW_PASSWORD,
+    COMPLETE,
+}

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

@@ -232,7 +232,7 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
                     : e.getErrorInformation();
             setLastError( pwmRequest, errorInfo );
             pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmRequest.getSessionLabel() );
         }
 
         pwmApplication.getStatisticsManager().incrementValue( Statistic.FORGOTTEN_USERNAME_FAILURES );

+ 51 - 0
server/src/main/java/password/pwm/http/servlet/PwmRequestID.java

@@ -0,0 +1,51 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.servlet;
+
+import password.pwm.util.java.AtomicLoopIntIncrementer;
+
+public class PwmRequestID
+{
+    private final int value;
+    private static final AtomicLoopIntIncrementer INCREMENTER = new AtomicLoopIntIncrementer( Integer.MAX_VALUE );
+
+    private PwmRequestID( final int value )
+    {
+        this.value = value;
+    }
+
+    private String value()
+    {
+        return String.valueOf( value );
+    }
+
+    public String toString()
+    {
+        return value();
+    }
+
+    public static PwmRequestID next()
+    {
+        return new PwmRequestID( INCREMENTER.next() );
+    }
+}

+ 4 - 4
server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java

@@ -278,7 +278,7 @@ public class ActivateUserServlet extends ControlledPwmServlet
         }
         catch ( PwmOperationalException e )
         {
-            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmRequest.getSessionLabel() );
             pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
             setLastError( pwmRequest, e.getErrorInformation() );
             LOGGER.debug( pwmSession, e.getErrorInformation() );
@@ -330,10 +330,10 @@ public class ActivateUserServlet extends ControlledPwmServlet
         try
         {
             final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
-                    pwmRequest,
+                    pwmRequest.commonValues(),
                     userEnteredCode,
                     activateUserBean.getTokenDestination(),
-                    null,
+                    activateUserBean.getUserIdentity(),
                     TokenType.ACTIVATION,
                     TokenService.TokenEntryType.unauthenticated
             );
@@ -440,7 +440,7 @@ public class ActivateUserServlet extends ControlledPwmServlet
             if ( !activateUserBean.isTokenSent() && activateUserBean.getTokenDestination() != null )
             {
                 TokenUtil.initializeAndSendToken(
-                        pwmRequest,
+                        pwmRequest.commonValues(),
                         TokenUtil.TokenInitAndSendRequest.builder()
                                 .userInfo( userInfo )
                                 .tokenDestinationItem( activateUserBean.getTokenDestination() )

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java

@@ -55,8 +55,6 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.stats.AvgStatistic;
 import password.pwm.util.PasswordData;
-import password.pwm.util.password.PwmPasswordRuleValidator;
-import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -64,6 +62,8 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
+import password.pwm.util.password.PwmPasswordRuleValidator;
+import password.pwm.util.password.RandomPasswordGenerator;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestCheckPasswordServer;
 import password.pwm.ws.server.rest.RestRandomPasswordServer;

+ 31 - 33
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -246,7 +246,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         final boolean disallowAllButUnlock;
         {
-            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
             final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
                     PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
                     RecoveryMinLifetimeOption.class
@@ -272,7 +272,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                     case resetPassword:
                         if ( disallowAllButUnlock )
                         {
-                            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+                            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
                             PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
                         }
                         this.executeResetPassword( pwmRequest );
@@ -328,7 +328,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             throws PwmUnrecoverableException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        final List<TokenDestinationItem> items = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+        final List<TokenDestinationItem> items = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest.commonValues(), forgottenPasswordBean );
 
         final String requestedID = pwmRequest.readParameterAsString( "choice", PwmHttpRequestWrapper.Flag.BypassValidation );
 
@@ -346,9 +346,9 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             throws PwmUnrecoverableException, ServletException, IOException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        final String requestedChoiceStr = pwmRequest.readParameterAsString( "choice" );
+        final String requestedChoiceStr = pwmRequest.readParameterAsString( PwmConstants.PARAM_METHOD_CHOICE );
         final LinkedHashSet<IdentityVerificationMethod> remainingAvailableOptionalMethods = new LinkedHashSet<>(
-                ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods( pwmRequest, forgottenPasswordBean )
+                ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods( pwmRequest.commonValues(), forgottenPasswordBean )
         );
         pwmRequest.setAttribute( PwmRequestAttribute.AvailableAuthMethods, remainingAvailableOptionalMethods );
 
@@ -467,7 +467,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             AuthenticationUtility.checkIfUserEligibleToAuthentication( pwmApplication, userIdentity );
 
             final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-            ForgottenPasswordUtil.initForgottenPasswordBean( pwmRequest, userIdentity, forgottenPasswordBean );
+            ForgottenPasswordUtil.initForgottenPasswordBean( pwmRequest.commonValues(), userIdentity, forgottenPasswordBean );
 
             // clear intruder search values
             pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
@@ -485,7 +485,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                 pwmApplication.getStatisticsManager().incrementValue( Statistic.RECOVERY_FAILURES );
 
                 pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-                pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+                pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmRequest.getSessionLabel() );
 
                 LOGGER.debug( pwmSession, errorInfo );
                 setLastError( pwmRequest, errorInfo );
@@ -495,7 +495,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         if ( bogusUserModeEnabled )
         {
-            ForgottenPasswordUtil.initBogusForgottenPasswordBean( pwmRequest );
+            ForgottenPasswordUtil.initBogusForgottenPasswordBean( pwmRequest.commonValues(), forgottenPasswordBean( pwmRequest ) );
             forgottenPasswordBean( pwmRequest ).setUserSearchValues( FormUtility.asStringMap( formValues ) );
         }
 
@@ -509,13 +509,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
         final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
 
-
-
         ErrorInformation errorInformation = null;
         try
         {
             final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
-                    pwmRequest,
+                    pwmRequest.commonValues(),
                     userEnteredCode,
                     forgottenPasswordBean.getProgress().getTokenDestination(),
                     null,
@@ -528,7 +526,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             {
                 // clean session, user supplied token (clicked email, etc) and this is first request
                 ForgottenPasswordUtil.initForgottenPasswordBean(
-                        pwmRequest,
+                        pwmRequest.commonValues(),
                         tokenPayload.getUserIdentity(),
                         forgottenPasswordBean
                 );
@@ -622,7 +620,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
         LOGGER.debug( pwmRequest, () -> String.format( "entered OTP: %s", userEnteredCode ) );
 
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
         final OTPUserRecord otpUserRecord = userInfo.getOtpUserRecord();
 
         final boolean otpPassed;
@@ -744,7 +742,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
 
-        final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet( pwmRequest, forgottenPasswordBean );
+        final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet( pwmRequest.commonValues(), forgottenPasswordBean );
         if ( responseSet == null )
         {
             final String errorMsg = "attempt to check responses, but responses are not loaded into session bean";
@@ -777,7 +775,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             // special case for nmas, clear out existing challenges and input fields.
             if ( !responsesPassed && responseSet instanceof NMASCrOperator.NMASCRResponseSet )
             {
-                forgottenPasswordBean.setPresentableChallengeSet( responseSet.getPresentableChallengeSet() );
+                forgottenPasswordBean.setPresentableChallengeSet( responseSet.getPresentableChallengeSet().asChallengeSetBean() );
             }
 
             if ( responsesPassed )
@@ -837,9 +835,9 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
 
         {
-            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
             final TokenDestinationItem tokenDestinationItem = forgottenPasswordBean.getProgress().getTokenDestination();
-            ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, tokenDestinationItem );
+            ForgottenPasswordUtil.initializeAndSendToken( pwmRequest.commonValues(), userInfo, tokenDestinationItem );
         }
 
         final RestResultBean restResultBean = RestResultBean.forSuccessMessage( pwmRequest, Message.Success_TokenResend );
@@ -863,7 +861,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             {
                 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() );
+                pwmRequest.getPwmApplication().getIntruderManager().convenience().markAttributes( formMap, pwmRequest.getSessionLabel() );
             }
 
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE,
@@ -1029,7 +1027,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             if ( satisfiedOptionalMethods.size() < recoveryFlags.getMinimumOptionalAuthMethods() )
             {
                 final Set<IdentityVerificationMethod> remainingAvailableOptionalMethods = ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods(
-                        pwmRequest,
+                        pwmRequest.commonValues(),
                         forgottenPasswordBean
                 );
                 if ( remainingAvailableOptionalMethods.isEmpty() )
@@ -1085,7 +1083,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_SUCCESSES );
         }
 
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
         if ( userInfo == null )
         {
             throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to load userInfo while processing forgotten password controller" );
@@ -1160,10 +1158,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             theUser.unlockPassword();
 
             // mark the event log
-            final UserInfo userInfoBean = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+            final UserInfo userInfoBean = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
             pwmApplication.getAuditManager().submit( AuditEvent.UNLOCK_PASSWORD, userInfoBean, pwmSession );
 
-            ForgottenPasswordUtil.sendUnlockNoticeEmail( pwmRequest, forgottenPasswordBean );
+            ForgottenPasswordUtil.sendUnlockNoticeEmail( pwmRequest.commonValues(), forgottenPasswordBean );
 
             pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_UnlockAccount );
         }
@@ -1374,7 +1372,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         try
         {
             ForgottenPasswordUtil.initForgottenPasswordBean(
-                    pwmRequest,
+                    pwmRequest.commonValues(),
                     forgottenPasswordBean.getUserIdentity(),
                     forgottenPasswordBean
             );
@@ -1399,7 +1397,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     {
         LOGGER.debug( pwmRequest, () -> "attempting to forward request to handle verification method " + method.toString() );
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        ForgottenPasswordUtil.verifyRequirementsForAuthMethod( pwmRequest, forgottenPasswordBean, method );
+        ForgottenPasswordUtil.verifyRequirementsForAuthMethod( pwmRequest.commonValues(), forgottenPasswordBean, method );
         switch ( method )
         {
             case PREVIOUS_AUTH:
@@ -1428,7 +1426,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             {
                 pwmRequest.setAttribute(
                         PwmRequestAttribute.ForgottenPasswordOtpRecord,
-                        ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean ).getOtpUserRecord()
+                        ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean ).getOtpUserRecord()
                 );
                 pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ENTER_OTP );
             }
@@ -1437,7 +1435,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             case TOKEN:
             {
                 final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
-                final List<TokenDestinationItem> tokenDestinations = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+                final List<TokenDestinationItem> tokenDestinations = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest.commonValues(), forgottenPasswordBean );
                 if ( progress.getTokenDestination() == null )
                 {
                     final boolean autoSelect = Boolean.parseBoolean( pwmRequest.getConfig().readAppProperty( AppProperty.FORGOTTEN_PASSWORD_TOKEN_AUTO_SELECT_DEST ) );
@@ -1456,8 +1454,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
                 if ( !progress.isTokenSent() )
                 {
-                    final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-                    ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, progress.getTokenDestination() );
+                    final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
+                    ForgottenPasswordUtil.initializeAndSendToken( pwmRequest.commonValues(), userInfo, progress.getTokenDestination() );
                     progress.setTokenSent( true );
                 }
 
@@ -1471,7 +1469,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             case REMOTE_RESPONSES:
             {
-                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest.commonValues(), forgottenPasswordBean );
                 final VerificationMethodSystem remoteMethod;
                 if ( forgottenPasswordBean.getProgress().getRemoteRecoveryMethod() == null )
                 {
@@ -1519,10 +1517,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             throws ServletException, PwmUnrecoverableException, IOException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+        final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest.commonValues(), forgottenPasswordBean );
         pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, new ArrayList<>( destItems ) );
 
-        if ( ForgottenPasswordUtil.hasOtherMethodChoices( pwmRequest, forgottenPasswordBean, IdentityVerificationMethod.TOKEN ) )
+        if ( ForgottenPasswordUtil.hasOtherMethodChoices( pwmRequest.commonValues(), forgottenPasswordBean, IdentityVerificationMethod.TOKEN ) )
         {
             pwmRequest.setAttribute( PwmRequestAttribute.GoBackAction, ResetAction.clearActionChoice.name() );
         }
@@ -1539,7 +1537,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                 forgottenPasswordBean
         );
 
-        final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+        final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest.commonValues(), forgottenPasswordBean );
 
         ResetAction goBackAction = null;
 
@@ -1548,7 +1546,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         {
             goBackAction = ResetAction.clearTokenDestination;
         }
-        else if ( ForgottenPasswordUtil.hasOtherMethodChoices( pwmRequest, forgottenPasswordBean, IdentityVerificationMethod.TOKEN ) )
+        else if ( ForgottenPasswordUtil.hasOtherMethodChoices( pwmRequest.commonValues(), forgottenPasswordBean, IdentityVerificationMethod.TOKEN ) )
         {
             goBackAction = ResetAction.clearActionChoice;
         }

+ 414 - 0
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStageProcessor.java

@@ -0,0 +1,414 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.servlet.forgottenpw;
+
+import password.pwm.PwmApplication;
+import password.pwm.bean.PasswordStatus;
+import password.pwm.bean.SessionLabel;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
+import password.pwm.config.profile.ForgottenPasswordProfile;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.bean.ForgottenPasswordBean;
+import password.pwm.http.bean.ForgottenPasswordStage;
+import password.pwm.ldap.UserInfo;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.operations.PasswordUtility;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+class ForgottenPasswordStageProcessor
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ForgottenPasswordStateMachine.class );
+    private static final List<NextStageProcessor> NEXT_STAGE_PROCESSORS;
+
+    static ForgottenPasswordStage nextStage( final ForgottenPasswordStateMachine stateMachine )
+            throws PwmUnrecoverableException
+    {
+        for ( final NextStageProcessor nextStageProcessor : NEXT_STAGE_PROCESSORS )
+        {
+            final Optional<ForgottenPasswordStage> nextStage = nextStageProcessor.nextStage( stateMachine );
+            if ( nextStage.isPresent() )
+            {
+                return nextStage.get();
+            }
+        }
+        return ForgottenPasswordStage.IDENTIFICATION;
+    }
+
+    static
+    {
+        final List<NextStageProcessor> list = new ArrayList<>();
+        list.add( new StageProcessor1() );
+        list.add( new StageProcessor2() );
+        list.add( new StageProcessor3() );
+        list.add( new StageProcessor4() );
+        list.add( new StageProcessor5() );
+        list.add( new StageProcessor6() );
+        list.add( new StageProcessor7() );
+        NEXT_STAGE_PROCESSORS = Collections.unmodifiableList( list );
+    }
+
+    private interface NextStageProcessor
+    {
+        Optional<ForgottenPasswordStage> nextStage( ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException;
+    }
+
+    private static class StageProcessor1 implements  NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+        {
+            final CommonValues commonValues = stateMachine.getCommonValues();
+
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            stateMachine.getRequestFlags().clear();
+
+            // check if completed
+            if ( forgottenPasswordBean.getProgress().getExecutedRecoveryAction() != null )
+            {
+                return Optional.of( ForgottenPasswordStage.COMPLETE );
+            }
+
+            // check locale
+            if ( forgottenPasswordBean.getUserLocale() == null )
+            {
+                forgottenPasswordBean.setUserLocale( commonValues.getLocale() );
+            }
+
+            if ( !Objects.equals( forgottenPasswordBean.getUserLocale(), commonValues.getLocale() ) )
+            {
+                LOGGER.debug( commonValues.getSessionLabel(), () -> "user locale has changed, resetting forgotten password state" );
+                stateMachine.clear();
+                return Optional.of( ForgottenPasswordStage.IDENTIFICATION );
+            }
+
+            // check for identified user;
+            if ( forgottenPasswordBean.getUserIdentity() == null && !forgottenPasswordBean.isBogusUser() )
+            {
+                return Optional.of( ForgottenPasswordStage.IDENTIFICATION );
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    private static class StageProcessor2 implements NextStageProcessor
+    {
+
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final CommonValues commonValues = stateMachine.getCommonValues();
+            final PwmApplication pwmApplication = commonValues.getPwmApplication();
+            final SessionLabel sessionLabel = commonValues.getSessionLabel();
+            final Configuration config = pwmApplication.getConfig();
+
+            final ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
+            final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
+
+            if ( forgottenPasswordBean.isBogusUser() )
+            {
+                return Optional.of( ForgottenPasswordStage.VERIFICATION );
+            }
+
+            final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmApplication, forgottenPasswordBean );
+            {
+                final Map<String, ForgottenPasswordProfile> profileIDList = config.getForgottenPasswordProfiles();
+                final String profileDebugMsg = forgottenPasswordProfile != null && profileIDList != null && profileIDList.size() > 1
+                        ? " profile=" + forgottenPasswordProfile.getIdentifier() + ", "
+                        : "";
+                LOGGER.trace( sessionLabel, () -> "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 ) );
+            }
+
+            // dispatch required auth methods.
+            for ( final IdentityVerificationMethod method : recoveryFlags.getRequiredAuthMethods() )
+            {
+                if ( !progress.getSatisfiedMethods().contains( method ) )
+                {
+                    return Optional.of( ForgottenPasswordStage.VERIFICATION );
+                }
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    static class StageProcessor3 implements NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
+
+            if ( Objects.equals( progress.getInProgressVerificationMethod(), IdentityVerificationMethod.TOKEN ) )
+            {
+                if ( progress.getTokenDestination() == null )
+                {
+                    return Optional.of( ForgottenPasswordStage.TOKEN_CHOICE );
+                }
+            }
+
+            // redirect if an verification method is in progress
+            if ( progress.getInProgressVerificationMethod() != null )
+            {
+                if ( progress.getSatisfiedMethods().contains( progress.getInProgressVerificationMethod() ) )
+                {
+                    progress.setInProgressVerificationMethod( null );
+                }
+                else
+                {
+                    stateMachine.getRequestFlags().put( PwmRequestAttribute.ForgottenPasswordOptionalPageView, "true" );
+                    return Optional.of( ForgottenPasswordStage.VERIFICATION );
+                }
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    static class StageProcessor4 implements NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final CommonValues commonValues = stateMachine.getCommonValues();
+            final SessionLabel sessionLabel = commonValues.getSessionLabel();
+
+            final ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
+            final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
+
+            // 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(
+                            commonValues,
+                            forgottenPasswordBean
+                    );
+
+                    // for rest method, fail if any required methods are not supported
+                    {
+                        final Set<IdentityVerificationMethod> tempSet = new HashSet<>( remainingAvailableOptionalMethods );
+                        tempSet.removeAll( ForgottenPasswordStateMachine.supportedVerificationMethods() );
+                        if ( !tempSet.isEmpty() )
+                        {
+                            final IdentityVerificationMethod unsupportedMethod = tempSet.iterator().next();
+                            final String msg = "verification method " + unsupportedMethod + " is configured but is not available for use by REST service";
+                            throw new PwmUnrecoverableException( PwmError.CONFIG_FORMAT_ERROR, msg );
+                        }
+                    }
+
+                    if ( remainingAvailableOptionalMethods.isEmpty() )
+                    {
+                        final String errorMsg = "additional optional verification methods are needed"
+                                + ", however all available optional verification methods have been satisfied by user";
+                        final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_RECOVERY_SEQUENCE_INCOMPLETE, errorMsg );
+                        LOGGER.error( sessionLabel, errorInformation );
+                        throw new PwmUnrecoverableException( errorInformation );
+                    }
+                    else
+                    {
+                        if ( remainingAvailableOptionalMethods.size() == 1 )
+                        {
+                            final IdentityVerificationMethod remainingMethod = remainingAvailableOptionalMethods.iterator().next();
+                            LOGGER.debug( sessionLabel, () -> "only 1 remaining available optional verification method, will redirect to " + remainingMethod.toString() );
+                            progress.setInProgressVerificationMethod( remainingMethod );
+                            return Optional.of( ForgottenPasswordStage.VERIFICATION );
+                        }
+                    }
+                    return Optional.of( ForgottenPasswordStage.METHOD_CHOICE );
+                }
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    static class StageProcessor5 implements NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final CommonValues commonValues = stateMachine.getCommonValues();
+            final SessionLabel sessionLabel = commonValues.getSessionLabel();
+
+            final ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
+            final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
+
+            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_RECOVERY_SEQUENCE_INCOMPLETE, errorMsg );
+                LOGGER.error( sessionLabel, errorInformation );
+                stateMachine.clear();
+                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( sessionLabel, errorInformation );
+                    stateMachine.clear();
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    static class StageProcessor6 implements NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final CommonValues commonValues = stateMachine.getCommonValues();
+            final PwmApplication pwmApplication = commonValues.getPwmApplication();
+            final SessionLabel sessionLabel = commonValues.getSessionLabel();
+
+            if ( !forgottenPasswordBean.getProgress().isAllPassed() )
+            {
+                forgottenPasswordBean.getProgress().setAllPassed( true );
+                pwmApplication.getStatisticsManager().incrementValue( Statistic.RECOVERY_SUCCESSES );
+            }
+
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordBean );
+            if ( userInfo == null )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "unable to load userInfo while processing forgotten password controller" );
+            }
+
+            // check if user's pw is within min lifetime window
+            final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmApplication, forgottenPasswordBean );
+            final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                    PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                    RecoveryMinLifetimeOption.class
+            );
+            if ( minLifetimeOption == RecoveryMinLifetimeOption.NONE
+                    || (
+                    !userInfo.isPasswordLocked()
+                            &&  minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY )
+            )
+            {
+                if ( userInfo.isWithinPasswordMinimumLifetime() )
+                {
+                    PasswordUtility.throwPasswordTooSoonException( userInfo, sessionLabel );
+                }
+            }
+
+            return Optional.empty();
+        }
+    }
+
+    static class StageProcessor7 implements NextStageProcessor
+    {
+        public Optional<ForgottenPasswordStage> nextStage( final ForgottenPasswordStateMachine stateMachine )
+                throws PwmUnrecoverableException
+        {
+            final ForgottenPasswordBean forgottenPasswordBean = stateMachine.getForgottenPasswordBean();
+            final CommonValues commonValues = stateMachine.getCommonValues();
+            final PwmApplication pwmApplication = commonValues.getPwmApplication();
+            final SessionLabel sessionLabel = commonValues.getSessionLabel();
+            final Configuration config = pwmApplication.getConfig();
+
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordBean );
+
+            final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmApplication, forgottenPasswordBean );
+
+            final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                    PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                    RecoveryMinLifetimeOption.class
+            );
+            final boolean disallowAllButUnlock = minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY
+                    && userInfo.isPasswordLocked();
+
+            LOGGER.trace( sessionLabel, () -> "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 )
+            {
+                if ( disallowAllButUnlock )
+                {
+                    PasswordUtility.throwPasswordTooSoonException( userInfo, sessionLabel );
+                }
+                return Optional.of( ForgottenPasswordStage.COMPLETE );
+            }
+
+            if ( forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.RECOVERY_ALLOW_UNLOCK ) )
+            {
+                final PasswordStatus passwordStatus = userInfo.getPasswordStatus();
+
+                if ( !passwordStatus.isExpired() && !passwordStatus.isPreExpired() )
+                {
+                    if ( userInfo.isPasswordLocked() )
+                    {
+                        final boolean inhibitReset = minLifetimeOption != RecoveryMinLifetimeOption.ALLOW
+                                && userInfo.isWithinPasswordMinimumLifetime();
+
+                        stateMachine.getRequestFlags().put( PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset, String.valueOf( inhibitReset ) );
+                        return Optional.of( ForgottenPasswordStage.ACTION_CHOICE );
+                    }
+                }
+            }
+
+            return Optional.of( ForgottenPasswordStage.NEW_PASSWORD );
+        }
+    }
+
+}

+ 1116 - 0
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordStateMachine.java

@@ -0,0 +1,1116 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.servlet.forgottenpw;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.cr.Challenge;
+import com.novell.ldapchai.cr.ResponseSet;
+import com.novell.ldapchai.cr.bean.ChallengeBean;
+import com.novell.ldapchai.cr.bean.ChallengeSetBean;
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.SelectableContextMode;
+import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.profile.PwmPasswordPolicy;
+import password.pwm.config.profile.PwmPasswordRule;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmDataValidationException;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.bean.ForgottenPasswordBean;
+import password.pwm.http.bean.ForgottenPasswordStage;
+import password.pwm.http.tag.PasswordRequirementsTag;
+import password.pwm.i18n.Display;
+import password.pwm.i18n.Message;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.ldap.auth.AuthenticationUtility;
+import password.pwm.ldap.auth.SessionAuthenticator;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.svc.token.TokenPayload;
+import password.pwm.svc.token.TokenService;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
+import password.pwm.util.PasswordData;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.i18n.LocaleHelper;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.operations.PasswordUtility;
+import password.pwm.util.operations.otp.OTPUserRecord;
+import password.pwm.ws.server.PresentableForm;
+import password.pwm.ws.server.PresentableFormRow;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ForgottenPasswordStateMachine
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ForgottenPasswordStateMachine.class );
+
+    private static final Map<ForgottenPasswordStage, StageHandler> STAGE_HANDLERS;
+
+    private static final String PARAM_PASSWORD = "password1";
+    private static final String PARAM_PASSWORD_CONFIRM = "password2";
+
+    static
+    {
+        final Map<ForgottenPasswordStage, StageHandler> stageStateHandlerMap = new HashMap<>();
+        stageStateHandlerMap.put( ForgottenPasswordStage.IDENTIFICATION, new IdentificationStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.METHOD_CHOICE, new MethodChoiceStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.TOKEN_CHOICE, new TokenChoiceStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.VERIFICATION, new VerificationStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.ACTION_CHOICE, new ActionChoiceStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.NEW_PASSWORD, new PasswordChangeStageHandler() );
+        stageStateHandlerMap.put( ForgottenPasswordStage.COMPLETE, new CompletedStageHandler() );
+        STAGE_HANDLERS = Collections.unmodifiableMap( stageStateHandlerMap );
+    }
+
+    interface StageHandler
+    {
+        void applyForm( ForgottenPasswordStateMachine forgottenPasswordStateMachine, Map<String, String> formValues ) throws PwmUnrecoverableException;
+
+        PresentableForm generateForm( ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+                throws PwmUnrecoverableException;
+    }
+
+    private ForgottenPasswordBean forgottenPasswordBean;
+    private final CommonValues commonValues;
+    private final Map<PwmRequestAttribute, String> requestFlags = new ConcurrentHashMap<>();
+
+    public ForgottenPasswordStateMachine(
+            final CommonValues commonValues,
+            final ForgottenPasswordBean forgottenPasswordBean
+    )
+            throws PwmUnrecoverableException
+    {
+        this.commonValues = commonValues;
+        updateBean( forgottenPasswordBean );
+        nextStage();
+    }
+
+    private void updateBean( final ForgottenPasswordBean forgottenPasswordBean )
+    {
+        this.forgottenPasswordBean = forgottenPasswordBean == null
+                ? new ForgottenPasswordBean()
+                : JsonUtil.cloneUsingJson( forgottenPasswordBean, ForgottenPasswordBean.class );
+    }
+
+    public ForgottenPasswordBean getForgottenPasswordBean()
+    {
+        return forgottenPasswordBean;
+    }
+
+    CommonValues getCommonValues()
+    {
+        return commonValues;
+    }
+
+    static Set<IdentityVerificationMethod> supportedVerificationMethods()
+    {
+        return Collections.unmodifiableSet( VerificationStageHandler.VERIFICATION_HANDLERS.keySet() );
+    }
+
+    public Map<PwmRequestAttribute, String> getRequestFlags()
+    {
+        return requestFlags;
+    }
+
+
+    public ForgottenPasswordStage nextStage()
+            throws PwmUnrecoverableException
+    {
+        return ForgottenPasswordStageProcessor.nextStage( this );
+    }
+
+
+    void clear()
+    {
+        forgottenPasswordBean = new ForgottenPasswordBean();
+        requestFlags.clear();
+    }
+
+    public void applyFormValues( final Map<String, String> values ) throws PwmUnrecoverableException
+    {
+        final ForgottenPasswordStage stage = nextStage();
+
+        final StageHandler handler = STAGE_HANDLERS.get( stage );
+
+        if ( handler != null )
+        {
+            handler.applyForm( this, values );
+        }
+        else
+        {
+            throw new IllegalStateException( "unhandled stage for apply form: " + stage.name() );
+        }
+
+        nextStage();
+    }
+
+    public PresentableForm nextForm()
+            throws PwmUnrecoverableException
+    {
+        final ForgottenPasswordStage stage = nextStage();
+
+        final StageHandler handler = STAGE_HANDLERS.get( stage );
+
+        if ( handler != null )
+        {
+            return handler.generateForm( this );
+        }
+        else
+        {
+            throw new IllegalStateException( "unhandled stage for next form: " + stage.name() );
+        }
+    }
+
+    static class CompletedStageHandler implements StageHandler
+    {
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+        {
+            // action is complete, nothing further to execute
+        }
+
+        @Override
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            return PresentableForm.builder()
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ChangePassword, commonValues.getConfig() ) )
+                    .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Message.Success_PasswordChange, commonValues.getConfig() ) )
+                    .build();
+        }
+    }
+
+    static class PasswordChangeStageHandler implements StageHandler
+    {
+        private static final String PARAM_VERIFICATION_CHOICE = "verificationOnly";
+
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final PasswordData password1 = PasswordData.forStringValue( formValues.get( PARAM_PASSWORD ) );
+            final PasswordData password2 = PasswordData.forStringValue( formValues.get( PARAM_PASSWORD_CONFIRM ) );
+
+            final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean().getUserIdentity() );
+            final boolean caseSensitive = userInfo.getPasswordPolicy().getRuleHelper().readBooleanValue(
+                    PwmPasswordRule.CaseSensitive );
+            if ( PasswordUtility.PasswordCheckInfo.MatchStatus.MATCH != PasswordUtility.figureMatchStatus( caseSensitive,
+                    password1, password2 ) )
+            {
+                throw PwmUnrecoverableException.newException( PwmError.PASSWORD_DOESNOTMATCH, null );
+            }
+
+            final boolean verifyOnly = Boolean.parseBoolean( formValues.get( PARAM_VERIFICATION_CHOICE ) );
+
+            try
+            {
+                if ( verifyOnly )
+                {
+                    final PasswordUtility.PasswordCheckInfo passwordCheckInfo = PasswordUtility.checkEnteredPassword(
+                            commonValues.getPwmApplication(),
+                            commonValues.getLocale(),
+                            commonValues.getPwmApplication().getProxiedChaiUser( userInfo.getUserIdentity() ),
+                            userInfo,
+                            null,
+                            password1,
+                            password2
+                    );
+
+                    if ( !passwordCheckInfo.isPassed() )
+                    {
+                        final PwmError pwmError = PwmError.forErrorNumber( passwordCheckInfo.getErrorCode() );
+                        throw PwmUnrecoverableException.newException( pwmError, passwordCheckInfo.getMessage() );
+                    }
+                }
+                else
+                {
+                    PasswordUtility.setPassword(
+                            forgottenPasswordStateMachine.getCommonValues().getPwmApplication(),
+                            forgottenPasswordStateMachine.getCommonValues().getSessionLabel(),
+                            forgottenPasswordStateMachine.getCommonValues().getPwmApplication().getProxyChaiProvider( userInfo.getUserIdentity().getLdapProfileID() ),
+                            userInfo,
+                            null,
+                            password1 );
+                }
+            }
+            catch ( PwmOperationalException e )
+            {
+                throw new PwmUnrecoverableException( e.getErrorInformation() );
+            }
+            catch ( ChaiUnavailableException e )
+            {
+                throw PwmUnrecoverableException.fromChaiException( e );
+            }
+
+            forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().setExecutedRecoveryAction( RecoveryAction.RESETPW );
+        }
+
+        @Override
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final Configuration config = forgottenPasswordStateMachine.getCommonValues().getConfig();
+            final Locale locale = forgottenPasswordStateMachine.getCommonValues().getLocale();
+            final UserIdentity userIdentity = forgottenPasswordStateMachine.getForgottenPasswordBean().getUserIdentity();
+            final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy( commonValues, userIdentity );
+            final MacroMachine macroMachine = MacroMachine.forUser( commonValues, userIdentity );
+            final PwmPasswordPolicy pwmPasswordPolicy = userInfo.getPasswordPolicy();
+
+            final boolean valueMasking = commonValues.getConfig().readSettingAsBoolean( PwmSetting.DISPLAY_MASK_PASSWORD_FIELDS );
+            final FormConfiguration.Type formType = valueMasking
+                    ? FormConfiguration.Type.password
+                    : FormConfiguration.Type.text;
+
+            final List<PresentableFormRow> formRows = new ArrayList<>();
+            formRows.add( PresentableFormRow.builder()
+                    .name( PARAM_PASSWORD )
+                    .type( formType )
+                    .label( LocaleHelper.getLocalizedMessage( locale, Display.Field_Password, config ) )
+                    .required( true )
+                    .build() );
+            formRows.add( PresentableFormRow.builder()
+                    .name( PARAM_PASSWORD_CONFIRM )
+                    .type( formType )
+                    .label( LocaleHelper.getLocalizedMessage( locale, Display.Field_ConfirmPassword, config ) )
+                    .required( true )
+                    .build() );
+
+            final List<String> passwordRequirementsList = PasswordRequirementsTag.getPasswordRequirementsStrings(
+                    pwmPasswordPolicy,
+                    commonValues.getConfig(),
+                    commonValues.getLocale(),
+                    macroMachine );
+
+            final String ruleDelimiter = commonValues.getConfig().readAppProperty( AppProperty.REST_SERVER_FORGOTTEN_PW_RULE_DELIMITER );
+            final String ruleText = StringUtil.collectionToString( passwordRequirementsList, ruleDelimiter );
+
+            return PresentableForm.builder()
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ChangePassword, commonValues.getConfig() ) )
+                    .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_ChangePassword, commonValues.getConfig() ) )
+                    .messageDetail( ruleText )
+                    .formRows( formRows )
+                    .build();
+        }
+    }
+
+    static class ActionChoiceStageHandler implements StageHandler
+    {
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+        {
+
+        }
+
+        @Override
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+        {
+            return PresentableForm.builder()
+                    .message( "you win!" )
+                    .build();
+        }
+    }
+
+    static class TokenChoiceStageHandler implements StageHandler
+    {
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                throws PwmUnrecoverableException
+        {
+            final List<TokenDestinationItem> tokenDestinationItems = ForgottenPasswordUtil.figureAvailableTokenDestinations(
+                    forgottenPasswordStateMachine.getCommonValues(),
+                    forgottenPasswordStateMachine.getForgottenPasswordBean() );
+
+            final Optional<TokenDestinationItem> selectedItem = TokenDestinationItem.tokenDestinationItemForID( tokenDestinationItems, formValues.get( PwmConstants.PARAM_TOKEN ) );
+            if ( selectedItem.isPresent() )
+            {
+                forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().setTokenDestination( selectedItem.get() );
+
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo(
+                        forgottenPasswordStateMachine.getCommonValues(),
+                        forgottenPasswordStateMachine.getForgottenPasswordBean() );
+                ForgottenPasswordUtil.initializeAndSendToken( forgottenPasswordStateMachine.getCommonValues(), userInfo, selectedItem.get() );
+                forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().setTokenSent( true );
+            }
+
+        }
+
+        @Override
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final List<TokenDestinationItem> tokenDestinationItems = ForgottenPasswordUtil.figureAvailableTokenDestinations(
+                    forgottenPasswordStateMachine.getCommonValues(),
+                    forgottenPasswordStateMachine.getForgottenPasswordBean() );
+
+            final Map<String, String> selectOptions = new LinkedHashMap<>();
+
+            for ( final TokenDestinationItem item : tokenDestinationItems )
+            {
+                selectOptions.put( item.getId(), item.longDisplay( commonValues.getLocale(), commonValues.getConfig() ) );
+            }
+
+            final PresentableFormRow formRow = PresentableFormRow.builder()
+                    .name( PwmConstants.PARAM_TOKEN )
+                    .type( FormConfiguration.Type.select )
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Button_Select, commonValues.getConfig() ) )
+                    .selectOptions( selectOptions )
+                    .required( true )
+                    .build();
+
+            return PresentableForm.builder()
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                    .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverTokenSendChoices, commonValues.getConfig() ) )
+                    .formRow( formRow )
+                    .build();
+        }
+    }
+
+    static class VerificationStageHandler implements StageHandler
+    {
+        private static final Map<IdentityVerificationMethod, StageHandler> VERIFICATION_HANDLERS;
+
+        static
+        {
+            final Map<IdentityVerificationMethod, StageHandler> stageStateHandlerMap = new HashMap<>();
+            stageStateHandlerMap.put( IdentityVerificationMethod.CHALLENGE_RESPONSES, new ChallengeResponseHandler() );
+            stageStateHandlerMap.put( IdentityVerificationMethod.ATTRIBUTES, new AttributeVerificationHandler() );
+            stageStateHandlerMap.put( IdentityVerificationMethod.TOKEN, new TokenVerificationHandler() );
+            stageStateHandlerMap.put( IdentityVerificationMethod.OTP, new OTPVerificationHandler() );
+            VERIFICATION_HANDLERS = Collections.unmodifiableMap( stageStateHandlerMap );
+        }
+
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                throws PwmUnrecoverableException
+        {
+            final IdentityVerificationMethod method = forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getInProgressVerificationMethod();
+
+            final StageHandler handler = VERIFICATION_HANDLERS.get( method );
+
+            if ( handler != null )
+            {
+                handler.applyForm( forgottenPasswordStateMachine, formValues );
+            }
+            else
+            {
+                throw new IllegalStateException( "unhandled method for apply form: " + method.name() );
+            }
+        }
+
+        @Override
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+                throws PwmUnrecoverableException
+        {
+            final IdentityVerificationMethod method = forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getInProgressVerificationMethod();
+
+            final StageHandler handler = VERIFICATION_HANDLERS.get( method );
+
+            if ( handler != null )
+            {
+                return handler.generateForm( forgottenPasswordStateMachine );
+            }
+            else
+            {
+                throw new IllegalStateException( "unhandled method for next form: " + method.name() );
+            }
+        }
+
+        static class OTPVerificationHandler implements StageHandler
+        {
+            public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                    throws PwmUnrecoverableException
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final String userEnteredCode = formValues.get( PwmConstants.PARAM_OTP_TOKEN );
+
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean() );
+                final OTPUserRecord otpUserRecord = userInfo.getOtpUserRecord();
+
+                ErrorInformation errorInformation = null;
+
+                boolean otpPassed = false;
+                if ( otpUserRecord != null && !StringUtil.isEmpty( userEnteredCode ) )
+                {
+                    LOGGER.trace( commonValues.getSessionLabel(), () -> "checking entered OTP for user " + userInfo.getUserIdentity().toDisplayString() );
+                    try
+                    {
+                        // forces service to use proxy account to update (write) updated otp record if necessary.
+                        otpPassed = commonValues.getPwmApplication().getOtpService().validateToken(
+                                null,
+                                userInfo.getUserIdentity(),
+                                otpUserRecord,
+                                userEnteredCode,
+                                true
+                        );
+                    }
+                    catch ( PwmOperationalException e )
+                    {
+                        errorInformation = new ErrorInformation( PwmError.ERROR_INCORRECT_OTP_TOKEN, e.getErrorInformation().toDebugStr() );
+                    }
+                }
+
+                final String passedStr = otpPassed ? "passed" : "failed";
+                LOGGER.trace( commonValues.getSessionLabel(), () -> "one time password validation has " + passedStr + " for user "
+                        + userInfo.getUserIdentity().toDisplayString() );
+
+                if ( otpPassed )
+                {
+                    commonValues.getPwmApplication().getStatisticsManager().incrementValue( Statistic.RECOVERY_OTP_PASSED );
+                    forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.OTP );
+                }
+                else
+                {
+                    errorInformation = errorInformation == null
+                            ? new ErrorInformation( PwmError.ERROR_INCORRECT_OTP_TOKEN )
+                            : errorInformation;
+                    commonValues.getPwmApplication().getStatisticsManager().incrementValue( Statistic.RECOVERY_OTP_FAILED );
+                    handleUserVerificationBadAttempt( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean(), errorInformation );
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+            }
+
+            public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine ) throws PwmUnrecoverableException
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo(
+                        forgottenPasswordStateMachine.getCommonValues(),
+                        forgottenPasswordStateMachine.getForgottenPasswordBean() );
+
+                final OTPUserRecord otpUserRecord = userInfo == null ? null : userInfo.getOtpUserRecord();
+
+                final String identifier = otpUserRecord == null
+                        ? null
+                        : otpUserRecord.getIdentifier();
+
+                final String message;
+                if ( StringUtil.isEmpty( identifier ) )
+                {
+                    message = LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverOTP, commonValues.getConfig() );
+                }
+                else
+                {
+                    message = LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverOTPIdentified, commonValues.getConfig(), new String[]
+                            {
+                                    identifier,
+                            }
+                    );
+                }
+
+                final PresentableFormRow formRow = PresentableFormRow.builder()
+                        .name( PwmConstants.PARAM_OTP_TOKEN )
+                        .type( FormConfiguration.Type.text )
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Field_Code, commonValues.getConfig() ) )
+                        .required( true )
+                        .build();
+
+                return PresentableForm.builder()
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                        .message( message )
+                        .formRow( formRow )
+                        .build();
+            }
+        }
+
+        static class TokenVerificationHandler implements StageHandler
+        {
+            public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues ) throws PwmUnrecoverableException
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final TokenDestinationItem tokenDestinationItem = forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getTokenDestination();
+                final String userEnteredCode = formValues.get( PwmConstants.PARAM_TOKEN );
+
+                ErrorInformation errorInformation = null;
+                try
+                {
+                    final TokenPayload tokenPayload = TokenUtil.checkEnteredCode(
+                            commonValues,
+                            userEnteredCode,
+                            tokenDestinationItem,
+                            forgottenPasswordStateMachine.getForgottenPasswordBean().getUserIdentity(),
+                            TokenType.FORGOTTEN_PW,
+                            TokenService.TokenEntryType.unauthenticated
+                    );
+
+                    if ( tokenPayload == null )
+                    {
+                        throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, "token incorrect" );
+                    }
+
+                    forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.TOKEN );
+                    StatisticsManager.incrementStat( commonValues.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED );
+
+                    if ( commonValues.getConfig().readSettingAsBoolean( PwmSetting.DISPLAY_TOKEN_SUCCESS_BUTTON ) )
+                    {
+                        return;
+                    }
+                }
+                catch ( PwmUnrecoverableException e )
+                {
+                    LOGGER.debug( commonValues.getSessionLabel(), () -> "error while checking entered token: " );
+                    errorInformation = e.getErrorInformation();
+                }
+
+                if ( !forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getSatisfiedMethods().contains( IdentityVerificationMethod.TOKEN ) )
+                {
+                    if ( errorInformation == null )
+                    {
+                        errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
+                    }
+                    handleUserVerificationBadAttempt( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean(), errorInformation );
+                }
+
+                if ( errorInformation != null )
+                {
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+            }
+
+            public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final boolean valueMasking = commonValues.getConfig().readSettingAsBoolean( PwmSetting.TOKEN_ENABLE_VALUE_MASKING );
+                final FormConfiguration.Type formType = valueMasking
+                        ? FormConfiguration.Type.password
+                        : FormConfiguration.Type.text;
+
+                final String tokenDisplay = forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getTokenDestination().getDisplay();
+                final String message = LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverEnterCode, commonValues.getConfig(), new String[]
+                        {
+                                tokenDisplay,
+                        }
+                );
+
+                final PresentableFormRow formRow = PresentableFormRow.builder()
+                        .name( PwmConstants.PARAM_TOKEN )
+                        .type( formType )
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Field_VerificationMethodToken, commonValues.getConfig() ) )
+                        .required( true )
+                        .build();
+
+                return PresentableForm.builder()
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                        .message( message )
+                        .formRow( formRow )
+                        .build();
+            }
+        }
+
+        static class ChallengeResponseHandler implements StageHandler
+        {
+
+            public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                    throws PwmUnrecoverableException
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean() );
+                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_INTERNAL, errorMsg );
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+
+                // read the supplied responses from the user
+                final Map<Challenge, String> crMap = ForgottenPasswordUtil.readResponsesFromMap(
+                        forgottenPasswordStateMachine.getForgottenPasswordBean().getPresentableChallengeSet(),
+                        formValues
+                );
+
+                final boolean responsesPassed;
+                try
+                {
+                    responsesPassed = responseSet.test( crMap );
+                }
+                catch ( ChaiUnavailableException e )
+                {
+                    throw PwmUnrecoverableException.fromChaiException( e );
+                }
+
+                if ( responsesPassed )
+                {
+                    final UserIdentity userIdentity = forgottenPasswordStateMachine.getForgottenPasswordBean().getUserIdentity();
+                    LOGGER.debug( commonValues.getSessionLabel(), () -> "user '" + userIdentity + "' has supplied correct responses" );
+                    forgottenPasswordStateMachine.getForgottenPasswordBean().getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.CHALLENGE_RESPONSES );
+                }
+                else
+                {
+                    final String errorMsg = "incorrect response to one or more challenges";
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE, errorMsg );
+                    handleUserVerificationBadAttempt( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean(), errorInformation );
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+            }
+
+            public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final ChallengeSetBean challengeSetBean = forgottenPasswordStateMachine.getForgottenPasswordBean().getPresentableChallengeSet();
+                final List<PresentableFormRow> formRows = new ArrayList<>();
+
+                int loopCounter = 0;
+                for ( final ChallengeBean challengeBean : challengeSetBean.getChallenges() )
+                {
+                    loopCounter++;
+                    formRows.add( PresentableFormRow.builder()
+                            .name( PwmConstants.PARAM_RESPONSE_PREFIX + loopCounter )
+                            .type( FormConfiguration.Type.password )
+                            .label( challengeBean.getChallengeText() )
+                            .required( true )
+                            .build()
+                    );
+                }
+                return PresentableForm.builder()
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                        .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverPassword, commonValues.getConfig() ) )
+                        .formRows( formRows )
+                        .build();
+            }
+
+        }
+
+        static class AttributeVerificationHandler implements StageHandler
+        {
+            public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formData )
+                    throws PwmUnrecoverableException
+            {
+                final PwmApplication pwmApplication = forgottenPasswordStateMachine.getCommonValues().getPwmApplication();
+                final Locale locale = forgottenPasswordStateMachine.getCommonValues().getLocale();
+                final SessionLabel sessionLabel = forgottenPasswordStateMachine.getCommonValues().getSessionLabel();
+                final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordStateMachine.getForgottenPasswordBean();
+
+                if ( forgottenPasswordBean.isBogusUser() )
+                {
+                    final FormConfiguration formConfiguration = forgottenPasswordBean.getAttributeForm().iterator().next();
+
+                    if ( forgottenPasswordBean.getUserSearchValues() != null )
+                    {
+                        final List<FormConfiguration> formConfigurations = pwmApplication.getConfig().readSettingAsForm( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM );
+                        final Map<FormConfiguration, String> formMap = FormUtility.asFormConfigurationMap( formConfigurations, forgottenPasswordBean.getUserSearchValues() );
+                        pwmApplication.getIntruderManager().convenience().markAttributes( formMap, forgottenPasswordStateMachine.getCommonValues().getSessionLabel() );
+                    }
+
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE,
+                            "incorrect value for attribute '" + formConfiguration.getName() + "'", new String[]
+                            {
+                                    formConfiguration.getLabel( locale ),
+                            }
+                    );
+
+                    throw new PwmUnrecoverableException( errorInformation );
+                }
+
+                if ( forgottenPasswordBean.getUserIdentity() == null )
+                {
+                    return;
+                }
+                final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
+
+                try
+                {
+                    // check attributes
+                    final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
+
+                    final List<FormConfiguration> requiredAttributesForm = forgottenPasswordBean.getAttributeForm();
+
+                    if ( requiredAttributesForm.isEmpty() )
+                    {
+                        return;
+                    }
+
+                    final Map<FormConfiguration, String> formValues = FormUtility.readFormValuesFromMap(
+                            formData, requiredAttributesForm, locale );
+
+                    for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
+                    {
+                        final FormConfiguration formConfiguration = entry.getKey();
+                        final String attrName = formConfiguration.getName();
+
+                        try
+                        {
+                            if ( theUser.compareStringAttribute( attrName, entry.getValue() ) )
+                            {
+                                LOGGER.trace( sessionLabel, () -> "successful validation of ldap attribute value for '" + attrName + "'" );
+                            }
+                            else
+                            {
+                                throw new PwmDataValidationException( new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE,
+                                        "incorrect value for '" + attrName + "'", new String[]
+                                        {
+                                                formConfiguration.getLabel( locale ),
+                                        }
+                                ) );
+                            }
+                        }
+                        catch ( ChaiOperationException e )
+                        {
+                            LOGGER.error( sessionLabel, "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[]
+                                    {
+                                            formConfiguration.getLabel( locale ),
+                                    }
+                            ) );
+                        }
+                        catch ( ChaiUnavailableException e )
+                        {
+                            throw PwmUnrecoverableException.fromChaiException( e );
+                        }
+                    }
+
+                    forgottenPasswordBean.getProgress().getSatisfiedMethods().add( IdentityVerificationMethod.ATTRIBUTES );
+                }
+                catch ( PwmDataValidationException e )
+                {
+                    handleUserVerificationBadAttempt(
+                            forgottenPasswordStateMachine.getCommonValues(),
+                            forgottenPasswordBean,
+                            new ErrorInformation( PwmError.ERROR_INCORRECT_RESPONSE, e.getErrorInformation().toDebugStr() ) );
+                }
+            }
+
+            public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+            {
+                final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+                final List<FormConfiguration> formConfigurations = forgottenPasswordStateMachine.getForgottenPasswordBean().getAttributeForm();
+                return PresentableForm.builder()
+                        .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                        .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverPassword, commonValues.getConfig() ) )
+                        .formRows( PresentableFormRow.fromFormConfigurations( formConfigurations, forgottenPasswordStateMachine.getCommonValues().getLocale() ) )
+                        .build();
+            }
+        }
+
+
+    }
+
+    static class MethodChoiceStageHandler implements StageHandler
+    {
+        @Override
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> formValues )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordStateMachine.getForgottenPasswordBean();
+            final LinkedHashSet<IdentityVerificationMethod> remainingAvailableOptionalMethods = new LinkedHashSet<>(
+                    ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods( commonValues, forgottenPasswordBean )
+            );
+
+            final IdentityVerificationMethod requestedChoice = JavaHelper.readEnumFromString(
+                    IdentityVerificationMethod.class,
+                    null,
+                    formValues.get( PwmConstants.PARAM_METHOD_CHOICE ) );
+            if ( requestedChoice == null )
+            {
+                final String errorMsg = "unknown verification method requested";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_MISSING_PARAMETER, errorMsg );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+
+            if ( remainingAvailableOptionalMethods.contains( requestedChoice ) )
+            {
+                forgottenPasswordBean.getProgress().setInProgressVerificationMethod( requestedChoice );
+            }
+        }
+
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final LinkedHashSet<IdentityVerificationMethod> remainingAvailableOptionalMethods = new LinkedHashSet<>(
+                    ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean() )
+            );
+
+            final Map<String, String> selectOptions = new LinkedHashMap<>();
+            for ( final IdentityVerificationMethod method : remainingAvailableOptionalMethods )
+            {
+                if ( method.isUserSelectable() )
+                {
+                    selectOptions.put( method.name(), method.getLabel( commonValues.getConfig(), commonValues.getLocale() ) );
+                }
+            }
+
+            final FormConfiguration formConfiguration = FormConfiguration.builder()
+                    .type( FormConfiguration.Type.select )
+                    .required( true )
+                    .selectOptions( selectOptions )
+                    .labels( Collections.singletonMap( "", LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Button_Select, commonValues.getConfig() ) ) )
+                    .name( PwmConstants.PARAM_METHOD_CHOICE )
+                    .build();
+
+            return PresentableForm.builder()
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                    .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_RecoverVerificationChoice, commonValues.getConfig() ) )
+                    .formRow( PresentableFormRow.fromFormConfiguration( formConfiguration, commonValues.getLocale() ) )
+                    .build();
+        }
+    }
+
+    static class IdentificationStageHandler implements StageHandler
+    {
+        public PresentableForm generateForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+            final String profile = forgottenPasswordStateMachine.getForgottenPasswordBean().getProfile();
+            final List<FormConfiguration> formFields = new ArrayList<>( makeSelectableContextValues( commonValues, profile ) );
+            formFields.addAll( commonValues.getConfig().readSettingAsForm( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM ) );
+
+            return PresentableForm.builder()
+                    .label( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Title_ForgottenPassword, commonValues.getConfig() ) )
+                    .message( LocaleHelper.getLocalizedMessage( commonValues.getLocale(), Display.Display_ForgottenPassword, commonValues.getConfig() ) )
+                    .formRows( PresentableFormRow.fromFormConfigurations( formFields, commonValues.getLocale() ) )
+                    .build();
+        }
+
+        public void applyForm( final ForgottenPasswordStateMachine forgottenPasswordStateMachine, final Map<String, String> values )
+                throws PwmUnrecoverableException
+        {
+            final CommonValues commonValues = forgottenPasswordStateMachine.getCommonValues();
+
+            if ( forgottenPasswordStateMachine.nextStage() != ForgottenPasswordStage.IDENTIFICATION )
+            {
+                forgottenPasswordStateMachine.clear();
+            }
+
+            if ( values == null )
+            {
+                return;
+            }
+
+            // process input profile
+            {
+                final String inputProfile = values.get( PwmConstants.PARAM_LDAP_PROFILE );
+                if ( !StringUtil.isEmpty( inputProfile ) && commonValues.getConfig().getLdapProfiles().containsKey( inputProfile ) )
+                {
+                    forgottenPasswordStateMachine.getForgottenPasswordBean().setProfile( inputProfile );
+                }
+            }
+
+            final LdapProfile ldapProfile = commonValues.getConfig().getLdapProfiles().getOrDefault(
+                    forgottenPasswordStateMachine.getForgottenPasswordBean().getProfile(),
+                    commonValues.getConfig().getDefaultLdapProfile() );
+
+            final String contextParam = values.get( PwmConstants.PARAM_CONTEXT );
+
+            final List<FormConfiguration> forgottenPasswordForm = commonValues.getConfig().readSettingAsForm( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FORM );
+
+            final boolean bogusUserModeEnabled = commonValues.getConfig().readSettingAsBoolean( PwmSetting.RECOVERY_BOGUS_USER_ENABLE );
+
+            Map<FormConfiguration, String> formValues = new LinkedHashMap<>();
+
+            try
+            {
+                //read the values from the request
+                formValues = FormUtility.readFormValuesFromMap( values, forgottenPasswordForm, commonValues.getLocale() );
+
+                // check for intruder search values
+                commonValues.getPwmApplication().getIntruderManager().convenience().checkAttributes( formValues );
+
+                // see if the values meet the configured form requirements.
+                FormUtility.validateFormValues( commonValues.getConfig(), formValues, commonValues.getLocale() );
+
+                final String searchFilter;
+                {
+                    final String configuredSearchFilter = commonValues.getConfig().readSettingAsString( PwmSetting.FORGOTTEN_PASSWORD_SEARCH_FILTER );
+                    if ( configuredSearchFilter == null || configuredSearchFilter.isEmpty() )
+                    {
+                        searchFilter = FormUtility.ldapSearchFilterForForm( commonValues.getPwmApplication(), forgottenPasswordForm );
+                        LOGGER.trace( commonValues.getSessionLabel(), () -> "auto generated ldap search filter: " + searchFilter );
+                    }
+                    else
+                    {
+                        searchFilter = configuredSearchFilter;
+                    }
+                }
+                final UserIdentity userIdentity;
+
+                // convert the username field to an identity
+                {
+                    final UserSearchEngine userSearchEngine = commonValues.getPwmApplication().getUserSearchEngine();
+                    final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                            .filter( searchFilter )
+                            .formValues( formValues )
+                            .contexts( Collections.singletonList( contextParam ) )
+                            .ldapProfile( ldapProfile.getIdentifier() )
+                            .build();
+
+                    userIdentity = userSearchEngine.performSingleUserSearch( searchConfiguration, commonValues.getSessionLabel() );
+                }
+
+                if ( userIdentity == null )
+                {
+                    throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_CANT_MATCH_USER ) );
+                }
+
+                AuthenticationUtility.checkIfUserEligibleToAuthentication( commonValues.getPwmApplication(), userIdentity );
+
+                ForgottenPasswordUtil.initForgottenPasswordBean( commonValues, userIdentity, forgottenPasswordStateMachine.getForgottenPasswordBean() );
+
+                // clear intruder search values
+                commonValues.getPwmApplication().getIntruderManager().convenience().clearAttributes( formValues );
+
+                return;
+            }
+            catch ( PwmOperationalException e )
+            {
+                if ( e.getError() != PwmError.ERROR_CANT_MATCH_USER || !bogusUserModeEnabled )
+                {
+                    final ErrorInformation errorInfo = new ErrorInformation(
+                            PwmError.ERROR_RESPONSES_NORESPONSES,
+                            e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
+                    );
+                    commonValues.getPwmApplication().getStatisticsManager().incrementValue( Statistic.RECOVERY_FAILURES );
+
+                    commonValues.getPwmApplication().getIntruderManager().convenience().markAttributes( formValues, commonValues.getSessionLabel() );
+
+                    LOGGER.debug( commonValues.getSessionLabel(), errorInfo );
+                    forgottenPasswordStateMachine.clear();
+                    throw new PwmUnrecoverableException( errorInfo );
+                }
+            }
+
+            // only reachable if user not matched and bogus mode is enabled
+            ForgottenPasswordUtil.initBogusForgottenPasswordBean( commonValues, forgottenPasswordStateMachine.getForgottenPasswordBean() );
+            forgottenPasswordStateMachine.getForgottenPasswordBean().setUserSearchValues( FormUtility.asStringMap( formValues ) );
+        }
+
+        private List<FormConfiguration> makeSelectableContextValues( final CommonValues commonValues, final String profile )
+                throws PwmUnrecoverableException
+        {
+            final SelectableContextMode selectableContextMode = commonValues.getConfig().readSettingAsEnum(
+                    PwmSetting.LDAP_SELECTABLE_CONTEXT_MODE,
+                    SelectableContextMode.class );
+
+            if ( selectableContextMode == null || selectableContextMode == SelectableContextMode.NONE )
+            {
+                return Collections.emptyList();
+            }
+
+            final List<FormConfiguration> returnList = new ArrayList<>();
+
+            if ( selectableContextMode == SelectableContextMode.SHOW_PROFILE && commonValues.getConfig().getLdapProfiles().size() > 1 )
+            {
+                final Map<String, String> profileSelectValues = new LinkedHashMap<>();
+                for ( final LdapProfile ldapProfile : commonValues.getConfig().getLdapProfiles().values() )
+                {
+                    profileSelectValues.put( ldapProfile.getIdentifier(), ldapProfile.getDisplayName( commonValues.getLocale() ) );
+                }
+                final Map<String, String> labelLocaleMap = LocaleHelper.localeMapToStringMap(
+                        LocaleHelper.getUniqueLocalizations( commonValues.getConfig(), Display.class, "Field_Profile", commonValues.getLocale() ) );
+                final FormConfiguration formConfiguration = FormConfiguration.builder()
+                        .name( PwmConstants.PARAM_LDAP_PROFILE )
+                        .labels( labelLocaleMap )
+                        .type( FormConfiguration.Type.select )
+                        .selectOptions( profileSelectValues )
+                        .required( true )
+                        .build();
+                returnList.add( formConfiguration );
+            }
+
+            final LdapProfile selectedProfile = commonValues.getConfig().getLdapProfiles().getOrDefault( profile, commonValues.getConfig().getDefaultLdapProfile() );
+            final Map<String, String> selectableContexts = selectedProfile.getSelectableContexts( commonValues.getPwmApplication() );
+            if ( selectableContexts != null && selectableContexts.size() > 1 )
+            {
+                final Map<String, String> labelLocaleMap = LocaleHelper.localeMapToStringMap(
+                        LocaleHelper.getUniqueLocalizations( commonValues.getConfig(), Display.class, "Field_Context", commonValues.getLocale() ) );
+                final FormConfiguration formConfiguration = FormConfiguration.builder()
+                        .name( PwmConstants.PARAM_CONTEXT )
+                        .labels( labelLocaleMap )
+                        .type( FormConfiguration.Type.select )
+                        .selectOptions( selectableContexts )
+                        .required( true )
+                        .build();
+                returnList.add( formConfiguration );
+            }
+
+            return Collections.unmodifiableList( returnList );
+        }
+    }
+
+    private static void handleUserVerificationBadAttempt(
+            final CommonValues commonValues,
+            final ForgottenPasswordBean forgottenPasswordBean,
+            final ErrorInformation errorInformation
+    )
+            throws PwmUnrecoverableException
+    {
+        LOGGER.debug( commonValues.getSessionLabel(), errorInformation );
+
+        final UserIdentity userIdentity = forgottenPasswordBean == null
+                ? null
+                : forgottenPasswordBean.getUserIdentity();
+
+
+        // add a bit of jitter to pretend like we're checking a data source
+        final long jitterMs = 300 + commonValues.getPwmApplication().getSecureService().pwmRandom().nextInt( 700 );
+        TimeDuration.of( jitterMs, TimeDuration.Unit.MILLISECONDS ).pause();
+
+        if ( userIdentity != null )
+        {
+            SessionAuthenticator.simulateBadPassword( commonValues, userIdentity );
+
+
+            commonValues.getPwmApplication().getIntruderManager().convenience().markUserIdentity( userIdentity,
+                    commonValues.getSessionLabel() );
+        }
+
+        StatisticsManager.incrementStat( commonValues.getPwmApplication(), Statistic.RECOVERY_FAILURES );
+    }
+}

+ 84 - 104
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -23,9 +23,12 @@
 package password.pwm.http.servlet.forgottenpw;
 
 import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.cr.ChaiChallenge;
 import com.novell.ldapchai.cr.Challenge;
 import com.novell.ldapchai.cr.ChallengeSet;
 import com.novell.ldapchai.cr.ResponseSet;
+import com.novell.ldapchai.cr.bean.ChallengeBean;
+import com.novell.ldapchai.cr.bean.ChallengeSetBean;
 import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiOperationException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
@@ -52,6 +55,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.auth.HttpAuthRecord;
 import password.pwm.http.bean.ForgottenPasswordBean;
@@ -62,7 +66,6 @@ import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
@@ -89,7 +92,7 @@ public class ForgottenPasswordUtil
     private static final PwmLogger LOGGER = PwmLogger.forClass( ForgottenPasswordUtil.class );
 
     static Set<IdentityVerificationMethod> figureRemainingAvailableOptionalAuthMethods(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean
     )
     {
@@ -103,7 +106,7 @@ public class ForgottenPasswordUtil
         {
             try
             {
-                verifyRequirementsForAuthMethod( pwmRequest, forgottenPasswordBean, recoveryVerificationMethods );
+                verifyRequirementsForAuthMethod( commonValues, forgottenPasswordBean, recoveryVerificationMethods );
             }
             catch ( PwmUnrecoverableException e )
             {
@@ -125,14 +128,13 @@ public class ForgottenPasswordUtil
             final ForgottenPasswordBean.RecoveryFlags recoveryFlags,
             final ForgottenPasswordBean.Progress progress )
     {
-        final Set<IdentityVerificationMethod> result = new LinkedHashSet<>();
-        result.addAll( recoveryFlags.getOptionalAuthMethods() );
+        final Set<IdentityVerificationMethod> result = new LinkedHashSet<>( recoveryFlags.getOptionalAuthMethods() );
         result.retainAll( progress.getSatisfiedMethods() );
         return Collections.unmodifiableSet( result );
     }
 
     static UserInfo readUserInfo(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean
     )
             throws PwmUnrecoverableException
@@ -142,39 +144,20 @@ public class ForgottenPasswordUtil
             return null;
         }
 
-        final String cacheKey = PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE;
-
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
 
-        {
-            final UserInfo userInfoFromSession = ( UserInfo ) pwmRequest.getHttpServletRequest().getAttribute( cacheKey );
-            if ( userInfoFromSession != null )
-            {
-                if ( userIdentity.equals( userInfoFromSession.getUserIdentity() ) )
-                {
-                    LOGGER.trace( pwmRequest, () -> "using request cached userInfo" );
-                    return userInfoFromSession;
-                }
-                else
-                {
-                    LOGGER.trace( pwmRequest, () -> "request cached userInfo is not for current user, clearing." );
-                    pwmRequest.getHttpServletRequest().getSession().setAttribute( cacheKey, null );
-                }
-            }
-        }
-
-        final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy(
-                pwmRequest.getPwmApplication(),
-                pwmRequest.getSessionLabel(),
-                userIdentity, pwmRequest.getLocale()
+        return UserInfoFactory.newUserInfoUsingProxy(
+                commonValues.getPwmApplication(),
+                commonValues.getSessionLabel(),
+                userIdentity,
+                commonValues.getLocale()
         );
-
-        pwmRequest.getHttpServletRequest().setAttribute( cacheKey, userInfo );
-
-        return userInfo;
     }
 
-    static ResponseSet readResponseSet( final PwmRequest pwmRequest, final ForgottenPasswordBean forgottenPasswordBean )
+    static ResponseSet readResponseSet(
+            final CommonValues commonValues,
+            final ForgottenPasswordBean forgottenPasswordBean
+    )
             throws PwmUnrecoverableException
     {
 
@@ -183,7 +166,7 @@ public class ForgottenPasswordUtil
             return null;
         }
 
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmApplication pwmApplication = commonValues.getPwmApplication();
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
         final ResponseSet responseSet;
 
@@ -191,7 +174,7 @@ public class ForgottenPasswordUtil
         {
             final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
             responseSet = pwmApplication.getCrService().readUserResponseSet(
-                    pwmRequest.getSessionLabel(),
+                    commonValues.getSessionLabel(),
                     userIdentity,
                     theUser
             );
@@ -205,27 +188,27 @@ public class ForgottenPasswordUtil
     }
 
     static void sendUnlockNoticeEmail(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean
     )
             throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
     {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Configuration config = pwmRequest.getConfig();
-        final Locale locale = pwmRequest.getLocale();
+        final PwmApplication pwmApplication = commonValues.getPwmApplication();
+        final Configuration config = commonValues.getConfig();
+        final Locale locale = commonValues.getLocale();
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
         final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_UNLOCK, locale );
 
         if ( configuredEmailSetting == null )
         {
-            LOGGER.debug( pwmRequest, () -> "skipping send unlock notice email for '" + userIdentity + "' no email configured" );
+            LOGGER.debug( commonValues.getSessionLabel(), () -> "skipping send unlock notice email for '" + userIdentity + "' no email configured" );
             return;
         }
 
-        final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = readUserInfo( commonValues, forgottenPasswordBean );
         final MacroMachine macroMachine = MacroMachine.forUser(
                 pwmApplication,
-                pwmRequest.getSessionLabel(),
+                commonValues.getSessionLabel(),
                 userInfo,
                 null
         );
@@ -272,43 +255,29 @@ public class ForgottenPasswordUtil
     }
 
     static List<TokenDestinationItem> figureAvailableTokenDestinations(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean
     )
             throws PwmUnrecoverableException
     {
-        {
-            @SuppressWarnings( "unchecked" )
-            final List<TokenDestinationItem> cachedItems = (List<TokenDestinationItem>) pwmRequest.getHttpServletRequest().getAttribute(
-                    PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE
-            );
-            if ( cachedItems != null )
-            {
-                return cachedItems;
-            }
-        }
-
         final String profileID = forgottenPasswordBean.getForgottenPasswordProfileID();
-        final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get( profileID );
+        final ForgottenPasswordProfile forgottenPasswordProfile = commonValues.getConfig().getForgottenPasswordProfiles().get( profileID );
         final MessageSendMethod tokenSendMethod = forgottenPasswordProfile.readSettingAsEnum( PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class );
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordBean );
 
         final List<TokenDestinationItem> items = TokenUtil.figureAvailableTokenDestinations(
-                pwmRequest.getPwmApplication(),
-                pwmRequest.getSessionLabel(),
-                pwmRequest.getLocale(),
+                commonValues.getPwmApplication(),
+                commonValues.getSessionLabel(),
+                commonValues.getLocale(),
                 userInfo,
                 tokenSendMethod
         );
 
-        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( items );
-        pwmRequest.getHttpServletRequest().setAttribute( PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE, finalList );
-
-        return finalList;
+        return Collections.unmodifiableList( items );
     }
 
     static void verifyRequirementsForAuthMethod(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean,
             final IdentityVerificationMethod recoveryVerificationMethods
     )
@@ -318,7 +287,7 @@ public class ForgottenPasswordUtil
         {
             case TOKEN:
             {
-                ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+                ForgottenPasswordUtil.figureAvailableTokenDestinations( commonValues, forgottenPasswordBean );
             }
             break;
 
@@ -336,7 +305,7 @@ public class ForgottenPasswordUtil
 
             case OTP:
             {
-                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordBean );
                 if ( userInfo.getOtpUserRecord() == null )
                 {
                     final String errorMsg = "could not find a one time password configuration for " + userInfo.getUserIdentity();
@@ -348,8 +317,8 @@ public class ForgottenPasswordUtil
 
             case CHALLENGE_RESPONSES:
             {
-                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-                final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet( pwmRequest, forgottenPasswordBean );
+                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( commonValues, forgottenPasswordBean );
+                final ResponseSet responseSet = ForgottenPasswordUtil.readResponseSet( commonValues, forgottenPasswordBean );
                 if ( responseSet == null )
                 {
                     final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_RESPONSES_NORESPONSES );
@@ -390,26 +359,46 @@ public class ForgottenPasswordUtil
 
     static Map<Challenge, String> readResponsesFromHttpRequest(
             final PwmRequest req,
-            final ChallengeSet challengeSet
+            final ChallengeSetBean challengeSet
     )
             throws PwmUnrecoverableException
     {
         final Map<Challenge, String> responses = new LinkedHashMap<>();
 
         int counter = 0;
-        for ( final Challenge loopChallenge : challengeSet.getChallenges() )
+        for ( final ChallengeBean loopChallenge : challengeSet.getChallenges() )
         {
             counter++;
             final String answer = req.readParameterAsString( PwmConstants.PARAM_RESPONSE_PREFIX + counter );
 
-            responses.put( loopChallenge, answer.length() > 0 ? answer : "" );
+            responses.put( ChaiChallenge.fromChallengeBean( loopChallenge ), answer.length() > 0 ? answer : "" );
         }
 
         return responses;
     }
 
+    static Map<Challenge, String> readResponsesFromMap(
+            final ChallengeSetBean challengeSet,
+            final Map<String, String> formData
+    )
+    {
+        final Map<Challenge, String> responses = new LinkedHashMap<>();
+
+        int counter = 0;
+        for ( final ChallengeBean loopChallenge : challengeSet.getChallenges() )
+        {
+            counter++;
+            final String answer = formData.get( PwmConstants.PARAM_RESPONSE_PREFIX + counter );
+
+            responses.put( ChaiChallenge.fromChallengeBean( loopChallenge ), answer.length() > 0 ? answer : "" );
+        }
+
+        return responses;
+    }
+
+
     static void initializeAndSendToken(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final UserInfo userInfo,
             final TokenDestinationItem tokenDestinationItem
 
@@ -417,7 +406,7 @@ public class ForgottenPasswordUtil
             throws PwmUnrecoverableException
     {
         TokenUtil.initializeAndSendToken(
-                pwmRequest,
+                commonValues,
                 TokenUtil.TokenInitAndSendRequest.builder()
                         .userInfo( userInfo )
                         .tokenDestinationItem( tokenDestinationItem )
@@ -427,7 +416,7 @@ public class ForgottenPasswordUtil
                         .build()
         );
 
-        StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_TOKENS_SENT );
+        commonValues.getPwmApplication().getStatisticsManager().incrementValue( Statistic.RECOVERY_TOKENS_SENT );
     }
 
 
@@ -556,18 +545,16 @@ public class ForgottenPasswordUtil
         }
     }
 
-    static void initBogusForgottenPasswordBean( final PwmRequest pwmRequest )
+    static void initBogusForgottenPasswordBean( final CommonValues commonValues, final ForgottenPasswordBean forgottenPasswordBean )
             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();
+            final String firstProfile = commonValues.getConfig().getChallengeProfileIDs().iterator().next();
+            final ChallengeSet challengeSet = commonValues.getConfig().getChallengeProfile( firstProfile, PwmConstants.DEFAULT_LOCALE ).getChallengeSet();
             challengeList.addAll( challengeSet.getRequiredChallenges() );
             for ( int i = 0; i < challengeSet.getMinRandomRequired(); i++ )
             {
@@ -594,7 +581,7 @@ public class ForgottenPasswordUtil
         forgottenPasswordBean.setAttributeForm( formData );
         forgottenPasswordBean.setBogusUser( true );
         {
-            final String profileID = pwmRequest.getConfig().getForgottenPasswordProfiles().keySet().iterator().next();
+            final String profileID = commonValues.getConfig().getForgottenPasswordProfiles().keySet().iterator().next();
             forgottenPasswordBean.setForgottenPasswordProfileID( profileID  );
         }
 
@@ -605,6 +592,7 @@ public class ForgottenPasswordUtil
                 0
         );
 
+        forgottenPasswordBean.getProgress().setInProgressVerificationMethod( IdentityVerificationMethod.ATTRIBUTES );
         forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
     }
 
@@ -680,24 +668,24 @@ public class ForgottenPasswordUtil
 
 
     static void initForgottenPasswordBean(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final UserIdentity userIdentity,
             final ForgottenPasswordBean forgottenPasswordBean
     )
             throws PwmUnrecoverableException, PwmOperationalException
     {
 
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Locale locale = pwmRequest.getLocale();
-        final SessionLabel sessionLabel = pwmRequest.getSessionLabel();
+        final PwmApplication pwmApplication = commonValues.getPwmApplication();
+        final Locale locale = commonValues.getLocale();
+        final SessionLabel sessionLabel = commonValues.getSessionLabel();
 
         forgottenPasswordBean.setUserIdentity( userIdentity );
 
-        final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = readUserInfo( commonValues, forgottenPasswordBean );
 
         final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile(
                 pwmApplication,
-                pwmRequest.getSessionLabel(),
+                commonValues.getSessionLabel(),
                 userIdentity
         );
         final String forgottenProfileID = forgottenPasswordProfile.getIdentifier();
@@ -763,18 +751,10 @@ public class ForgottenPasswordUtil
             }
         }
 
-        final List<FormConfiguration> attributeForm;
-        try
-        {
-            attributeForm = figureAttributeForm( forgottenPasswordProfile, forgottenPasswordBean, pwmRequest, userIdentity );
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            throw new PwmUnrecoverableException( PwmError.forChaiError( e.getErrorCode() ) );
-        }
+        final List<FormConfiguration> attributeForm = figureAttributeForm( forgottenPasswordProfile, forgottenPasswordBean, commonValues, userIdentity );
 
         forgottenPasswordBean.setUserLocale( locale );
-        forgottenPasswordBean.setPresentableChallengeSet( challengeSet );
+        forgottenPasswordBean.setPresentableChallengeSet( challengeSet == null ? null : challengeSet.asChallengeSetBean() );
         forgottenPasswordBean.setAttributeForm( attributeForm );
 
         forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
@@ -782,17 +762,17 @@ public class ForgottenPasswordUtil
 
         for ( final IdentityVerificationMethod recoveryVerificationMethods : recoveryFlags.getRequiredAuthMethods() )
         {
-            verifyRequirementsForAuthMethod( pwmRequest, forgottenPasswordBean, recoveryVerificationMethods );
+            verifyRequirementsForAuthMethod( commonValues, forgottenPasswordBean, recoveryVerificationMethods );
         }
     }
 
     static List<FormConfiguration> figureAttributeForm(
             final ForgottenPasswordProfile forgottenPasswordProfile,
             final ForgottenPasswordBean forgottenPasswordBean,
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final UserIdentity userIdentity
     )
-            throws ChaiUnavailableException, PwmOperationalException, PwmUnrecoverableException
+            throws PwmOperationalException, PwmUnrecoverableException
     {
         final List<FormConfiguration> requiredAttributesForm = forgottenPasswordProfile.readSettingAsForm( PwmSetting.RECOVERY_ATTRIBUTE_FORM );
         if ( requiredAttributesForm.isEmpty() )
@@ -800,7 +780,7 @@ public class ForgottenPasswordUtil
             return requiredAttributesForm;
         }
 
-        final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
+        final UserInfo userInfo = readUserInfo( commonValues, forgottenPasswordBean );
         final List<FormConfiguration> returnList = new ArrayList<>();
         for ( final FormConfiguration formItem : requiredAttributesForm )
         {
@@ -819,7 +799,7 @@ public class ForgottenPasswordUtil
                     }
                     else
                     {
-                        LOGGER.trace( pwmRequest, () -> "excluding optional required attribute(" + formItem.getName() + "), user has no value" );
+                        LOGGER.trace( commonValues.getSessionLabel(), () -> "excluding optional required attribute(" + formItem.getName() + "), user has no value" );
                     }
                 }
                 catch ( PwmUnrecoverableException e )
@@ -859,7 +839,7 @@ public class ForgottenPasswordUtil
     }
 
     static boolean hasOtherMethodChoices(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final ForgottenPasswordBean forgottenPasswordBean,
             final IdentityVerificationMethod thisMethod
     )
@@ -882,7 +862,7 @@ public class ForgottenPasswordUtil
 
         {
             final Set<IdentityVerificationMethod> remainingAvailableOptionalMethods = ForgottenPasswordUtil.figureRemainingAvailableOptionalAuthMethods(
-                    pwmRequest,
+                    commonValues,
                     forgottenPasswordBean
             );
             final Set<IdentityVerificationMethod> otherOptionalMethodChoices = new HashSet<>( remainingAvailableOptionalMethods );

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

@@ -468,7 +468,7 @@ public class NewUserServlet extends ControlledPwmServlet
         try
         {
             tokenPayload = TokenUtil.checkEnteredCode(
-                    pwmRequest,
+                    pwmRequest.commonValues(),
                     userEnteredCode,
                     tokenDestinationItem,
                     null,

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

@@ -697,7 +697,7 @@ class NewUserUtils
 
 
                     TokenUtil.initializeAndSendToken(
-                            pwmRequest,
+                            pwmRequest.commonValues(),
                             TokenUtil.TokenInitAndSendRequest.builder()
                                     .userInfo(  null )
                                     .tokenDestinationItem( tokenDestinationItem )

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

@@ -173,7 +173,7 @@ public class UpdateProfileServlet extends ControlledPwmServlet
         try
         {
             TokenUtil.checkEnteredCode(
-                    pwmRequest,
+                    pwmRequest.commonValues(),
                     userEnteredCode,
                     tokenDestinationItem,
                     pwmRequest.getUserInfoIfLoggedIn(),

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

@@ -323,7 +323,7 @@ public class UpdateProfileUtil
                             : updateProfileProfile.getTokenDurationSMS( pwmRequest.getConfig() );
 
                     TokenUtil.initializeAndSendToken(
-                            pwmRequest,
+                            pwmRequest.commonValues(),
                             TokenUtil.TokenInitAndSendRequest.builder()
                                     .userInfo( pwmRequest.getPwmSession().getUserInfo() )
                                     .tokenDestinationItem( tokenDestinationItem )

+ 1 - 0
server/src/main/java/password/pwm/i18n/Display.java

@@ -64,6 +64,7 @@ public enum Display implements PwmDisplayBundle
     Button_SetResponses,
     Button_Show,
     Button_Show_Responses,
+    Button_Select,
     Button_Skip,
     Button_SMS,
     Button_Unlock,

+ 12 - 4
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -215,7 +215,7 @@ public class LdapOperationsHelper
             final UserIdentity userIdentity,
             final boolean throwExceptionOnError
     )
-            throws ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final boolean enableCache = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_CACHE_USER_GUID_ENABLE ) );
         final CacheKey cacheKey = CacheKey.newKey( LdapOperationsHelper.class, userIdentity, "guidValue" );
@@ -442,7 +442,7 @@ public class LdapOperationsHelper
                 final UserIdentity userIdentity,
                 final boolean throwExceptionOnError
         )
-                throws ChaiUnavailableException, PwmUnrecoverableException
+                throws PwmUnrecoverableException
         {
             final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
             final LdapProfile ldapProfile = pwmApplication.getConfig().getLdapProfiles().get( userIdentity.getLdapProfileID() );
@@ -485,6 +485,10 @@ public class LdapOperationsHelper
                         + userIdentity + " from '" + guidAttributeName + "', error: " + e.getMessage();
                 return processError( errorMsg, throwExceptionOnError );
             }
+            catch ( ChaiUnavailableException e )
+            {
+                throw PwmUnrecoverableException.fromChaiException( e );
+            }
         }
 
         private static String processError( final String errorMsg, final boolean throwExceptionOnError )
@@ -504,7 +508,7 @@ public class LdapOperationsHelper
                 final SessionLabel sessionLabel,
                 final String guidValue
         )
-                throws ChaiUnavailableException, PwmUnrecoverableException
+                throws PwmUnrecoverableException
         {
             boolean exists = false;
             for ( final LdapProfile ldapProfile : pwmApplication.getConfig().getLdapProfiles().values() )
@@ -541,7 +545,7 @@ public class LdapOperationsHelper
                 final UserIdentity userIdentity,
                 final String guidAttributeName
         )
-                throws ChaiUnavailableException, PwmUnrecoverableException
+                throws PwmUnrecoverableException
         {
             int attempts = 0;
             String newGuid = null;
@@ -582,6 +586,10 @@ public class LdapOperationsHelper
                 LOGGER.error( errorInformation.toDebugStr() );
                 throw new PwmUnrecoverableException( errorInformation );
             }
+            catch ( ChaiUnavailableException e )
+            {
+                throw PwmUnrecoverableException.fromChaiException( e );
+            }
         }
 
         private static String generateGuidValue(

+ 11 - 0
server/src/main/java/password/pwm/ldap/UserInfoFactory.java

@@ -30,6 +30,7 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
 import password.pwm.util.PasswordData;
 
 import java.util.Locale;
@@ -74,6 +75,16 @@ public class UserInfoFactory
         return newUserInfo( pwmApplication, sessionLabel, ldapLocale, userIdentity, provider, null );
     }
 
+    public static UserInfo newUserInfoUsingProxy(
+            final CommonValues commonValues,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final ChaiProvider provider = commonValues.getPwmApplication().getProxyChaiProvider( userIdentity.getLdapProfileID() );
+        return newUserInfo( commonValues.getPwmApplication(), commonValues.getSessionLabel(), commonValues.getLocale(), userIdentity, provider, null );
+    }
+
     public static UserInfo newUserInfoUsingProxy(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,

+ 7 - 8
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -591,14 +591,7 @@ public class UserInfoReader implements UserInfo
     @Override
     public String getUserGuid( ) throws PwmUnrecoverableException
     {
-        try
-        {
-            return LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, false );
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
+        return LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, false );
     }
 
     @Override
@@ -885,4 +878,10 @@ public class UserInfoReader implements UserInfo
         }
         return null;
     }
+
+    @Override
+    public String toString()
+    {
+        return "UserInfoReader: " + this.getUserIdentity().toDisplayString();
+    }
 }

+ 11 - 1
server/src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java

@@ -41,6 +41,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
 import password.pwm.http.PwmSession;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
@@ -246,12 +247,20 @@ public class SessionAuthenticator
         }
     }
 
-
     public void simulateBadPassword(
             final UserIdentity userIdentity
     )
             throws PwmUnrecoverableException
     {
+        final CommonValues commonValues = new CommonValues( pwmApplication, sessionLabel, null, null );
+        simulateBadPassword( commonValues, userIdentity );
+    }
+
+    public static void simulateBadPassword( final CommonValues commonValues, final UserIdentity userIdentity ) throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = commonValues.getPwmApplication();
+        final SessionLabel sessionLabel = commonValues.getSessionLabel();
+
         if ( !pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.SECURITY_SIMULATE_LDAP_BAD_PASSWORD ) )
         {
             return;
@@ -320,6 +329,7 @@ public class SessionAuthenticator
                 }
             }
         }
+
     }
 
     private void postFailureSequence(

+ 1 - 1
server/src/main/java/password/pwm/ldap/search/SearchConfiguration.java

@@ -62,7 +62,7 @@ public class SearchConfiguration implements Serializable
     {
         if ( this.username != null && this.formValues != null )
         {
-            throw new IllegalArgumentException( "username OR formValues cannot both be supplied" );
+            throw new IllegalArgumentException( "username OR formRows cannot both be supplied" );
         }
     }
 

+ 1 - 11
server/src/main/java/password/pwm/svc/event/DatabaseUserHistory.java

@@ -22,7 +22,6 @@
 
 package password.pwm.svc.event;
 
-import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.error.ErrorInformation;
@@ -70,16 +69,7 @@ class DatabaseUserHistory implements UserHistoryStore
             userIdentity = new UserIdentity( auditRecord.getPerpetratorDN(), auditRecord.getPerpetratorLdapProfile() );
         }
 
-        final String guid;
-        try
-        {
-            guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, null, userIdentity, false );
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            LOGGER.error( "unable to read guid for user '" + userIdentity + "', cannot update user history, error: " + e.getMessage() );
-            return;
-        }
+        final String guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, null, userIdentity, false );
 
         try
         {

+ 2 - 2
server/src/main/java/password/pwm/svc/intruder/IntruderManager.java

@@ -623,13 +623,13 @@ public class IntruderManager implements PwmService
             }
         }
 
-        public void markAttributes( final Map<FormConfiguration, String> formValues, final PwmSession pwmSession )
+        public void markAttributes( final Map<FormConfiguration, String> formValues, final SessionLabel sessionLabel )
                 throws PwmUnrecoverableException
         {
             final List<String> subjects = attributeFormToList( formValues );
             for ( final String subject : subjects )
             {
-                mark( RecordType.ATTRIBUTE, subject, pwmSession.getLabel() );
+                mark( RecordType.ATTRIBUTE, subject, sessionLabel );
             }
         }
 

+ 4 - 19
server/src/main/java/password/pwm/svc/pwnotify/PwNotifyDbStorageService.java

@@ -22,7 +22,6 @@
 
 package password.pwm.svc.pwnotify;
 
-import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
@@ -62,15 +61,8 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
     )
             throws PwmUnrecoverableException
     {
-        final String guid;
-        try
-        {
-            guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
-        }
+        final String guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
+
         if ( StringUtil.isEmpty( guid ) )
         {
             throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );
@@ -101,15 +93,8 @@ class PwNotifyDbStorageService implements PwNotifyStorageService
     )
             throws PwmUnrecoverableException
     {
-        final String guid;
-        try
-        {
-            guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            throw new PwmUnrecoverableException( PwmUnrecoverableException.fromChaiException( e ).getErrorInformation() );
-        }
+        final String guid = LdapOperationsHelper.readLdapGuidValue( pwmApplication, sessionLabel, userIdentity, true );
+
         if ( StringUtil.isEmpty( guid ) )
         {
             throw new PwmUnrecoverableException( PwmError.ERROR_MISSING_GUID );

+ 19 - 20
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -46,7 +46,7 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
-import password.pwm.http.PwmSession;
+import password.pwm.http.CommonValues;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.svc.PwmService;
@@ -247,7 +247,7 @@ public class TokenService implements PwmService
 
     private void markTokenAsClaimed(
             final TokenKey tokenKey,
-            final PwmSession pwmSession,
+            final SessionLabel sessionLabel,
             final TokenPayload tokenPayload
     )
             throws PwmUnrecoverableException
@@ -263,19 +263,19 @@ public class TokenService implements PwmService
         {
             try
             {
-                LOGGER.trace( pwmSession, () -> "removing claimed token: " + tokenPayload.toDebugString() );
+                LOGGER.trace( sessionLabel, () -> "removing claimed token: " + tokenPayload.toDebugString() );
                 tokenMachine.removeToken( tokenKey );
             }
             catch ( PwmOperationalException e )
             {
-                LOGGER.error( pwmSession, "error clearing claimed token: " + e.getMessage() );
+                LOGGER.error( sessionLabel, "error clearing claimed token: " + e.getMessage() );
             }
         }
 
         final AuditRecord auditRecord = new AuditRecordFactory( pwmApplication ).createUserAuditRecord(
                 AuditEvent.TOKEN_CLAIMED,
                 tokenPayload.getUserIdentity(),
-                pwmSession.getLabel(),
+                sessionLabel,
                 JsonUtil.serialize( tokenPayload )
         );
         pwmApplication.getAuditManager().submit( auditRecord );
@@ -535,7 +535,7 @@ public class TokenService implements PwmService
     }
 
     public TokenPayload processUserEnteredCode(
-            final PwmSession pwmSession,
+            final CommonValues commonValues,
             final UserIdentity sessionUserIdentity,
             final TokenType tokenType,
             final String userEnteredCode,
@@ -543,10 +543,11 @@ public class TokenService implements PwmService
     )
             throws PwmOperationalException, PwmUnrecoverableException
     {
+        final SessionLabel sessionLabel = commonValues.getSessionLabel();
         try
         {
             final TokenPayload tokenPayload = processUserEnteredCodeImpl(
-                    pwmSession,
+                    sessionLabel,
                     sessionUserIdentity,
                     tokenType,
                     userEnteredCode
@@ -555,7 +556,7 @@ public class TokenService implements PwmService
             {
                 pwmApplication.getIntruderManager().clear( RecordType.TOKEN_DEST, tokenPayload.getDestination().getValue() );
             }
-            markTokenAsClaimed( tokenMachine.keyFromKey( userEnteredCode ), pwmSession, tokenPayload );
+            markTokenAsClaimed( tokenMachine.keyFromKey( userEnteredCode ), sessionLabel, tokenPayload );
             return tokenPayload;
         }
         catch ( Exception e )
@@ -570,22 +571,20 @@ public class TokenService implements PwmService
                 errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, e.getMessage() );
             }
 
-            LOGGER.debug( pwmSession, errorInformation );
+            LOGGER.debug( sessionLabel, errorInformation );
 
             if ( sessionUserIdentity != null && tokenEntryType == TokenEntryType.unauthenticated )
             {
-                final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, null );
-                sessionAuthenticator.simulateBadPassword( sessionUserIdentity );
-                pwmApplication.getIntruderManager().convenience().markUserIdentity( sessionUserIdentity, pwmSession );
+                SessionAuthenticator.simulateBadPassword( commonValues, sessionUserIdentity );
+                pwmApplication.getIntruderManager().convenience().markUserIdentity( sessionUserIdentity, sessionLabel );
             }
-            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
             pwmApplication.getStatisticsManager().incrementValue( Statistic.RECOVERY_FAILURES );
             throw new PwmOperationalException( errorInformation );
         }
     }
 
     private TokenPayload processUserEnteredCodeImpl(
-            final PwmSession pwmSession,
+            final SessionLabel sessionLabel,
             final UserIdentity sessionUserIdentity,
             final TokenType tokenType,
             final String userEnteredCode
@@ -595,7 +594,7 @@ public class TokenService implements PwmService
         final TokenPayload tokenPayload;
         try
         {
-            tokenPayload = pwmApplication.getTokenService().retrieveTokenData( pwmSession.getLabel(), userEnteredCode );
+            tokenPayload = pwmApplication.getTokenService().retrieveTokenData( sessionLabel, userEnteredCode );
         }
         catch ( PwmOperationalException e )
         {
@@ -609,7 +608,7 @@ public class TokenService implements PwmService
             throw new PwmOperationalException( errorInformation );
         }
 
-        LOGGER.trace( pwmSession, () -> "retrieved tokenPayload: " + tokenPayload.toDebugString() );
+        LOGGER.trace( sessionLabel, () -> "retrieved tokenPayload: " + tokenPayload.toDebugString() );
 
         if ( tokenType != null && pwmApplication.getTokenService().supportsName() )
         {
@@ -641,12 +640,12 @@ public class TokenService implements PwmService
             {
                 final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
                         pwmApplication,
-                        pwmSession.getLabel(),
+                        sessionLabel,
                         tokenPayload.getUserIdentity() );
 
                 final String dateStringInToken = tokenPayload.getData().get( PwmConstants.TOKEN_KEY_PWD_CHG_DATE );
 
-                LOGGER.trace( pwmSession, () -> "tokenPayload=" + tokenPayload.toDebugString()
+                LOGGER.trace( sessionLabel, () -> "tokenPayload=" + tokenPayload.toDebugString()
                         + ", sessionUser=" + ( sessionUserIdentity == null ? "null" : sessionUserIdentity.toDisplayString() )
                         + ", payloadUserIdentity=" + tokenPayload.getUserIdentity().toDisplayString()
                         + ", userLastPasswordChange=" + JavaHelper.toIsoDate( userLastPasswordChange )
@@ -661,7 +660,7 @@ public class TokenService implements PwmService
                     {
                         final String errorString = "user password has changed since token issued, token rejected;"
                                 + " currentValue=" + userChangeString + ", tokenValue=" + dateStringInToken;
-                        LOGGER.trace( pwmSession, () -> errorString + "; token=" + tokenPayload.toDebugString() );
+                        LOGGER.trace( sessionLabel, () -> errorString + "; token=" + tokenPayload.toDebugString() );
                         final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_EXPIRED, errorString );
                         throw new PwmOperationalException( errorInformation );
                     }
@@ -675,7 +674,7 @@ public class TokenService implements PwmService
             }
         }
 
-        LOGGER.debug( pwmSession, () -> "token validation has been passed" );
+        LOGGER.debug( sessionLabel, () -> "token validation has been passed" );
         return tokenPayload;
     }
 

+ 22 - 15
server/src/main/java/password/pwm/svc/token/TokenUtil.java

@@ -36,7 +36,7 @@ import password.pwm.config.option.MessageSendMethod;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.PwmRequest;
+import password.pwm.http.CommonValues;
 import password.pwm.ldap.UserInfo;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
@@ -141,7 +141,7 @@ public class TokenUtil
     }
 
     public static TokenPayload checkEnteredCode(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final String userEnteredCode,
             final TokenDestinationItem tokenDestinationItem,
             final UserIdentity userIdentity,
@@ -150,11 +150,13 @@ public class TokenUtil
     )
             throws PwmUnrecoverableException
     {
+        final PwmApplication pwmApplication = commonValues.getPwmApplication();
+
         try
         {
-            final TokenPayload tokenPayload = pwmRequest.getPwmApplication().getTokenService().processUserEnteredCode(
-                    pwmRequest.getPwmSession(),
-                    pwmRequest.getUserInfoIfLoggedIn(),
+            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
+                    commonValues,
+                    userIdentity,
                     tokenType,
                     userEnteredCode,
                     tokenEntryType
@@ -175,7 +177,7 @@ public class TokenUtil
                         throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
                     }
 
-                    if ( !userIdentity.canonicalEquals( pwmRequest.getUserInfoIfLoggedIn(), pwmRequest.getPwmApplication() ) )
+                    if ( !userIdentity.canonicalEquals( tokenPayload.getUserIdentity(), pwmApplication ) )
                     {
                         final String errorMsg = "received token is not for currently authenticated user, received token is for: "
                                 + tokenPayload.getUserIdentity().toDisplayString();
@@ -206,12 +208,12 @@ public class TokenUtil
     }
 
     public static void initializeAndSendToken(
-            final PwmRequest pwmRequest,
+            final CommonValues commonValues,
             final TokenInitAndSendRequest tokenInitAndSendRequest
     )
             throws PwmUnrecoverableException
     {
-        final Configuration config = pwmRequest.getConfig();
+        final Configuration config = commonValues.getConfig();
         final UserInfo userInfo = tokenInitAndSendRequest.getUserInfo();
         final Map<String, String> tokenMapData = new LinkedHashMap<>();
         final MacroMachine macroMachine;
@@ -222,7 +224,12 @@ public class TokenUtil
             }
             else if ( tokenInitAndSendRequest.getUserInfo() != null )
             {
-                macroMachine = MacroMachine.forUser( pwmRequest, userInfo.getUserIdentity(), makeTokenDestStringReplacer( tokenInitAndSendRequest.getTokenDestinationItem() ) );
+                macroMachine = MacroMachine.forUser(
+                        commonValues.getPwmApplication(),
+                        commonValues.getLocale(),
+                        commonValues.getSessionLabel(),
+                        userInfo.getUserIdentity(),
+                        makeTokenDestStringReplacer( tokenInitAndSendRequest.getTokenDestinationItem() ) );
             }
             else
             {
@@ -256,14 +263,14 @@ public class TokenUtil
 
             try
             {
-                tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
+                tokenPayload = commonValues.getPwmApplication().getTokenService().createTokenPayload(
                         tokenInitAndSendRequest.getTokenType(),
                         tokenLifetime,
                         tokenMapData,
                         userInfo == null ? null : userInfo.getUserIdentity(),
                         tokenInitAndSendRequest.getTokenDestinationItem()
                 );
-                tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
+                tokenKey = commonValues.getPwmApplication().getTokenService().generateNewToken( tokenPayload, commonValues.getSessionLabel() );
             }
             catch ( PwmOperationalException e )
             {
@@ -273,22 +280,22 @@ public class TokenUtil
 
         final EmailItemBean emailItemBean = tokenInitAndSendRequest.getEmailToSend() == null
                 ? null
-                : config.readSettingAsEmail( tokenInitAndSendRequest.getEmailToSend(), pwmRequest.getLocale() );
+                : config.readSettingAsEmail( tokenInitAndSendRequest.getEmailToSend(), commonValues.getLocale() );
 
         final String smsMessage = tokenInitAndSendRequest.getSmsToSend() == null
                 ? null
-                : config.readSettingAsLocalizedString( tokenInitAndSendRequest.getSmsToSend(), pwmRequest.getLocale() );
+                : config.readSettingAsLocalizedString( tokenInitAndSendRequest.getSmsToSend(), commonValues.getLocale() );
 
         TokenService.TokenSender.sendToken(
                 TokenService.TokenSendInfo.builder()
-                        .pwmApplication( pwmRequest.getPwmApplication() )
+                        .pwmApplication( commonValues.getPwmApplication() )
                         .userInfo( userInfo )
                         .macroMachine( macroMachine )
                         .configuredEmailSetting( emailItemBean )
                         .tokenDestinationItem( tokenInitAndSendRequest.getTokenDestinationItem() )
                         .smsMessage( smsMessage )
                         .tokenKey( tokenKey )
-                        .sessionLabel( pwmRequest.getSessionLabel() )
+                        .sessionLabel( commonValues.getSessionLabel() )
                         .build()
         );
     }

+ 10 - 0
server/src/main/java/password/pwm/util/i18n/LocaleHelper.java

@@ -499,4 +499,14 @@ public class LocaleHelper
                 ? TextDirection.rtl
                 : TextDirection.ltr;
     }
+
+    public static Map<String, String> localeMapToStringMap( final Map<Locale, String> localeStringMap )
+    {
+        final Map<String, String> returnMap = new LinkedHashMap<>();
+        for ( final Map.Entry<Locale, String> entry : localeStringMap.entrySet() )
+        {
+            returnMap.put( LocaleHelper.getBrowserLocaleString( entry.getKey() ), entry.getValue() );
+        }
+        return Collections.unmodifiableMap( returnMap );
+    }
 }

+ 21 - 0
server/src/main/java/password/pwm/util/java/JsonUtil.java

@@ -32,6 +32,7 @@ import com.google.gson.JsonPrimitive;
 import com.google.gson.JsonSerializationContext;
 import com.google.gson.JsonSerializer;
 import com.google.gson.reflect.TypeToken;
+import com.novell.ldapchai.cr.ChallengeSet;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.PwmLdapVendor;
 import password.pwm.util.PasswordData;
@@ -265,6 +266,26 @@ public class JsonUtil
         }
     }
 
+    private static class ChallengeSetAdapter implements JsonDeserializer<ChallengeSet>
+    {
+        private ChallengeSetAdapter( )
+        {
+        }
+
+        public synchronized ChallengeSet deserialize( final JsonElement jsonElement, final Type type, final JsonDeserializationContext jsonDeserializationContext )
+        {
+            try
+            {
+                return null;
+            }
+            catch ( Exception e )
+            {
+                LOGGER.debug( () -> "unable to parse stored json ChallengeSet.class timestamp '" + jsonElement.getAsString() + "' error: " + e.getMessage() );
+                throw new JsonParseException( e );
+            }
+        }
+    }
+
     private static class ByteArrayToBase64TypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]>
     {
         public byte[] deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context ) throws JsonParseException

+ 1 - 0
server/src/main/java/password/pwm/util/logging/PwmLogManager.java

@@ -175,6 +175,7 @@ public class PwmLogManager
                 final RollingFileAppender fileAppender = new RollingFileAppender( patternLayout, fileName, true );
                 final Level level = Level.toLevel( fileLogLevel );
                 fileAppender.setThreshold( level );
+                fileAppender.setEncoding( PwmConstants.DEFAULT_CHARSET.name() );
                 fileAppender.setMaxBackupIndex( Integer.parseInt( config.readAppProperty( AppProperty.LOGGING_FILE_MAX_ROLLOVER ) ) );
                 fileAppender.setMaxFileSize( config.readAppProperty( AppProperty.LOGGING_FILE_MAX_SIZE ) );
 

+ 10 - 0
server/src/main/java/password/pwm/util/macro/MacroMachine.java

@@ -30,6 +30,7 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
 import password.pwm.http.PwmRequest;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
@@ -302,6 +303,15 @@ public class MacroMachine
         String replace( String matchedMacro, String newValue );
     }
 
+    public static MacroMachine forUser(
+            final CommonValues commonValues,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        return forUser( commonValues.getPwmApplication(), commonValues.getLocale(), commonValues.getSessionLabel(), userIdentity );
+    }
+
     public static MacroMachine forUser(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity

+ 129 - 0
server/src/main/java/password/pwm/util/secure/BeanCryptoMachine.java

@@ -0,0 +1,129 @@
+/*
+ * 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
+ */
+
+package password.pwm.util.secure;
+
+import lombok.Value;
+import password.pwm.AppProperty;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.Serializable;
+import java.time.Instant;
+import java.util.Optional;
+
+public class BeanCryptoMachine<T extends Serializable>
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( BeanCryptoMachine.class );
+    private static final String DELIMITER = ".";
+
+    private final CommonValues commonValues;
+    private final TimeDuration maxIdleTimeout;
+
+    private String key;
+
+    public BeanCryptoMachine( final CommonValues commonValues, final TimeDuration maxIdleTimeout )
+    {
+        this.commonValues = commonValues;
+        this.maxIdleTimeout = maxIdleTimeout;
+    }
+
+    private String newKey()
+    {
+        final int length = Integer.parseInt( commonValues.getConfig().readAppProperty( AppProperty.HTTP_COOKIE_NONCE_LENGTH ) );
+
+        final String random = commonValues.getPwmApplication().getSecureService().pwmRandom().alphaNumericString( length );
+
+        // timestamp component for uniqueness
+        final String prefix = Long.toString( System.currentTimeMillis(), Character.MAX_RADIX );
+
+        return random + prefix;
+    }
+
+    public Optional<T> decryprt(
+            final String input
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( StringUtil.isEmpty( input ) )
+        {
+            return Optional.empty();
+        }
+
+        final SecureService secureService = commonValues.getPwmApplication().getSecureService();
+        final int delimiterIndex = input.indexOf( DELIMITER );
+        final String key = input.substring( 0, delimiterIndex );
+        final String payload = input.substring( delimiterIndex + 1 );
+        final PwmSecurityKey pwmSecurityKey = secureService.appendedSecurityKey( key );
+        final Wrapper wrapper = secureService.decryptObject( payload, pwmSecurityKey, Wrapper.class );
+
+        final TimeDuration stateAge = TimeDuration.fromCurrent( wrapper.getTimestamp() );
+        if ( stateAge.isLongerThan( maxIdleTimeout ) )
+        {
+            LOGGER.trace( commonValues.getSessionLabel(), () -> "state in request is " + stateAge.asCompactString() + " old" );
+            return Optional.empty();
+        }
+
+        try
+        {
+            final Class restoreClass = Class.forName( wrapper.getClassName() );
+            final Object bean = JsonUtil.deserialize( wrapper.getBean(), restoreClass );
+
+            this.key = key;
+            return Optional.of( ( T ) bean );
+        }
+        catch ( ClassNotFoundException e )
+        {
+            final String msg = "error clasting return bean class";
+            throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_INTERNAL, msg ) );
+        }
+    }
+
+    public String encrypt( final T bean ) throws PwmUnrecoverableException
+    {
+        if ( key == null )
+        {
+            this.key = newKey();
+        }
+
+        final SecureService secureService = commonValues.getPwmApplication().getSecureService();
+        final PwmSecurityKey pwmSecurityKey = secureService.appendedSecurityKey( key );
+        final String className = bean.getClass().getName();
+        final String jsonBean = JsonUtil.serialize( bean );
+        final String payload = secureService.encryptObjectToString( new Wrapper( Instant.now(), className, jsonBean ), pwmSecurityKey );
+        return key + DELIMITER + payload;
+    }
+
+    @Value
+    static class Wrapper implements Serializable
+    {
+        private Instant timestamp;
+        private String className;
+        private String bean;
+    }
+}

+ 6 - 0
server/src/main/java/password/pwm/util/secure/SecureService.java

@@ -184,4 +184,10 @@ public class SecureService implements PwmService
         }
         return pwmRandom;
     }
+
+    public PwmSecurityKey appendedSecurityKey( final String appendage ) throws PwmUnrecoverableException
+    {
+        final String hash = this.pwmSecurityKey.keyHash( this  );
+        return new PwmSecurityKey( hash + appendage );
+    }
 }

+ 41 - 0
server/src/main/java/password/pwm/ws/server/PresentableForm.java

@@ -0,0 +1,41 @@
+/*
+ * 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
+ */
+
+package password.pwm.ws.server;
+
+import lombok.Builder;
+import lombok.Singular;
+import lombok.Value;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Value
+@Builder
+public class PresentableForm implements Serializable
+{
+    @Singular
+    private List<PresentableFormRow> formRows;
+    private String label;
+    private String message;
+    private String messageDetail;
+}

+ 74 - 0
server/src/main/java/password/pwm/ws/server/PresentableFormRow.java

@@ -0,0 +1,74 @@
+/*
+ * 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
+ */
+
+package password.pwm.ws.server;
+
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.config.value.data.FormConfiguration;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+@Value
+@Builder
+public class PresentableFormRow implements Serializable
+{
+    private String name;
+    private int minimumLength;
+    private int maximumLength;
+    private FormConfiguration.Type type;
+    private boolean required;
+    private String label;
+    private Map<String, String> selectOptions;
+
+    public static PresentableFormRow fromFormConfiguration( final FormConfiguration formConfiguration, final Locale locale )
+    {
+        final String label = formConfiguration.getLabel( locale );
+
+        return PresentableFormRow.builder()
+                .name( formConfiguration.getName() )
+                .label( label )
+                .minimumLength( formConfiguration.getMinimumLength() )
+                .maximumLength( formConfiguration.getMaximumLength() )
+                .type( formConfiguration.getType() )
+                .required ( formConfiguration.isRequired() )
+                .selectOptions( formConfiguration.getSelectOptions() )
+                .build();
+    }
+
+    public static List<PresentableFormRow> fromFormConfigurations( final List<FormConfiguration> formConfigurations, final Locale locale )
+    {
+        final List<PresentableFormRow> formRows = new ArrayList<>();
+        for ( final FormConfiguration formConfiguration : formConfigurations )
+        {
+            formRows.add( PresentableFormRow.fromFormConfiguration( formConfiguration, locale ) );
+        }
+        return Collections.unmodifiableList( formRows );
+    }
+}
+
+

+ 6 - 16
server/src/main/java/password/pwm/ws/server/RestAuthenticationProcessor.java

@@ -44,7 +44,6 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.logging.PwmLogger;
 
 import javax.servlet.http.HttpServletRequest;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -123,32 +122,23 @@ public class RestAuthenticationProcessor
                         RestAuthenticationType.LDAP,
                         null,
                         userIdentity,
-                        Collections.unmodifiableSet( new HashSet<>( Arrays.asList( WebServiceUsage.values() ) ) ),
+                        Collections.unmodifiableSet( new HashSet<>( WebServiceUsage.forType( RestAuthenticationType.LDAP ) ) ),
                         thirdParty,
                         chaiProvider
                 );
             }
         }
 
-        final Set<WebServiceUsage> publicUsages;
-        if ( pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.PUBLIC_HEALTH_STATS_WEBSERVICES ) )
-        {
-            final WebServiceUsage[] usages = {
-                    WebServiceUsage.Health,
-                    WebServiceUsage.Statistics,
-            };
-            publicUsages = Collections.unmodifiableSet( new HashSet<>( Arrays.asList( usages ) ) );
-        }
-        else
-        {
-            publicUsages = Collections.emptySet();
-        }
+        final Set<WebServiceUsage> publicUsages = WebServiceUsage.forType( RestAuthenticationType.PUBLIC );
+        final Set<WebServiceUsage> enabledUsages = new HashSet<>(
+                pwmApplication.getConfig().readSettingAsOptionList( PwmSetting.WEBSERVICES_PUBLIC_ENABLE, WebServiceUsage.class ) );
+        enabledUsages.retainAll( publicUsages );
 
         return new RestAuthentication(
                 RestAuthenticationType.PUBLIC,
                 null,
                 null,
-                publicUsages,
+                Collections.unmodifiableSet( enabledUsages ),
                 false,
                 null
         );

+ 9 - 0
server/src/main/java/password/pwm/ws/server/RestRequest.java

@@ -27,9 +27,11 @@ import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
 import password.pwm.http.HttpContentType;
 import password.pwm.http.HttpHeader;
 import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.http.servlet.PwmRequestID;
 import password.pwm.util.i18n.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
 
@@ -44,6 +46,7 @@ public class RestRequest extends PwmHttpRequestWrapper
     private final PwmApplication pwmApplication;
     private final RestAuthentication restAuthentication;
     private final SessionLabel sessionLabel;
+    private final PwmRequestID requestID;
 
     public static RestRequest forRequest(
             final PwmApplication pwmApplication,
@@ -67,6 +70,7 @@ public class RestRequest extends PwmHttpRequestWrapper
         this.pwmApplication = pwmApplication;
         this.restAuthentication = restAuthentication;
         this.sessionLabel = sessionLabel;
+        this.requestID = PwmRequestID.next();
     }
 
     public RestAuthentication getRestAuthentication( )
@@ -121,5 +125,10 @@ public class RestRequest extends PwmHttpRequestWrapper
         }
         return getPwmApplication().getProxyChaiProvider( ldapProfileID );
     }
+
+    public CommonValues commonValues()
+    {
+        return new CommonValues( pwmApplication, this.getSessionLabel(), this.getLocale(), requestID );
+    }
 }
 

+ 43 - 31
server/src/main/java/password/pwm/ws/server/RestResultBean.java

@@ -22,10 +22,8 @@
 
 package password.pwm.ws.server;
 
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import lombok.Builder;
+import lombok.Value;
 import password.pwm.PwmApplication;
 import password.pwm.config.Configuration;
 import password.pwm.error.ErrorInformation;
@@ -37,9 +35,8 @@ import password.pwm.util.java.JsonUtil;
 import java.io.Serializable;
 import java.util.Locale;
 
-@NoArgsConstructor( access = AccessLevel.PRIVATE )
-@Getter
-@Setter( AccessLevel.PRIVATE )
+@Value
+@Builder( toBuilder =  true )
 public class RestResultBean implements Serializable
 {
     private boolean error;
@@ -51,9 +48,9 @@ public class RestResultBean implements Serializable
 
     public static RestResultBean withData( final Serializable data )
     {
-        final RestResultBean restResultBean = new RestResultBean();
-        restResultBean.setData( data );
-        return restResultBean;
+        return RestResultBean.builder()
+                .data( data )
+                .build();
     }
 
     public static RestResultBean fromError(
@@ -64,17 +61,19 @@ public class RestResultBean implements Serializable
             final boolean forceDetail
     )
     {
-        final RestResultBean restResultBean = new RestResultBean();
-        restResultBean.setError( true );
-        restResultBean.setErrorMessage( errorInformation.toUserStr( locale, config ) );
-        if ( forceDetail || ( pwmApplication != null && pwmApplication.determineIfDetailErrorMsgShown() ) )
-        {
-            restResultBean.setErrorDetail( errorInformation.toDebugStr() );
-        }
-        restResultBean.setErrorCode( errorInformation.getError().getErrorCode() );
-        return restResultBean;
-    }
+        final String errorDetail =
+                errorInformation != null
+                        && ( forceDetail || pwmApplication != null && pwmApplication.determineIfDetailErrorMsgShown() )
+                ? errorInformation.toDebugStr()
+                : null;
 
+        return RestResultBean.builder()
+                .error( errorInformation != null )
+                .errorMessage( errorInformation == null ? null : errorInformation.toUserStr( locale, config ) )
+                .errorDetail( errorDetail )
+                .errorCode( errorInformation == null ? 0 : errorInformation.getError().getErrorCode() )
+                .build();
+    }
 
     public static RestResultBean fromError(
             final RestRequest restRequestBean,
@@ -87,6 +86,19 @@ public class RestResultBean implements Serializable
         return fromError( errorInformation, pwmApplication, locale, config, false );
     }
 
+    public static RestResultBean fromErrorWithData(
+            final RestRequest restRequestBean,
+            final ErrorInformation errorInformation,
+            final Serializable serializable
+    )
+    {
+        final PwmApplication pwmApplication = restRequestBean.getPwmApplication();
+        final Configuration config = restRequestBean.getPwmApplication().getConfig();
+        final Locale locale = restRequestBean.getLocale();
+        return fromError( errorInformation, pwmApplication, locale, config, false ).toBuilder().data( serializable ).build();
+    }
+
+
     public static RestResultBean fromError(
             final ErrorInformation errorInformation
     )
@@ -129,11 +141,11 @@ public class RestResultBean implements Serializable
 
     )
     {
-        final RestResultBean restResultBean = new RestResultBean();
         final String msgText = Message.getLocalizedMessage( locale, message, config, fieldValues );
-        restResultBean.setSuccessMessage( msgText );
-        restResultBean.setData( data );
-        return restResultBean;
+        return RestResultBean.builder()
+                .successMessage( msgText )
+                .data( data )
+                .build();
     }
 
     public static RestResultBean forSuccessMessage(
@@ -144,10 +156,10 @@ public class RestResultBean implements Serializable
 
     )
     {
-        final RestResultBean restResultBean = new RestResultBean();
         final String msgText = Message.getLocalizedMessage( locale, message, config, fieldValues );
-        restResultBean.setSuccessMessage( msgText );
-        return restResultBean;
+        return RestResultBean.builder()
+                .successMessage( msgText )
+                .build();
     }
 
     public static RestResultBean forSuccessMessage(
@@ -194,10 +206,10 @@ public class RestResultBean implements Serializable
             final Config message
     )
     {
-        final RestResultBean restResultBean = new RestResultBean();
         final String msgText = Config.getLocalizedMessage( locale, message, config );
-        restResultBean.setSuccessMessage( msgText );
-        return restResultBean;
+        return RestResultBean.builder()
+            .successMessage( msgText )
+            .build();
     }
 
     public static RestResultBean forConfirmMessage(
@@ -211,6 +223,6 @@ public class RestResultBean implements Serializable
 
     public String toJson( )
     {
-        return JsonUtil.serialize( this ) + "\n";
+        return JsonUtil.serialize( this, JsonUtil.Flag.PrettyPrint ) + "\n";
     }
 }

+ 34 - 16
server/src/main/java/password/pwm/ws/server/RestServlet.java

@@ -33,6 +33,7 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.PwmSetting;
+import password.pwm.config.option.WebServiceUsage;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
@@ -93,6 +94,7 @@ public abstract class RestServlet extends HttpServlet
         {
             final List<Locale> knownLocales = pwmApplication.getConfig().getKnownLocales();
             locale = LocaleHelper.localeResolver( req.getLocale(), knownLocales );
+            resp.setHeader( HttpHeader.ContentLanguage.getHttpName(), LocaleHelper.getBrowserLocaleString( locale ) );
         }
 
         final SessionLabel sessionLabel;
@@ -203,10 +205,12 @@ public abstract class RestServlet extends HttpServlet
                 {
                     throw ( PwmUnrecoverableException ) rootException;
                 }
+                LOGGER.error( restRequest.getSessionLabel(), "internal error executing rest request: " + e.getMessage(), e );
                 throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, e.getMessage() );
             }
             catch ( IllegalAccessException e )
             {
+                LOGGER.error( restRequest.getSessionLabel(), "internal error executing rest request: " + e.getMessage(), e );
                 throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, e.getMessage() );
             }
         }
@@ -222,7 +226,7 @@ public abstract class RestServlet extends HttpServlet
     }
 
     private Method discoverMethodForAction( final Class clazz, final RestRequest restRequest )
-            throws PwmUnrecoverableException
+            throws PwmUnrecoverableException, IOException
     {
         final HttpMethod reqMethod = restRequest.getMethod();
         final HttpContentType reqContent = restRequest.readContentType();
@@ -268,23 +272,23 @@ public abstract class RestServlet extends HttpServlet
         final String errorMsg;
         if ( !anyMatch.isMethodMatch() )
         {
-            errorMsg = "HTTP method invalid";
+            errorMsg = "HTTP method unavailable";
         }
         else if ( reqAccept == null && !anyMatch.isAcceptMatch() )
         {
             errorMsg = HttpHeader.Accept.getHttpName() + " header is required";
         }
-        else if ( !anyMatch.isAcceptMatch() )
-        {
-            errorMsg = HttpHeader.Accept.getHttpName() + " header value does not match an available processor";
-        }
         else if ( reqContent == null && !anyMatch.isContentMatch() )
         {
             errorMsg = HttpHeader.ContentType.getHttpName() + " header is required";
         }
+        else if ( !anyMatch.isAcceptMatch() )
+        {
+            errorMsg = HttpHeader.Accept.getHttpName() + " value is not accepted for this service";
+        }
         else if ( !anyMatch.isContentMatch() )
         {
-            errorMsg = HttpHeader.ContentType.getHttpName() + " header value does not match an available processor";
+            errorMsg = HttpHeader.ContentType.getHttpName() + " value is not accepted for this service";
         }
         else
         {
@@ -303,20 +307,34 @@ public abstract class RestServlet extends HttpServlet
             throw PwmUnrecoverableException.newException( PwmError.ERROR_INTERNAL, "class is missing " + RestWebServer.class.getSimpleName() + " annotation" );
         }
 
-
-        if ( classAnnotation.requireAuthentication() )
         {
-            if ( restRequest.getRestAuthentication().getType() == RestAuthenticationType.PUBLIC )
+            final RestAuthenticationType requestAuthType = restRequest.getRestAuthentication().getType();
+            final Collection<RestAuthenticationType> supportedTypes = classAnnotation.webService().getTypes();
+            if ( !supportedTypes.contains( requestAuthType ) )
             {
-                throw PwmUnrecoverableException.newException( PwmError.ERROR_UNAUTHORIZED, "this service requires authentication" );
+
+                final String msg;
+                if ( requestAuthType == RestAuthenticationType.PUBLIC )
+                {
+                    msg = "this service requires authentication";
+                }
+                else
+                {
+                    msg = "authentication type " + requestAuthType + " is not supported for this service";
+                }
+                LOGGER.trace( restRequest.getSessionLabel(), () -> msg );
+                throw PwmUnrecoverableException.newException( PwmError.ERROR_UNAUTHORIZED, msg );
             }
+        }
 
-            if ( !restRequest.getRestAuthentication().getUsages().contains( classAnnotation.webService() ) )
+        {
+            final WebServiceUsage thisWebService = classAnnotation.webService();
+            if ( !restRequest.getRestAuthentication().getUsages().contains( thisWebService ) )
             {
-                throw PwmUnrecoverableException.newException(
-                        PwmError.ERROR_UNAUTHORIZED,
-                        "access to " + classAnnotation.webService() + " service is not permitted for this login"
-                );
+                LOGGER.trace( restRequest.getSessionLabel(), () -> "permission denied for request to " + thisWebService
+                        + ", not a permitted usage for authentication type " + restRequest.getRestAuthentication().getType() );
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, null );
+                throw new PwmUnrecoverableException( errorInformation );
             }
         }
 

+ 0 - 2
server/src/main/java/password/pwm/ws/server/RestWebServer.java

@@ -34,6 +34,4 @@ import java.lang.annotation.Target;
 public @interface RestWebServer
 {
     WebServiceUsage webService( );
-
-    boolean requireAuthentication( );
 }

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestChallengesServer.java

@@ -72,7 +72,7 @@ import java.util.Map;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/challenges"
         }
 )
-@RestWebServer( webService = WebServiceUsage.CheckPassword, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.CheckPassword )
 public class RestChallengesServer extends RestServlet
 {
 

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestCheckPasswordServer.java

@@ -62,7 +62,7 @@ import java.time.Instant;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/checkpassword"
         }
 )
-@RestWebServer( webService = WebServiceUsage.CheckPassword, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.CheckPassword )
 public class RestCheckPasswordServer extends RestServlet
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestCheckPasswordServer.class );

+ 168 - 0
server/src/main/java/password/pwm/ws/server/rest/RestForgottenPasswordServer.java

@@ -0,0 +1,168 @@
+/*
+ * 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
+ */
+
+package password.pwm.ws.server.rest;
+
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.PwmConstants;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.config.option.WebServiceUsage;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.CommonValues;
+import password.pwm.http.HttpContentType;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.bean.ForgottenPasswordBean;
+import password.pwm.http.bean.ForgottenPasswordStage;
+import password.pwm.http.servlet.forgottenpw.ForgottenPasswordStateMachine;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.secure.BeanCryptoMachine;
+import password.pwm.ws.server.PresentableForm;
+import password.pwm.ws.server.RestMethodHandler;
+import password.pwm.ws.server.RestRequest;
+import password.pwm.ws.server.RestResultBean;
+import password.pwm.ws.server.RestServlet;
+import password.pwm.ws.server.RestWebServer;
+
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Optional;
+
+@WebServlet(
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/forgottenpassword",
+        }
+)
+@RestWebServer( webService = WebServiceUsage.ForgottenPassword )
+public class RestForgottenPasswordServer extends RestServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( RestForgottenPasswordServer.class );
+
+    @Value
+    @Builder
+    static class JsonResponse implements Serializable
+    {
+        private ForgottenPasswordStage stage;
+        private IdentityVerificationMethod method;
+        private PresentableForm form;
+        private String state;
+
+        static JsonResponse makeResponse(
+                final BeanCryptoMachine<ForgottenPasswordBean> beanBeanCryptoMachine,
+                final ForgottenPasswordStateMachine stateMachine
+        )
+                throws PwmUnrecoverableException
+        {
+            final String encryptedState = beanBeanCryptoMachine.encrypt( stateMachine.getForgottenPasswordBean() );
+            return JsonResponse.builder()
+                    .state( encryptedState )
+                    .method( stateMachine.getForgottenPasswordBean().getProgress().getInProgressVerificationMethod() )
+                    .stage( stateMachine.nextStage() )
+                    .form( stateMachine.nextForm() )
+                    .build();
+        }
+    }
+
+    @Value
+    static class JsonInput implements Serializable
+    {
+        private String state;
+        private Map<String, String> form;
+    }
+
+
+    @Override
+    public void preCheckRequest( final RestRequest request ) throws PwmUnrecoverableException
+    {
+
+    }
+
+    @RestMethodHandler( method = HttpMethod.POST, consumes = HttpContentType.json, produces = HttpContentType.json )
+    public RestResultBean doRestForgottenPasswordService( final RestRequest restRequest )
+            throws PwmUnrecoverableException, IOException
+    {
+        final CommonValues commonValues = restRequest.commonValues();
+        final JsonInput jsonInput = restRequest.readBodyAsJsonObject( JsonInput.class );
+        final BeanCryptoMachine<ForgottenPasswordBean> beanBeanCryptoMachine = new BeanCryptoMachine<>( commonValues, figureMaxIdleTimeout( commonValues ) );
+        final ForgottenPasswordStateMachine stateMachine;
+
+        final boolean newState;
+        try
+        {
+            final Optional<ForgottenPasswordBean> readBean = beanBeanCryptoMachine.decryprt( jsonInput.getState() );
+            final ForgottenPasswordBean inputBean = readBean.orElseGet( ForgottenPasswordBean::new );
+            stateMachine = new ForgottenPasswordStateMachine(
+                    restRequest.commonValues(),
+                    inputBean );
+
+            newState = !readBean.isPresent();
+
+            stateMachine.nextStage();
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            return RestResultBean.fromError( e.getErrorInformation() );
+        }
+
+        ErrorInformation errorInformation = null;
+        if ( !newState && !JavaHelper.isEmpty( jsonInput.getForm() ) )
+        {
+            try
+            {
+                stateMachine.applyFormValues( jsonInput.getForm() );
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                errorInformation = e.getErrorInformation();
+            }
+        }
+
+        JsonResponse jsonResponse = null;
+        try
+        {
+            jsonResponse = JsonResponse.makeResponse( beanBeanCryptoMachine, stateMachine );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            errorInformation = e.getErrorInformation();
+        }
+
+        final RestResultBean restResultBean = RestResultBean.fromErrorWithData( restRequest, errorInformation, jsonResponse );
+        LOGGER.trace( restRequest.getSessionLabel(), () -> "Sending Response State: " + JsonUtil.serialize( restResultBean ) );
+        return restResultBean;
+    }
+
+    private TimeDuration figureMaxIdleTimeout( final CommonValues commonValues )
+    {
+        final long idleSeconds = commonValues.getConfig().readSettingAsLong( PwmSetting.IDLE_TIMEOUT_SECONDS );
+        return TimeDuration.of( idleSeconds, TimeDuration.Unit.SECONDS );
+    }
+
+}
+

+ 1 - 8
server/src/main/java/password/pwm/ws/server/rest/RestFormSigningServer.java

@@ -57,7 +57,7 @@ import java.util.Map;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/signing/form",
         }
 )
-@RestWebServer( webService = WebServiceUsage.SigningForm, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.SigningForm )
 public class RestFormSigningServer extends RestServlet
 {
 
@@ -93,13 +93,6 @@ public class RestFormSigningServer extends RestServlet
     )
             throws PwmUnrecoverableException
     {
-        if ( !restRequest.getRestAuthentication().getUsages().contains( WebServiceUsage.SigningForm ) )
-        {
-            final String errorMsg = "request is not authenticated with permission for " + WebServiceUsage.SigningForm;
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNAUTHORIZED, errorMsg );
-            return RestResultBean.fromError( errorInformation );
-        }
-
         try
         {
             if ( !JavaHelper.isEmpty( inputFormData ) )

+ 1 - 5
server/src/main/java/password/pwm/ws/server/rest/RestHealthServer.java

@@ -52,7 +52,7 @@ import java.util.Locale;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/health",
         }
 )
-@RestWebServer( webService = WebServiceUsage.Health, requireAuthentication = false )
+@RestWebServer( webService = WebServiceUsage.Health )
 public class RestHealthServer extends RestServlet
 {
     private static final String PARAM_IMMEDIATE_REFRESH = "refreshImmediate";
@@ -60,10 +60,6 @@ public class RestHealthServer extends RestServlet
     @Override
     public void preCheckRequest( final RestRequest restRequest ) throws PwmUnrecoverableException
     {
-        if ( !restRequest.getRestAuthentication().getUsages().contains( WebServiceUsage.Health ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "public health service is not enabled" );
-        }
     }
 
     @RestMethodHandler( method = HttpMethod.GET, produces = HttpContentType.plain )

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestProfileServer.java

@@ -71,7 +71,7 @@ import java.util.Set;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/profile",
         }
 )
-@RestWebServer( webService = WebServiceUsage.RandomPassword, requireAuthentication = false )
+@RestWebServer( webService = WebServiceUsage.RandomPassword )
 public class RestProfileServer extends RestServlet
 {
 

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestRandomPasswordServer.java

@@ -58,7 +58,7 @@ import java.util.List;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/randompassword",
         }
 )
-@RestWebServer( webService = WebServiceUsage.RandomPassword, requireAuthentication = false )
+@RestWebServer( webService = WebServiceUsage.RandomPassword )
 public class RestRandomPasswordServer extends RestServlet
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestRandomPasswordServer.class );

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestSetPasswordServer.java

@@ -59,7 +59,7 @@ import java.io.Serializable;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/setpassword"
         }
 )
-@RestWebServer( webService = WebServiceUsage.SetPassword, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.SetPassword )
 public class RestSetPasswordServer extends RestServlet
 {
 

+ 1 - 5
server/src/main/java/password/pwm/ws/server/rest/RestStatisticsServer.java

@@ -69,7 +69,7 @@ import java.util.TreeMap;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/statistics"
         }
 )
-@RestWebServer( webService = WebServiceUsage.Statistics, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.Statistics )
 public class RestStatisticsServer extends RestServlet
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestStatisticsServer.class );
@@ -120,10 +120,6 @@ public class RestStatisticsServer extends RestServlet
     @Override
     public void preCheckRequest( final RestRequest restRequest ) throws PwmUnrecoverableException
     {
-        if ( !restRequest.getRestAuthentication().getUsages().contains( WebServiceUsage.Health ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "public statistics service is not enabled" );
-        }
     }
 
     @RestMethodHandler( method = HttpMethod.GET, consumes = HttpContentType.form, produces = HttpContentType.json )

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestStatusServer.java

@@ -56,7 +56,7 @@ import java.time.Instant;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/status",
         }
 )
-@RestWebServer( webService = WebServiceUsage.Status, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.Status )
 public class RestStatusServer extends RestServlet
 {
     public static final PwmLogger LOGGER = PwmLogger.forClass( RestStatusServer.class );

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestVerifyOtpServer.java

@@ -53,7 +53,7 @@ import java.io.Serializable;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/verifyotp",
         }
 )
-@RestWebServer( webService = WebServiceUsage.VerifyOtp, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.VerifyOtp )
 public class RestVerifyOtpServer extends RestServlet
 {
 

+ 1 - 1
server/src/main/java/password/pwm/ws/server/rest/RestVerifyResponsesServer.java

@@ -58,7 +58,7 @@ import java.util.Map;
                 PwmConstants.URL_PREFIX_PUBLIC + PwmConstants.URL_PREFIX_REST + "/verifyresponses",
         }
 )
-@RestWebServer( webService = WebServiceUsage.VerifyResponses, requireAuthentication = true )
+@RestWebServer( webService = WebServiceUsage.VerifyResponses )
 public class RestVerifyResponsesServer extends RestServlet
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestVerifyResponsesServer.class );

+ 2 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -327,6 +327,8 @@ token.resend.delayMS=3000
 token.verifyPwModifyTime=true
 token.removeOnClaim=true
 token.storage.maxKeyLength=100
+rest.server.forgottenPW.token.display=%LABEL%  %MESSAGE%  %VALUE%
+rest.server.forgottenPW.ruleDelimiter=<br/>
 urlshortener.url.regex=(https?://([^:@]+(:[^@]+)?@)?([a-zA-Z0-9.]+|d{1,3}.d{1,3}.d{1,3}.d{1,3}|[[0-9a-fA-F:]+])(:d{1,5})?/*[a-zA-Z0-9/\%_.]*?*[a-zA-Z0-9/\%_.=&#]*)
 wordlist.builtin.path=/WEB-INF/wordlist.zip
 wordlist.maxCharLength=64

+ 13 - 5
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -3939,11 +3939,6 @@
             <value>false</value>
         </default>
     </setting>
-    <setting hidden="false" key="webservices.healthStats.makePublic" level="2">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="false" key="webservices.external.secrets" level="2">
         <default/>
         <options>
@@ -3975,6 +3970,14 @@
             <value/>
         </default>
     </setting>
+    <setting hidden="false" key="webservices.public.enable" level="2">
+        <default/>
+        <options>
+            <option value="Health">Health - /health</option>
+            <option value="ForgottenPassword">Forgotten Password - /forgottenpassword</option>
+            <option value="Statistics">Statistics - /statistics</option>
+        </options>
+    </setting>
     <setting hidden="false" key="external.macros.urls" level="2">
         <default />
     </setting>
@@ -4081,6 +4084,11 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="true" key="webservices.healthStats.makePublic" level="2">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="true" key="challenge.enforceMinimumPasswordLifetime" level="2" required="true">
         <!-- deprecated 2018-02-27-->
         <default>

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

@@ -61,6 +61,7 @@ Button_RecoverPassword=Check Answers
 Button_Reset=Clear
 Button_Search=Search
 Button_SetResponses=Save Answers
+Button_Select=Select
 Button_Show=Show
 Button_Show_Responses=Show Responses
 Button_Skip=Skip

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

@@ -736,6 +736,7 @@ Setting_Description_webservices.enableReadAnswers=Enable this option to allow @P
 Setting_Description_webservices.external.secrets=Define Users and Passwords that are able to authenticate to and use the REST web services.  For each user, a list of available services and a password can be defined.  Invoking the REST web services using these users does not require an LDAP user and instead relies on the configured LDAP proxy user.   In most cases this is the prefered appraoch for REST clients to authenticate.  Usernames defined here will preempt LDAP username resolution.
 Setting_Description_webservices.healthStats.makePublic=Enable this option to enable the Health and Statistics web services publicly.  Normally, these require authentication as there might be security sensitive data available.  Enabling this option allows the users to use the web services without authentication.  You must enable this is setting for the public (non-authenticated) page at <i>/public/health.jsp</i> to be functional.
 Setting_Description_webservices.queryMatch=Add an LDAP filter that contains the users permitted to execute REST web services.
+Setting_Description_webservices.public.enable=Web Services which are enabled for public (unauthenticated) usage.
 Setting_Description_webservices.thirdParty.queryMatch=Add an LDAP filter that contains the users permitted to execute REST web services and specify a third party via the 'username' parameter.
 Setting_Description_webservice.userAttributes=Add the user attributes that the various web services use and @PwmAppName@ presents as part of the users' data sets.
 Setting_Description_wordlistCaseSensitive=Enable this option to treat the word list as case sensitive for all matches. Changing this value causes @PwmAppName@ to recompile the word list.
@@ -888,7 +889,7 @@ Setting_Label_external.macros.urls=External Macro REST Server URLs
 Setting_Label_external.pwcheck.urls=External Password Check REST Server URLs
 Setting_Label_external.remoteData.url=Remote Form Data Service
 Setting_Label_external.remoteResponses.url=External Remote Responses REST Server URL
-Setting_Label_external.webservices.enable=Enable Web Services
+Setting_Label_external.webservices.enable=Enable REST Web Server
 Setting_Label_forceBasicAuth=Force Basic Authentication
 Setting_Label_forgottenUsername.enable=Enable Forgotten User Name
 Setting_Label_forgottenUsername.form=Forgotten User Name Form
@@ -1253,9 +1254,10 @@ Setting_Label_updateAttributes.writeAttributes=Update Profile Actions
 Setting_Label_urlshortener.classname=Enable URL Shortening Service Class
 Setting_Label_urlshortener.parameters=Configuration Parameters for URL Shortening Service
 Setting_Label_useXForwardedForHeader=Use X-Forwarded-For Header
-Setting_Label_webservices.enableReadAnswers=Allow Challenge Service to Read Answers
+Setting_Label_webservices.enableReadAnswers=Allow Challenge REST Service to Read Answers
 Setting_Label_webservices.external.secrets=Web Service Non-LDAP Users and Passwords
 Setting_Label_webservices.healthStats.makePublic=Enable Public Health and Statistics Web Services
+Setting_Label_webservices.public.enable=Public REST Web Services
 Setting_Label_webservices.queryMatch=Web Services LDAP Authentication Permissions
 Setting_Label_webservices.thirdParty.queryMatch=Web Services LDAP Third Party Permissions
 Setting_Label_webservice.userAttributes=Web Service User Attributes

+ 1 - 0
webapp/pom.xml

@@ -99,6 +99,7 @@
                 <configuration>
                     <archiveClasses>true</archiveClasses>
                     <packagingExcludes>**/*.jsp</packagingExcludes>
+                    <!-- remove/comment the next line to prevent pre-compiled JSPs from being used in the output WAR -->
                     <webXml>${project.build.directory}/web.xml</webXml>
                     <archive>
                         <manifestEntries>

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp

@@ -58,7 +58,7 @@
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-forward"></span></pwm:if>
                             <%=method.getLabel(pwmRequest.getConfig(),pwmRequest.getLocale())%>
                         </button>
-                        <input type="hidden" name="choice" value="<%=method.toString()%>"/>
+                        <input type="hidden" name="<%=PwmConstants.PARAM_METHOD_CHOICE%>" value="<%=method.toString()%>"/>
                         <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.verificationChoice%>"/>
                         <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                     </form>

+ 4 - 4
webapp/src/main/webapp/WEB-INF/jsp/forgottenpassword-responses.jsp

@@ -20,15 +20,15 @@
  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 --%>
 
-<%@ page import="com.novell.ldapchai.cr.Challenge" %>
-<%@ page import="com.novell.ldapchai.cr.ChallengeSet" %>
+<%@ page import="com.novell.ldapchai.cr.bean.ChallengeBean" %>
+<%@ page import="com.novell.ldapchai.cr.bean.ChallengeSetBean" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.tag.value.PwmValue" %>
 <%@ page import="password.pwm.http.PwmRequestAttribute" %>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
-<% final ChallengeSet challengeSet = (ChallengeSet)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordChallengeSet); %>
+<% final ChallengeSetBean challengeSet = (ChallengeSetBean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordChallengeSet); %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
 <%--
@@ -49,7 +49,7 @@ this is handled this way so on browsers where hiding fields is not possible, the
 
             <% // loop through challenges
                 int counter = 0;
-                for (final Challenge loopChallenge : challengeSet.getChallenges()) {
+                for (final ChallengeBean loopChallenge : challengeSet.getChallenges()) {
                     counter++;
             %>
             <h2><label for="PwmResponse_R_<%=counter%>"><%= loopChallenge.getChallengeText() %>

+ 208 - 0
webapp/src/main/webapp/public/examples/rest-client-example.js

@@ -0,0 +1,208 @@
+/*
+ * 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
+ */
+
+let lastState;
+
+function initPage() {
+    const currentUrl = window.location.origin;
+    let path = window.location.pathname;
+    if (path) {
+        path = path.split("/")[1]
+    }
+    document.getElementById("base-url").value = currentUrl + '/' + path;
+}
+
+function handleSendForm(event) {
+    event.preventDefault();
+    resetDebugForm();
+
+    const sendUrl = document.getElementById("base-url").value + valueOfSelectedOption("module-url");
+    writeDebugData("Send URL", sendUrl);
+
+    const sendData = {};
+    if ( lastState ) {
+        sendData.state = lastState;
+    }
+    sendData.form = readFormData();
+
+    const postBody = JSON.stringify(sendData);
+    resetDataForm();
+    document.getElementById('button-send').disabled = true;
+    const xmlHttpReq = new XMLHttpRequest();
+    xmlHttpReq.open("POST", sendUrl);
+    xmlHttpReq.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
+    xmlHttpReq.send(postBody);
+    writeDebugData("Sent JSON",postBody);
+    writeDebugData("curl command", makeCurlString(sendUrl,postBody));
+    xmlHttpReq.onreadystatechange = function() {
+        if (this.readyState === 4) {
+            if (this.status === 200) {
+                writeDebugData("Received JSON", this.responseText);
+                const responseData = JSON.parse(this.responseText);
+                processResponse(responseData);
+            } else if (this.readyState === 4) {
+                alert('send failed: ' + this.status + this.statusText);
+            }
+            document.getElementById('button-send').disabled = false;
+        }
+    };
+}
+
+function handleResetForm()
+{
+    lastState = null;
+    resetDataForm();
+    resetDebugForm();
+}
+
+function resetDataForm()
+{
+    document.getElementById("table-data-form").innerHTML = '';
+}
+
+function resetDebugForm()
+{
+    document.getElementById('table-debug-data').innerHTML = '';
+}
+
+function readFormData()
+{
+    const formData = {};
+    const forms = document.getElementById("data-form");
+    for(const field of forms.elements) {
+        if (field.tagName === 'INPUT') {
+            const fieldName = field.name;
+            formData[fieldName] = field.value;
+        } else if (field.tagName === 'SELECT') {
+            const fieldName = field.name;
+            formData[fieldName] = field.options[field.selectedIndex].value;
+        }
+    }
+    return formData;
+}
+function processResponse(responseData)
+{
+    document.getElementById("error-display").innerText = (responseData.error === true)
+        ? 'Error: ' + responseData.errorMessage + (responseData.errorDetail !== undefined ? ' ' + responseData.errorDetail : '')
+        : '';
+
+    if ( responseData.data ) {
+        if (responseData.data.stage) {
+            writeDebugData("Received Stage",responseData.data.stage);
+        }
+        if (responseData.data.method) {
+            writeDebugData("Received Method",responseData.data.method);
+        }
+
+        if (responseData.data.state) {
+            writeDebugData("Received State",responseData.data.state);
+            lastState = responseData.data.state;
+        }
+
+        if (responseData.data.form) {
+            resetDataForm();
+
+        }
+        const formData = responseData.data.form;
+        if (formData) {
+            createDataForm(formData);
+        }
+    }
+}
+
+function createDataForm(formData) {
+
+    if (formData.label) {
+        const element = document.createElement('span');
+        element.innerText = formData.label;
+        createRow('table-data-form', "Label", element);
+    }
+
+    if (formData.message) {
+        const element = document.createElement('span');
+        element.innerText = formData.message;
+        createRow('table-data-form', "Message", element);
+    }
+
+    if (formData.formRows) {
+        createDataFormRows(formData.formRows);
+    }
+}
+
+function createDataFormRows(formRows) {
+    for (const key of Object.keys(formRows)) {
+        const formRow = formRows[key];
+
+        let formElement;
+        if (formRow.type === 'select') {
+            formElement = document.createElement("select");
+            for (const key of Object.keys(formRow.selectOptions)) {
+                const value = formRow.selectOptions[key];
+                const optionElement = document.createElement("option");
+                optionElement.value = key;
+                optionElement.innerText = value;
+                formElement.appendChild(optionElement);
+            }
+        } else {
+            formElement = document.createElement("input");
+            formElement.type = formRow.type;
+        }
+        formElement.name = formRow.name;
+        formElement.id = formRow.name;
+
+        createRow('table-data-form',formRow.label,formElement);
+    }
+}
+
+function createRow(tableID,label,valueElement) {
+    const tableElement = document.getElementById(tableID);
+    const tableRow = tableElement.insertRow(-1);
+    const labelRow = tableRow.insertCell(0);
+    labelRow.innerText = label;
+    const dataRow = tableRow.insertCell(-1);
+    dataRow.appendChild(valueElement);
+}
+
+function writeDebugData(key,value)
+{
+    const tableElement = document.getElementById('table-debug-data');
+    const tableRow = tableElement.insertRow(-1);
+    const labelCell = tableRow.insertCell(-1);
+    labelCell.innerText = key;
+    const valueCell = tableRow.insertCell(-1);
+    valueCell.innerText = value;
+}
+
+function valueOfSelectedOption(elementID)
+{
+    var e = document.getElementById(elementID);
+    return  e.options[e.selectedIndex].value;
+}
+
+function makeCurlString(url,jsonBody) {
+    return "curl -kv -H 'Content-Type: application/json' -d '" + jsonBody + "' '" + url + "'";
+}
+
+initPage();
+document.getElementById("button-send").addEventListener("click",handleSendForm);
+document.getElementById("button-reset").addEventListener("click",handleResetForm);
+

+ 61 - 0
webapp/src/main/webapp/public/examples/rest-client-example.jsp

@@ -0,0 +1,61 @@
+<%--
+ ~ 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
+--%>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<html>
+<head>
+</head>
+<body>
+<div>
+    <form name="url-form">
+        <input type="text" name="base-url" id="base-url"/>
+        <select name="module-url" id="module-url">
+            <option value="/public/rest/forgottenpassword">/public/rest/forgottenpassword</option>
+        </select>
+    </form>
+</div>
+<br/>
+<div>
+    <span id="error-display"></span>
+</div>
+<br/>
+<div>
+    <span>Interaction Form</span>
+    <form name="data-form" id="data-form">
+        <table id="table-data-form" border="1">
+        </table>
+        <button type="submit" name="button-send" id="button-send">Send Data</button>
+        <button type="button" name="button-reset" id="button-reset">Reset</button>
+    </form>
+</div>
+<br/>
+<div>
+    <span>Debug Data</span>
+    <table id="table-debug-data" border="1">
+    </table>
+</div>
+<br/>
+
+<script type="text/javascript" src="rest-client-example.js"></script>
+</body>
+</html>

文件差異過大導致無法顯示
+ 186 - 0
webapp/src/main/webapp/public/reference/rest.jsp


+ 17 - 13
webapp/src/main/webapp/public/resources/js/changepassword.js

@@ -33,11 +33,15 @@ var PWM_CHANGEPW = PWM_CHANGEPW || {};
 
 // takes password values in the password fields, sends an http request to the servlet
 // and then parses (and displays) the response from the servlet.
+
+PWM_CHANGEPW.passwordField = "password1";
+PWM_CHANGEPW.passwordConfirmField = "password2";
+
 PWM_CHANGEPW.validatePasswords = function(userDN, nextFunction)
 {
-    if (PWM_GLOBAL['previousP1'] !== PWM_MAIN.getObject("password1").value) {  // if p1 is changing, then clear out p2.
-        PWM_MAIN.getObject("password2").value = "";
-        PWM_GLOBAL['previousP1'] = PWM_MAIN.getObject("password1").value;
+    if (PWM_GLOBAL['previousP1'] !== PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value) {  // if p1 is changing, then clear out p2.
+        PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField ).value = "";
+        PWM_GLOBAL['previousP1'] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value;
     }
 
     var validationProps = {};
@@ -47,8 +51,8 @@ PWM_CHANGEPW.validatePasswords = function(userDN, nextFunction)
     validationProps['serviceURL'] = PWM_MAIN.addParamToUrl(window.location.pathname, 'processAction','checkPassword');
     validationProps['readDataFunction'] = function(){
         var returnObj = {};
-        returnObj['password1'] = PWM_MAIN.getObject("password1").value;
-        returnObj['password2'] = PWM_MAIN.getObject("password2").value;
+        returnObj[PWM_CHANGEPW.passwordField ] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField ).value;
+        returnObj[PWM_CHANGEPW.passwordConfirmField ] = PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField ).value;
         if (userDN) {
             returnObj['username'] = userDN;
         }
@@ -131,7 +135,7 @@ PWM_CHANGEPW.markStrength = function(strength) { //strength meter
         return;
     }
 
-    if (PWM_MAIN.getObject("password1").value.length > 0) {
+    if (PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value.length > 0) {
         PWM_MAIN.getObject("strengthBox").style.visibility = 'visible';
     } else {
         PWM_MAIN.getObject("strengthBox").style.visibility = 'hidden';
@@ -177,9 +181,9 @@ PWM_CHANGEPW.copyToPasswordFields = function(text) { // used to copy auto-genera
 
     PWM_MAIN.closeWaitDialog();
 
-    PWM_MAIN.getObject("password1").value = text;
+    PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value = text;
     PWM_CHANGEPW.validatePasswords();
-    PWM_MAIN.getObject("password2").focus();
+    PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).focus();
 
     ShowHidePasswordHandler.show('password1');
 };
@@ -206,13 +210,13 @@ PWM_CHANGEPW.handleChangePasswordSubmit=function(event) {
             PWM_MAIN.closeWaitDialog();
             var match = data['data']['match'];
             if ('MATCH' !== match) {
-                PWM_MAIN.getObject("password2").value = '';
+                PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).value = '';
             }
             var okFunction = function() {
                 if ('MATCH' === match || 'EMPTY' === match) {
-                    PWM_MAIN.getObject("password1").focus();
+                    PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).focus();
                 } else {
-                    PWM_MAIN.getObject("password2").focus();
+                    PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).focus();
                 }
                 PWM_CHANGEPW.validatePasswords();
             };
@@ -382,8 +386,8 @@ PWM_CHANGEPW.startupChangePasswordPage=function() {
     PWM_MAIN.addEventHandler('button-reset','click',function(event){
         console.log('intercepted reset button');
 
-        var p1Value = PWM_MAIN.getObject("password1").value;
-        var p2Value = PWM_MAIN.getObject("password2").value;
+        var p1Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordField).value;
+        var p2Value = PWM_MAIN.getObject(PWM_CHANGEPW.passwordConfirmField).value;
 
         var submitForm = function(){
             var resetForm = PWM_MAIN.getObject('form-reset');

部分文件因文件數量過多而無法顯示