瀏覽代碼

Merge remote-tracking branch 'origin/master' into update-jersey

James Albright 9 年之前
父節點
當前提交
1d7a75e711
共有 48 個文件被更改,包括 683 次插入2402 次删除
  1. 1 1
      README.md
  2. 11 5
      pom.xml
  3. 2 0
      src/main/java/password/pwm/AppProperty.java
  4. 6 6
      src/main/java/password/pwm/VerificationMethodSystem.java
  5. 7 7
      src/main/java/password/pwm/bean/RemoteVerificationResponseBean.java
  6. 2 2
      src/main/java/password/pwm/config/PwmSetting.java
  7. 6 6
      src/main/java/password/pwm/config/option/IdentityVerificationMethod.java
  8. 4 4
      src/main/java/password/pwm/config/profile/AbstractProfile.java
  9. 6 6
      src/main/java/password/pwm/config/profile/ForgottenPasswordProfile.java
  10. 5 5
      src/main/java/password/pwm/config/profile/HelpdeskProfile.java
  11. 11 46
      src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java
  12. 31 9
      src/main/java/password/pwm/config/value/VerificationMethodValue.java
  13. 1 0
      src/main/java/password/pwm/http/PwmRequest.java
  14. 19 19
      src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  15. 2 0
      src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  16. 43 43
      src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  17. 2 2
      src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java
  18. 11 0
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskClientDataBean.java
  19. 141 17
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  20. 10 1
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationRequestBean.java
  21. 3 3
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationResponseBean.java
  22. 140 9
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationStateBean.java
  23. 1 1
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  24. 2 0
      src/main/java/password/pwm/i18n/Display.java
  25. 2 0
      src/main/java/password/pwm/i18n/Message.java
  26. 10 8
      src/main/java/password/pwm/ldap/UserSearchEngine.java
  27. 2 0
      src/main/java/password/pwm/svc/event/AuditEvent.java
  28. 0 2089
      src/main/java/password/pwm/util/Base64Util.java
  29. 8 8
      src/main/java/password/pwm/util/StringUtil.java
  30. 3 3
      src/main/java/password/pwm/util/logging/PwmLogEvent.java
  31. 8 10
      src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java
  32. 5 5
      src/main/java/password/pwm/ws/client/rest/naaf/NAAFLoginSequence.java
  33. 2 2
      src/main/java/password/pwm/ws/client/rest/naaf/PwmNAAFVerificationMethod.java
  34. 3 1
      src/main/resources/password/pwm/AppProperty.properties
  35. 11 10
      src/main/resources/password/pwm/config/PwmSetting.xml
  36. 2 0
      src/main/resources/password/pwm/i18n/Display.properties
  37. 2 0
      src/main/resources/password/pwm/i18n/Message.properties
  38. 1 1
      src/main/webapp/WEB-INF/jsp/configeditor.jsp
  39. 3 3
      src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp
  40. 3 3
      src/main/webapp/WEB-INF/jsp/forgottenpassword-naaf.jsp
  41. 3 3
      src/main/webapp/WEB-INF/jsp/forgottenpassword-remote.jsp
  42. 2 8
      src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp
  43. 7 1
      src/main/webapp/WEB-INF/jsp/helpdesk.jsp
  44. 1 1
      src/main/webapp/WEB-INF/jsp/peoplesearch.jsp
  45. 2 2
      src/main/webapp/private/config/index.jsp
  46. 1 1
      src/main/webapp/public/resources/js/admin.js
  47. 4 4
      src/main/webapp/public/resources/js/configeditor.js
  48. 131 47
      src/main/webapp/public/resources/js/helpdesk.js

+ 1 - 1
README.md

@@ -8,7 +8,7 @@ Official project page is at [https://github.com/pwm-project/pwm/](https://github
 * [Release Downloads](https://drive.google.com/folderview?id=0B3oHdiTrftrGV3ZrMi1LUzVCY1U&usp=sharing#list) - the release versions are quite dated, consider using a current build.
 * [Release Downloads](https://drive.google.com/folderview?id=0B3oHdiTrftrGV3ZrMi1LUzVCY1U&usp=sharing#list) - the release versions are quite dated, consider using a current build.
 * [Current Builds](http://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
 * [Current Builds](http://www.pwm-project.org/artifacts/pwm/) - Current downloads built from recent github project commits
 * [Google Groups](https://groups.google.com/group/pwm-general) - please ask for assistance here.
 * [Google Groups](https://groups.google.com/group/pwm-general) - please ask for assistance here.
-* [PWM Admin Guide](http://pwm.googlecode.com/svn/trunk/pwm/supplemental/PWMAdministrationGuide.pdf)
+* [PWM Admin Guide](http://pwm.googlecode.com/svn/trunk/pwm/supplemental/PWMAdministrationGuide.pdf) - guide for 1.7.  For current documentation, please help us migrate to the [PWM Wiki](https://github.com/pwm-project/pwm/wiki) 
 
 
 Features
 Features
 * Web based configuration manager with over 400 configurable settings
 * Web based configuration manager with over 400 configurable settings

+ 11 - 5
pom.xml

@@ -303,12 +303,12 @@
         <dependency>
         <dependency>
             <groupId>com.sun.mail</groupId>
             <groupId>com.sun.mail</groupId>
             <artifactId>javax.mail</artifactId>
             <artifactId>javax.mail</artifactId>
-            <version>1.5.2</version>
+            <version>1.5.5</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
             <artifactId>httpclient</artifactId>
-            <version>4.5.1</version>
+            <version>4.5.2</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
             <groupId>com.googlecode.concurrentlinkedhashmap</groupId>
@@ -343,7 +343,7 @@
         <dependency>
         <dependency>
             <groupId>org.jasig.cas.client</groupId>
             <groupId>org.jasig.cas.client</groupId>
             <artifactId>cas-client-core</artifactId>
             <artifactId>cas-client-core</artifactId>
-            <version>3.2.0</version>
+            <version>3.4.1</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.mapdb</groupId>
             <groupId>org.mapdb</groupId>
@@ -358,12 +358,12 @@
         <dependency>
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcprov-jdk15on</artifactId>
             <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.53</version>
+            <version>1.54</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>org.bouncycastle</groupId>
             <groupId>org.bouncycastle</groupId>
             <artifactId>bcpkix-jdk15on</artifactId>
             <artifactId>bcpkix-jdk15on</artifactId>
-            <version>1.53</version>
+            <version>1.54</version>
         </dependency>
         </dependency>
         <dependency>
         <dependency>
             <groupId>javax.xml</groupId>
             <groupId>javax.xml</groupId>
@@ -390,6 +390,12 @@
             <artifactId>jcl-core</artifactId>
             <artifactId>jcl-core</artifactId>
             <version>2.7</version>
             <version>2.7</version>
         </dependency>
         </dependency>
+        <dependency>
+            <groupId>net.iharder</groupId>
+            <artifactId>base64</artifactId>
+            <version>2.3.9</version>
+        </dependency>
+
 
 
         <!-- client webjar dependencies -->
         <!-- client webjar dependencies -->
         <!-- changes in client dependencies require updating AppProperty.properties:http.resources.webjarMappings -->
         <!-- changes in client dependencies require updating AppProperty.properties:http.resources.webjarMappings -->

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

@@ -148,6 +148,8 @@ public enum AppProperty {
     HEALTH_JAVA_MIN_HEAP_BYTES                      ("health.java.minHeapBytes"),
     HEALTH_JAVA_MIN_HEAP_BYTES                      ("health.java.minHeapBytes"),
     HELPDESK_TOKEN_MAX_AGE                          ("helpdesk.token.maxAgeSeconds"),
     HELPDESK_TOKEN_MAX_AGE                          ("helpdesk.token.maxAgeSeconds"),
     HELPDESK_TOKEN_VALUE                            ("helpdesk.token.value"),
     HELPDESK_TOKEN_VALUE                            ("helpdesk.token.value"),
+    HELPDESK_VERIFICATION_INVALID_DELAY_MS          ("helpdesk.verification.invalid.delayMs"),
+    HELPDESK_VERIFICATION_TIMEOUT_SECONDS           ("helpdesk.verification.timeoutSeconds"),
     LDAP_CHAI_SETTINGS                              ("ldap.chaiSettings"),
     LDAP_CHAI_SETTINGS                              ("ldap.chaiSettings"),
     LDAP_CONNECTION_TIMEOUT                         ("ldap.connection.timeoutMS"),
     LDAP_CONNECTION_TIMEOUT                         ("ldap.connection.timeoutMS"),
     LDAP_PROFILE_RETRY_DELAY                        ("ldap.profile.retryDelayMS"),
     LDAP_PROFILE_RETRY_DELAY                        ("ldap.profile.retryDelayMS"),

+ 6 - 6
src/main/java/password/pwm/RecoveryVerificationMethod.java → src/main/java/password/pwm/VerificationMethodSystem.java

@@ -32,7 +32,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 
 
-public interface RecoveryVerificationMethod {
+public interface VerificationMethodSystem {
     enum VerificationState {
     enum VerificationState {
         INPROGRESS,
         INPROGRESS,
         FAILED,
         FAILED,
@@ -65,15 +65,15 @@ public interface RecoveryVerificationMethod {
         }
         }
     }
     }
 
 
-    public List<UserPrompt> getCurrentPrompts() throws PwmUnrecoverableException;
+    List<UserPrompt> getCurrentPrompts() throws PwmUnrecoverableException;
 
 
-    public String getCurrentDisplayInstructions();
+    String getCurrentDisplayInstructions();
 
 
-    public ErrorInformation respondToPrompts(final Map<String, String> answers) throws PwmUnrecoverableException;
+    ErrorInformation respondToPrompts(final Map<String, String> answers) throws PwmUnrecoverableException;
 
 
-    public VerificationState getVerificationState();
+    VerificationState getVerificationState();
 
 
-    public void init(final PwmApplication pwmApplication, final UserInfoBean userInfoBean, SessionLabel sessionLabel, Locale locale)
+    void init(final PwmApplication pwmApplication, final UserInfoBean userInfoBean, SessionLabel sessionLabel, Locale locale)
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
             ;
             ;
 
 

+ 7 - 7
src/main/java/password/pwm/bean/RemoteVerificationResponseBean.java

@@ -22,15 +22,15 @@
 
 
 package password.pwm.bean;
 package password.pwm.bean;
 
 
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.List;
 import java.util.List;
 
 
 public class RemoteVerificationResponseBean implements Serializable {
 public class RemoteVerificationResponseBean implements Serializable {
     private String displayInstructions;
     private String displayInstructions;
-    private RecoveryVerificationMethod.VerificationState verificationState;
-    private List<RecoveryVerificationMethod.UserPromptBean> userPrompts;
+    private VerificationMethodSystem.VerificationState verificationState;
+    private List<VerificationMethodSystem.UserPromptBean> userPrompts;
     private String errorMessage;
     private String errorMessage;
 
 
     public String getDisplayInstructions() {
     public String getDisplayInstructions() {
@@ -41,19 +41,19 @@ public class RemoteVerificationResponseBean implements Serializable {
         this.displayInstructions = displayInstructions;
         this.displayInstructions = displayInstructions;
     }
     }
 
 
-    public RecoveryVerificationMethod.VerificationState getVerificationState() {
+    public VerificationMethodSystem.VerificationState getVerificationState() {
         return verificationState;
         return verificationState;
     }
     }
 
 
-    public void setVerificationState(RecoveryVerificationMethod.VerificationState verificationState) {
+    public void setVerificationState(VerificationMethodSystem.VerificationState verificationState) {
         this.verificationState = verificationState;
         this.verificationState = verificationState;
     }
     }
 
 
-    public List<RecoveryVerificationMethod.UserPromptBean> getUserPrompts() {
+    public List<VerificationMethodSystem.UserPromptBean> getUserPrompts() {
         return userPrompts;
         return userPrompts;
     }
     }
 
 
-    public void setUserPrompts(List<RecoveryVerificationMethod.UserPromptBean> userPrompts) {
+    public void setUserPrompts(List<VerificationMethodSystem.UserPromptBean> userPrompts) {
         this.userPrompts = userPrompts;
         this.userPrompts = userPrompts;
     }
     }
 
 

+ 2 - 2
src/main/java/password/pwm/config/PwmSetting.java

@@ -935,8 +935,6 @@ public enum PwmSetting {
             "helpdesk.displayName", PwmSettingSyntax.STRING, PwmSettingCategory.HELPDESK_PROFILE),
             "helpdesk.displayName", PwmSettingSyntax.STRING, PwmSettingCategory.HELPDESK_PROFILE),
     HELPDESK_TOKEN_SEND_METHOD(
     HELPDESK_TOKEN_SEND_METHOD(
             "helpdesk.token.sendMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.HELPDESK_PROFILE),
             "helpdesk.token.sendMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.HELPDESK_PROFILE),
-    HELPDESK_ENABLE_OTP_VERIFY(
-            "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_PROFILE),
     HELPDESK_PASSWORD_MASKVALUE(
     HELPDESK_PASSWORD_MASKVALUE(
             "helpdesk.setPassword.maskValue", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_PROFILE),
             "helpdesk.setPassword.maskValue", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_PROFILE),
     HELPDESK_VERIFICATION_METHODS(
     HELPDESK_VERIFICATION_METHODS(
@@ -1074,6 +1072,8 @@ public enum PwmSetting {
             "challenge.requireResponses", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS),
             "challenge.requireResponses", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS),
     FORGOTTEN_PASSWORD_REQUIRE_OTP(
     FORGOTTEN_PASSWORD_REQUIRE_OTP(
             "recovery.require.otp", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS),
             "recovery.require.otp", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS),
+    HELPDESK_ENABLE_OTP_VERIFY(
+            "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_PROFILE),
 
 
 
 
 
 

+ 6 - 6
src/main/java/password/pwm/config/option/RecoveryVerificationMethods.java → src/main/java/password/pwm/config/option/IdentityVerificationMethod.java

@@ -30,7 +30,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 
 
-public enum RecoveryVerificationMethods implements ConfigurationOption {
+public enum IdentityVerificationMethod implements ConfigurationOption {
     PREVIOUS_AUTH(      false,  Display.Field_VerificationMethodPreviousAuth),
     PREVIOUS_AUTH(      false,  Display.Field_VerificationMethodPreviousAuth),
     ATTRIBUTES(         true,   Display.Field_VerificationMethodAttributes),
     ATTRIBUTES(         true,   Display.Field_VerificationMethodAttributes),
     CHALLENGE_RESPONSES(true,   Display.Field_VerificationMethodChallengeResponses),
     CHALLENGE_RESPONSES(true,   Display.Field_VerificationMethodChallengeResponses),
@@ -44,7 +44,7 @@ public enum RecoveryVerificationMethods implements ConfigurationOption {
     private final boolean userSelectable;
     private final boolean userSelectable;
     private final Display displayKey;
     private final Display displayKey;
 
 
-    RecoveryVerificationMethods(boolean userSelectable, Display displayKey) {
+    IdentityVerificationMethod(boolean userSelectable, Display displayKey) {
         this.userSelectable = userSelectable;
         this.userSelectable = userSelectable;
         this.displayKey = displayKey;
         this.displayKey = displayKey;
     }
     }
@@ -61,10 +61,10 @@ public enum RecoveryVerificationMethods implements ConfigurationOption {
         return Display.getLocalizedMessage(locale, this.getDisplayKey(), configuration);
         return Display.getLocalizedMessage(locale, this.getDisplayKey(), configuration);
     }
     }
 
 
-    public static RecoveryVerificationMethods[] availableValues() {
-        final List<RecoveryVerificationMethods> values = new ArrayList<>();
-        values.addAll(Arrays.asList(RecoveryVerificationMethods.values()));
-        return values.toArray(new RecoveryVerificationMethods[values.size()]);
+    public static IdentityVerificationMethod[] availableValues() {
+        final List<IdentityVerificationMethod> values = new ArrayList<>();
+        values.addAll(Arrays.asList(IdentityVerificationMethod.values()));
+        return values.toArray(new IdentityVerificationMethod[values.size()]);
     }
     }
 
 
 }
 }

+ 4 - 4
src/main/java/password/pwm/config/profile/AbstractProfile.java

@@ -23,7 +23,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.config.*;
 import password.pwm.config.*;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.util.PasswordData;
 import password.pwm.util.PasswordData;
@@ -128,12 +128,12 @@ public abstract class AbstractProfile implements Profile, SettingReader {
         return valueMap;
         return valueMap;
     }
     }
 
 
-    public Set<RecoveryVerificationMethods> readVerificationMethods(final PwmSetting pwmSetting, VerificationMethodValue.EnabledState enabledState) {
-        final Set<RecoveryVerificationMethods> result = new LinkedHashSet<>();
+    public Set<IdentityVerificationMethod> readVerificationMethods(final PwmSetting pwmSetting, VerificationMethodValue.EnabledState enabledState) {
+        final Set<IdentityVerificationMethod> result = new LinkedHashSet<>();
         final StoredValue configValue = storedValueMap.get(pwmSetting);
         final StoredValue configValue = storedValueMap.get(pwmSetting);
         final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
         final VerificationMethodValue.VerificationMethodSettings verificationMethodSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
 
 
-        for (final RecoveryVerificationMethods recoveryVerificationMethods : RecoveryVerificationMethods.availableValues()) {
+        for (final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.availableValues()) {
             if (verificationMethodSettings.getMethodSettings().containsKey(recoveryVerificationMethods)) {
             if (verificationMethodSettings.getMethodSettings().containsKey(recoveryVerificationMethods)) {
                 if (verificationMethodSettings.getMethodSettings().get(recoveryVerificationMethods).getEnabledState() == enabledState) {
                 if (verificationMethodSettings.getMethodSettings().get(recoveryVerificationMethods).getEnabledState() == enabledState) {
                     result.add(recoveryVerificationMethods);
                     result.add(recoveryVerificationMethods);

+ 6 - 6
src/main/java/password/pwm/config/profile/ForgottenPasswordProfile.java

@@ -23,7 +23,7 @@
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
 import password.pwm.config.*;
 import password.pwm.config.*;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.config.value.VerificationMethodValue;
 
 
@@ -31,8 +31,8 @@ import java.util.*;
 
 
 public class ForgottenPasswordProfile extends AbstractProfile {
 public class ForgottenPasswordProfile extends AbstractProfile {
 
 
-    private Set<RecoveryVerificationMethods> requiredRecoveryVerificationMethods;
-    private Set<RecoveryVerificationMethods> optionalRecoveryVerificationMethods;
+    private Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods;
+    private Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods;
 
 
     public ForgottenPasswordProfile(String identifier, Map<PwmSetting, StoredValue> storedValueMap) {
     public ForgottenPasswordProfile(String identifier, Map<PwmSetting, StoredValue> storedValueMap) {
         super(identifier, storedValueMap);
         super(identifier, storedValueMap);
@@ -59,21 +59,21 @@ public class ForgottenPasswordProfile extends AbstractProfile {
         return ProfileType.ForgottenPassword;
         return ProfileType.ForgottenPassword;
     }
     }
     
     
-    public Set<RecoveryVerificationMethods> requiredRecoveryAuthenticationMethods() {
+    public Set<IdentityVerificationMethod> requiredRecoveryAuthenticationMethods() {
         if (requiredRecoveryVerificationMethods == null) {
         if (requiredRecoveryVerificationMethods == null) {
             requiredRecoveryVerificationMethods = readRecoveryAuthMethods(VerificationMethodValue.EnabledState.required);
             requiredRecoveryVerificationMethods = readRecoveryAuthMethods(VerificationMethodValue.EnabledState.required);
         }
         }
         return requiredRecoveryVerificationMethods;
         return requiredRecoveryVerificationMethods;
     }
     }
 
 
-    public Set<RecoveryVerificationMethods> optionalRecoveryAuthenticationMethods() {
+    public Set<IdentityVerificationMethod> optionalRecoveryAuthenticationMethods() {
         if (optionalRecoveryVerificationMethods == null) {
         if (optionalRecoveryVerificationMethods == null) {
             optionalRecoveryVerificationMethods = readRecoveryAuthMethods(VerificationMethodValue.EnabledState.optional);
             optionalRecoveryVerificationMethods = readRecoveryAuthMethods(VerificationMethodValue.EnabledState.optional);
         }
         }
         return optionalRecoveryVerificationMethods;
         return optionalRecoveryVerificationMethods;
     }
     }
     
     
-    private Set<RecoveryVerificationMethods> readRecoveryAuthMethods(final VerificationMethodValue.EnabledState enabledState) {
+    private Set<IdentityVerificationMethod> readRecoveryAuthMethods(final VerificationMethodValue.EnabledState enabledState) {
         return this.readVerificationMethods(PwmSetting.RECOVERY_VERIFICATION_METHODS, enabledState);
         return this.readVerificationMethods(PwmSetting.RECOVERY_VERIFICATION_METHODS, enabledState);
     }
     }
 
 

+ 5 - 5
src/main/java/password/pwm/config/profile/HelpdeskProfile.java

@@ -24,7 +24,7 @@ package password.pwm.config.profile;
 
 
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.value.VerificationMethodValue;
 import password.pwm.config.value.VerificationMethodValue;
 
 
@@ -54,14 +54,14 @@ public class HelpdeskProfile extends AbstractProfile implements Profile {
         return PROFILE_TYPE;
         return PROFILE_TYPE;
     }
     }
 
 
-    public  Collection<RecoveryVerificationMethods> readOptionalVerificationMethods() {
-        final Set<RecoveryVerificationMethods> result = new LinkedHashSet<>();
-        result.addAll(readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.optional));
+    public Collection<IdentityVerificationMethod> readOptionalVerificationMethods() {
+        final Set<IdentityVerificationMethod> result = new LinkedHashSet<>();
         result.addAll(readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.optional));
         result.addAll(readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.optional));
+        result.addAll(readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.required));
         return Collections.unmodifiableSet(result);
         return Collections.unmodifiableSet(result);
     }
     }
 
 
-    public  Collection<RecoveryVerificationMethods> readRequiredVerificationMethods() {
+    public Collection<IdentityVerificationMethod> readRequiredVerificationMethods() {
         return readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.required);
         return readVerificationMethods(PwmSetting.HELPDESK_VERIFICATION_METHODS, VerificationMethodValue.EnabledState.required);
     }
     }
 
 

+ 11 - 46
src/main/java/password/pwm/config/profile/PwmPasswordPolicy.java

@@ -22,32 +22,23 @@
 
 
 package password.pwm.config.profile;
 package password.pwm.config.profile;
 
 
-import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-
+import com.novell.ldapchai.ChaiPasswordPolicy;
+import com.novell.ldapchai.ChaiPasswordRule;
+import com.novell.ldapchai.util.DefaultChaiPasswordPolicy;
+import com.novell.ldapchai.util.PasswordRuleHelper;
+import com.novell.ldapchai.util.StringHelper;
 import password.pwm.config.UserPermission;
 import password.pwm.config.UserPermission;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.config.option.ADPolicyComplexity;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthMessage;
 import password.pwm.health.HealthRecord;
 import password.pwm.health.HealthRecord;
 import password.pwm.util.Helper;
 import password.pwm.util.Helper;
+import password.pwm.util.JsonUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
 
 
-import com.novell.ldapchai.ChaiPasswordPolicy;
-import com.novell.ldapchai.ChaiPasswordRule;
-import com.novell.ldapchai.util.DefaultChaiPasswordPolicy;
-import com.novell.ldapchai.util.PasswordRuleHelper;
-import com.novell.ldapchai.util.StringHelper;
+import java.io.Serializable;
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 
 
 
 /**
 /**
@@ -123,33 +114,7 @@ public class PwmPasswordPolicy implements Profile,Serializable {
 
 
     @Override
     @Override
     public String toString() {
     public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("PwmPasswordPolicy");
-        sb.append(": ");
-
-        final List<String> outputList = new ArrayList<>();
-        for (final String key : policyMap.keySet()) {
-            final PwmPasswordRule rule = PwmPasswordRule.forKey(key);
-            if (rule != null) {
-                switch (rule) {
-                    case DisallowedAttributes:
-                    case DisallowedValues:
-                        outputList.add(rule + "=[" + StringHelper.stringCollectionToString(StringHelper.tokenizeString(policyMap.get(key), "\n"), ", ") + "]");
-                        break;
-                    default:
-                        outputList.add(rule + "=" + policyMap.get(key));
-                        break;
-                }
-            } else {
-                outputList.add(key + "=" + policyMap.get(key));
-            }
-        }
-
-        sb.append("{");
-        sb.append(StringHelper.stringCollectionToString(outputList, ", "));
-        sb.append("}");
-
-        return sb.toString();
+        return "PwmPasswordPolicy" + ": " + JsonUtil.serialize(this);
     }
     }
 
 
     public ChaiPasswordPolicy getChaiPasswordPolicy() {
     public ChaiPasswordPolicy getChaiPasswordPolicy() {

+ 31 - 9
src/main/java/password/pwm/config/value/VerificationMethodValue.java

@@ -26,9 +26,11 @@ import org.jdom2.CDATA;
 import org.jdom2.Element;
 import org.jdom2.Element;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
+import password.pwm.i18n.Display;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.JsonUtil;
+import password.pwm.util.LocaleHelper;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmSecurityKey;
 import password.pwm.util.secure.PwmSecurityKey;
 
 
@@ -48,18 +50,18 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
     }
     }
 
 
     public static class VerificationMethodSettings implements Serializable {
     public static class VerificationMethodSettings implements Serializable {
-        private Map<RecoveryVerificationMethods,VerificationMethodSetting> methodSettings = new HashMap<>();
+        private Map<IdentityVerificationMethod,VerificationMethodSetting> methodSettings = new HashMap<>();
         private int minOptionalRequired = 0;
         private int minOptionalRequired = 0;
 
 
         public VerificationMethodSettings() {
         public VerificationMethodSettings() {
         }
         }
 
 
-        public VerificationMethodSettings(Map<RecoveryVerificationMethods, VerificationMethodSetting> methodSettings, int minOptionalRequired) {
+        public VerificationMethodSettings(Map<IdentityVerificationMethod, VerificationMethodSetting> methodSettings, int minOptionalRequired) {
             this.methodSettings = methodSettings;
             this.methodSettings = methodSettings;
             this.minOptionalRequired = minOptionalRequired;
             this.minOptionalRequired = minOptionalRequired;
         }
         }
 
 
-        public Map<RecoveryVerificationMethods, VerificationMethodSetting> getMethodSettings() {
+        public Map<IdentityVerificationMethod, VerificationMethodSetting> getMethodSettings() {
             return Collections.unmodifiableMap(methodSettings);
             return Collections.unmodifiableMap(methodSettings);
         }
         }
 
 
@@ -86,7 +88,7 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
 
 
     public VerificationMethodValue(VerificationMethodSettings value) {
     public VerificationMethodValue(VerificationMethodSettings value) {
         this.value = value;
         this.value = value;
-        for (final RecoveryVerificationMethods recoveryVerificationMethods : RecoveryVerificationMethods.availableValues()) {
+        for (final IdentityVerificationMethod recoveryVerificationMethods : IdentityVerificationMethod.availableValues()) {
             if (!value.methodSettings.containsKey(recoveryVerificationMethods)) {
             if (!value.methodSettings.containsKey(recoveryVerificationMethods)) {
                 value.methodSettings.put(recoveryVerificationMethods,new VerificationMethodSetting(EnabledState.disabled));
                 value.methodSettings.put(recoveryVerificationMethods,new VerificationMethodSetting(EnabledState.disabled));
             }
             }
@@ -140,11 +142,31 @@ public class VerificationMethodValue extends AbstractValue implements StoredValu
             return "No Verification Methods";
             return "No Verification Methods";
         }
         }
         final StringBuilder out = new StringBuilder();
         final StringBuilder out = new StringBuilder();
-        for (final RecoveryVerificationMethods method : value.getMethodSettings().keySet()) {
-            out.append(" ").append(method.toString()).append(": ").append(value.getMethodSettings().get(method).getEnabledState());
-            out.append("\n");
+        final List<String> optionals = new ArrayList<>();
+        final List<String> required = new ArrayList<>();
+        for (final IdentityVerificationMethod method : value.getMethodSettings().keySet()) {
+            switch (value.getMethodSettings().get(method).getEnabledState()) {
+                case optional:
+                    optionals.add(method.getLabel(null, locale));
+                    break;
+
+                case required:
+                    required.add(method.getLabel(null, locale));
+                    break;
+            }
+            method.getLabel(null,locale);
         }
         }
-        out.append("  Minimum Optional Methods Required: ").append(value.getMinOptionalRequired());
+
+        out.append("optional methods: ").append(optionals.isEmpty()
+                        ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, null)
+                        : JsonUtil.serializeCollection(optionals)
+        );
+        out.append(", required methods: ").append(required.isEmpty()
+                        ? LocaleHelper.getLocalizedMessage(locale, Display.Value_NotApplicable, null)
+                        : JsonUtil.serializeCollection(required)
+        );
+
+        out.append(",  minimum optional methods required: ").append(value.getMinOptionalRequired());
         return out.toString();
         return out.toString();
     }
     }
 }
 }

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

@@ -273,6 +273,7 @@ public class PwmRequest extends PwmHttpRequestWrapper implements Serializable {
         HelpdeskDetail,
         HelpdeskDetail,
         HelpdeskObfuscatedDN,
         HelpdeskObfuscatedDN,
         HelpdeskUsername,
         HelpdeskUsername,
+        HelpdeskVerificationEnabled,
 
 
         ConfigFilename,
         ConfigFilename,
         ConfigLastModified,
         ConfigLastModified,

+ 19 - 19
src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -24,11 +24,11 @@ package password.pwm.http.bean;
 
 
 import com.novell.ldapchai.cr.ChallengeSet;
 import com.novell.ldapchai.cr.ChallengeSet;
 import com.novell.ldapchai.cr.ResponseSet;
 import com.novell.ldapchai.cr.ResponseSet;
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.FormConfiguration;
 import password.pwm.config.FormConfiguration;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
 import java.util.*;
 import java.util.*;
@@ -122,16 +122,16 @@ public class ForgottenPasswordBean extends PwmSessionBean {
     public static class Progress implements Serializable {
     public static class Progress implements Serializable {
         private boolean tokenSent;
         private boolean tokenSent;
         private boolean allPassed;
         private boolean allPassed;
-        private final Set<RecoveryVerificationMethods> satisfiedMethods = new HashSet<>();
+        private final Set<IdentityVerificationMethod> satisfiedMethods = new HashSet<>();
 
 
         private MessageSendMethod tokenSendChoice;
         private MessageSendMethod tokenSendChoice;
         private String tokenSentAddress;
         private String tokenSentAddress;
-        private RecoveryVerificationMethods inProgressVerificationMethod;
+        private IdentityVerificationMethod inProgressVerificationMethod;
 
 
-        private transient RecoveryVerificationMethod naafRecoveryMethod;
-        private transient RecoveryVerificationMethod remoteRecoveryMethod;
+        private transient VerificationMethodSystem naafRecoveryMethod;
+        private transient VerificationMethodSystem remoteRecoveryMethod;
 
 
-        public Set<RecoveryVerificationMethods> getSatisfiedMethods() {
+        public Set<IdentityVerificationMethod> getSatisfiedMethods() {
             return satisfiedMethods;
             return satisfiedMethods;
         }
         }
 
 
@@ -175,35 +175,35 @@ public class ForgottenPasswordBean extends PwmSessionBean {
             this.tokenSentAddress = tokenSentAddress;
             this.tokenSentAddress = tokenSentAddress;
         }
         }
 
 
-        public RecoveryVerificationMethods getInProgressVerificationMethod() {
+        public IdentityVerificationMethod getInProgressVerificationMethod() {
             return inProgressVerificationMethod;
             return inProgressVerificationMethod;
         }
         }
 
 
-        public void setInProgressVerificationMethod(RecoveryVerificationMethods inProgressVerificationMethod) {
+        public void setInProgressVerificationMethod(IdentityVerificationMethod inProgressVerificationMethod) {
             this.inProgressVerificationMethod = inProgressVerificationMethod;
             this.inProgressVerificationMethod = inProgressVerificationMethod;
         }
         }
 
 
-        public void setNaafRecoveryMethod(RecoveryVerificationMethod naafRecoveryMethod) {
+        public void setNaafRecoveryMethod(VerificationMethodSystem naafRecoveryMethod) {
             this.naafRecoveryMethod = naafRecoveryMethod;
             this.naafRecoveryMethod = naafRecoveryMethod;
         }
         }
 
 
-        public RecoveryVerificationMethod getNaafRecoveryMethod() {
+        public VerificationMethodSystem getNaafRecoveryMethod() {
             return naafRecoveryMethod;
             return naafRecoveryMethod;
         }
         }
 
 
-        public RecoveryVerificationMethod getRemoteRecoveryMethod() {
+        public VerificationMethodSystem getRemoteRecoveryMethod() {
             return remoteRecoveryMethod;
             return remoteRecoveryMethod;
         }
         }
 
 
-        public void setRemoteRecoveryMethod(RecoveryVerificationMethod remoteRecoveryMethod) {
+        public void setRemoteRecoveryMethod(VerificationMethodSystem remoteRecoveryMethod) {
             this.remoteRecoveryMethod = remoteRecoveryMethod;
             this.remoteRecoveryMethod = remoteRecoveryMethod;
         }
         }
     }
     }
 
 
     public static class RecoveryFlags implements Serializable {
     public static class RecoveryFlags implements Serializable {
         private final boolean allowWhenLdapIntruderLocked;
         private final boolean allowWhenLdapIntruderLocked;
-        private final Set<RecoveryVerificationMethods> requiredAuthMethods;
-        private final Set<RecoveryVerificationMethods> optionalAuthMethods;
+        private final Set<IdentityVerificationMethod> requiredAuthMethods;
+        private final Set<IdentityVerificationMethod> optionalAuthMethods;
         private final int minimumOptionalAuthMethods;
         private final int minimumOptionalAuthMethods;
         private final MessageSendMethod tokenSendMethod;
         private final MessageSendMethod tokenSendMethod;
 
 
@@ -217,8 +217,8 @@ public class ForgottenPasswordBean extends PwmSessionBean {
         }
         }
 
 
         public RecoveryFlags(
         public RecoveryFlags(
-                final Set<RecoveryVerificationMethods> requiredAuthMethods,
-                final Set<RecoveryVerificationMethods> optionalAuthMethods,
+                final Set<IdentityVerificationMethod> requiredAuthMethods,
+                final Set<IdentityVerificationMethod> optionalAuthMethods,
                 final int minimumOptionalAuthMethods,
                 final int minimumOptionalAuthMethods,
                 final boolean allowWhenLdapIntruderLocked,
                 final boolean allowWhenLdapIntruderLocked,
                 final MessageSendMethod tokenSendMethod
                 final MessageSendMethod tokenSendMethod
@@ -231,7 +231,7 @@ public class ForgottenPasswordBean extends PwmSessionBean {
             this.tokenSendMethod = tokenSendMethod;
             this.tokenSendMethod = tokenSendMethod;
         }
         }
 
 
-        public Set<RecoveryVerificationMethods> getRequiredAuthMethods() {
+        public Set<IdentityVerificationMethod> getRequiredAuthMethods() {
             return requiredAuthMethods;
             return requiredAuthMethods;
         }
         }
 
 
@@ -244,7 +244,7 @@ public class ForgottenPasswordBean extends PwmSessionBean {
             return tokenSendMethod;
             return tokenSendMethod;
         }
         }
 
 
-        public Set<RecoveryVerificationMethods> getOptionalAuthMethods() {
+        public Set<IdentityVerificationMethod> getOptionalAuthMethods() {
             return optionalAuthMethods;
             return optionalAuthMethods;
         }
         }
 
 

+ 2 - 0
src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -467,6 +467,8 @@ public class DebugItemGenerator {
                 dataRow.add(record.getProfile() == null ? "" : record.getProfile());
                 dataRow.add(record.getProfile() == null ? "" : record.getProfile());
                 csvPrinter.printRecord(dataRow);
                 csvPrinter.printRecord(dataRow);
             }
             }
+            csvPrinter.flush();
+            outputStream.write(byteArrayOutputStream.toByteArray());
         }
         }
     }
     }
 
 

+ 43 - 43
src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -33,12 +33,12 @@ import com.novell.ldapchai.provider.ChaiProvider;
 import password.pwm.AppProperty;
 import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.*;
 import password.pwm.bean.*;
 import password.pwm.config.*;
 import password.pwm.config.*;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
 import password.pwm.config.option.RecoveryAction;
-import password.pwm.config.option.RecoveryVerificationMethods;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
 import password.pwm.config.profile.ProfileUtility;
@@ -295,13 +295,13 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
     {
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
         final String requestedChoiceStr = pwmRequest.readParameterAsString("choice");
         final String requestedChoiceStr = pwmRequest.readParameterAsString("choice");
-        final LinkedHashSet<RecoveryVerificationMethods> remainingAvailableOptionalMethods = new LinkedHashSet<>(figureRemainingAvailableOptionalAuthMethods(forgottenPasswordBean));
+        final LinkedHashSet<IdentityVerificationMethod> remainingAvailableOptionalMethods = new LinkedHashSet<>(figureRemainingAvailableOptionalAuthMethods(forgottenPasswordBean));
         pwmRequest.setAttribute(PwmRequest.Attribute.AvailableAuthMethods, remainingAvailableOptionalMethods);
         pwmRequest.setAttribute(PwmRequest.Attribute.AvailableAuthMethods, remainingAvailableOptionalMethods);
 
 
-        RecoveryVerificationMethods requestedChoice = null;
+        IdentityVerificationMethod requestedChoice = null;
         if (requestedChoiceStr != null && !requestedChoiceStr.isEmpty()) {
         if (requestedChoiceStr != null && !requestedChoiceStr.isEmpty()) {
             try {
             try {
-                requestedChoice = RecoveryVerificationMethods.valueOf(requestedChoiceStr);
+                requestedChoice = IdentityVerificationMethod.valueOf(requestedChoiceStr);
             } catch (IllegalArgumentException e) {
             } catch (IllegalArgumentException e) {
                 final String errorMsg = "unknown verification method requested";
                 final String errorMsg = "unknown verification method requested";
                 final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,errorMsg);
                 final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,errorMsg);
@@ -428,7 +428,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                             forgottenPasswordBean
                             forgottenPasswordBean
                     );
                     );
                 }
                 }
-                forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.TOKEN);
+                forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.TOKEN);
                 StatisticsManager.incrementStat(pwmRequest.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED);
                 StatisticsManager.incrementStat(pwmRequest.getPwmApplication(), Statistic.RECOVERY_TOKENS_PASSED);
             }
             }
         } catch (PwmOperationalException e) {
         } catch (PwmOperationalException e) {
@@ -436,7 +436,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
             errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT,errorMsg);
             errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT,errorMsg);
         }
         }
 
 
-        if (!forgottenPasswordBean.getProgress().getSatisfiedMethods().contains(RecoveryVerificationMethods.TOKEN)) {
+        if (!forgottenPasswordBean.getProgress().getSatisfiedMethods().contains(IdentityVerificationMethod.TOKEN)) {
             if (errorInformation == null) {
             if (errorInformation == null) {
                 errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT);
                 errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_INCORRECT);
             }
             }
@@ -449,7 +449,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
     {
     {
         final String PREFIX = "naaf-";
         final String PREFIX = "naaf-";
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
-        final RecoveryVerificationMethod naafMethod = forgottenPasswordBean.getProgress().getNaafRecoveryMethod();
+        final VerificationMethodSystem naafMethod = forgottenPasswordBean.getProgress().getNaafRecoveryMethod();
 
 
         final Map<String,String> naafResponses = new LinkedHashMap<>();
         final Map<String,String> naafResponses = new LinkedHashMap<>();
         {
         {
@@ -465,11 +465,11 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
         final ErrorInformation errorInformation = naafMethod.respondToPrompts(naafResponses);
         final ErrorInformation errorInformation = naafMethod.respondToPrompts(naafResponses);
 
 
-        if (naafMethod.getVerificationState() == RecoveryVerificationMethod.VerificationState.COMPLETE) {
-            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.NAAF);
+        if (naafMethod.getVerificationState() == VerificationMethodSystem.VerificationState.COMPLETE) {
+            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.NAAF);
         }
         }
 
 
-        if (naafMethod.getVerificationState() == RecoveryVerificationMethod.VerificationState.FAILED) {
+        if (naafMethod.getVerificationState() == VerificationMethodSystem.VerificationState.FAILED) {
             forgottenPasswordBean.getProgress().setNaafRecoveryMethod(null);
             forgottenPasswordBean.getProgress().setNaafRecoveryMethod(null);
             pwmRequest.respondWithError(errorInformation,true);
             pwmRequest.respondWithError(errorInformation,true);
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
@@ -488,7 +488,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
     {
     {
         final String PREFIX = "remote-";
         final String PREFIX = "remote-";
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean(pwmRequest);
-        final RecoveryVerificationMethod remoteRecoveryMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
+        final VerificationMethodSystem remoteRecoveryMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
 
 
         final Map<String,String> remoteResponses = new LinkedHashMap<>();
         final Map<String,String> remoteResponses = new LinkedHashMap<>();
         {
         {
@@ -504,11 +504,11 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
         final ErrorInformation errorInformation = remoteRecoveryMethod.respondToPrompts(remoteResponses);
         final ErrorInformation errorInformation = remoteRecoveryMethod.respondToPrompts(remoteResponses);
 
 
-        if (remoteRecoveryMethod.getVerificationState() == RecoveryVerificationMethod.VerificationState.COMPLETE) {
-            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.REMOTE_RESPONSES);
+        if (remoteRecoveryMethod.getVerificationState() == VerificationMethodSystem.VerificationState.COMPLETE) {
+            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.REMOTE_RESPONSES);
         }
         }
 
 
-        if (remoteRecoveryMethod.getVerificationState() == RecoveryVerificationMethod.VerificationState.FAILED) {
+        if (remoteRecoveryMethod.getVerificationState() == VerificationMethodSystem.VerificationState.FAILED) {
             forgottenPasswordBean.getProgress().setNaafRecoveryMethod(null);
             forgottenPasswordBean.getProgress().setNaafRecoveryMethod(null);
             pwmRequest.respondWithError(errorInformation,true);
             pwmRequest.respondWithError(errorInformation,true);
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, errorInformation);
@@ -546,7 +546,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                 if (otpPassed) {
                 if (otpPassed) {
                     StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_PASSED);
                     StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_PASSED);
                     LOGGER.debug(pwmRequest, "one time password validation has been passed");
                     LOGGER.debug(pwmRequest, "one time password validation has been passed");
-                    forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.OTP);
+                    forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.OTP);
                 } else {
                 } else {
                     StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_FAILED);
                     StatisticsManager.incrementStat(pwmRequest, Statistic.RECOVERY_OTP_FAILED);
                     handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_OTP_TOKEN));
                     handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_OTP_TOKEN));
@@ -610,7 +610,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
             return;
             return;
         }
         }
 
 
-        forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.CHALLENGE_RESPONSES);
+        forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.CHALLENGE_RESPONSES);
     }
     }
 
 
     private void processCheckAttributes(final PwmRequest pwmRequest)
     private void processCheckAttributes(final PwmRequest pwmRequest)
@@ -651,7 +651,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                 }
                 }
             }
             }
 
 
-            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(RecoveryVerificationMethods.ATTRIBUTES);
+            forgottenPasswordBean.getProgress().getSatisfiedMethods().add(IdentityVerificationMethod.ATTRIBUTES);
         } catch (PwmDataValidationException e) {
         } catch (PwmDataValidationException e) {
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE,e.getErrorInformation().toDebugStr()));
             handleUserVerificationBadAttempt(pwmRequest, forgottenPasswordBean, new ErrorInformation(PwmError.ERROR_INCORRECT_RESPONSE,e.getErrorInformation().toDebugStr()));
         }
         }
@@ -691,17 +691,17 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
 
 
         // check for previous authentication
         // check for previous authentication
-        if (recoveryFlags.getRequiredAuthMethods().contains(RecoveryVerificationMethods.PREVIOUS_AUTH) || recoveryFlags.getOptionalAuthMethods().contains(RecoveryVerificationMethods.PREVIOUS_AUTH)) {
-            if (!progress.getSatisfiedMethods().contains(RecoveryVerificationMethods.PREVIOUS_AUTH)) {
+        if (recoveryFlags.getRequiredAuthMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH) || recoveryFlags.getOptionalAuthMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH)) {
+            if (!progress.getSatisfiedMethods().contains(IdentityVerificationMethod.PREVIOUS_AUTH)) {
                 if (checkAuthRecord(pwmRequest, forgottenPasswordBean.getUserInfo().getUserGuid())) {
                 if (checkAuthRecord(pwmRequest, forgottenPasswordBean.getUserInfo().getUserGuid())) {
-                    LOGGER.debug(pwmRequest, "marking " + RecoveryVerificationMethods.PREVIOUS_AUTH + " method as satisfied");
-                    progress.getSatisfiedMethods().add(RecoveryVerificationMethods.PREVIOUS_AUTH);
+                    LOGGER.debug(pwmRequest, "marking " + IdentityVerificationMethod.PREVIOUS_AUTH + " method as satisfied");
+                    progress.getSatisfiedMethods().add(IdentityVerificationMethod.PREVIOUS_AUTH);
                 }
                 }
             }
             }
         }
         }
 
 
         // dispatch required auth methods.
         // dispatch required auth methods.
-        for (final RecoveryVerificationMethods method : recoveryFlags.getRequiredAuthMethods()) {
+        for (final IdentityVerificationMethod method : recoveryFlags.getRequiredAuthMethods()) {
             if (!progress.getSatisfiedMethods().contains(method)) {
             if (!progress.getSatisfiedMethods().contains(method)) {
                 forwardUserBasedOnRecoveryMethod(pwmRequest, method);
                 forwardUserBasedOnRecoveryMethod(pwmRequest, method);
                 return;
                 return;
@@ -721,9 +721,9 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
         // check if more optional methods required
         // check if more optional methods required
         if (recoveryFlags.getMinimumOptionalAuthMethods() > 0) {
         if (recoveryFlags.getMinimumOptionalAuthMethods() > 0) {
-            final Set<RecoveryVerificationMethods> satisfiedOptionalMethods = figureSatisfiedOptionalAuthMethods(recoveryFlags,progress);
+            final Set<IdentityVerificationMethod> satisfiedOptionalMethods = figureSatisfiedOptionalAuthMethods(recoveryFlags,progress);
             if (satisfiedOptionalMethods.size() < recoveryFlags.getMinimumOptionalAuthMethods()) {
             if (satisfiedOptionalMethods.size() < recoveryFlags.getMinimumOptionalAuthMethods()) {
-                final Set<RecoveryVerificationMethods> remainingAvailableOptionalMethods = figureRemainingAvailableOptionalAuthMethods(forgottenPasswordBean);
+                final Set<IdentityVerificationMethod> remainingAvailableOptionalMethods = figureRemainingAvailableOptionalAuthMethods(forgottenPasswordBean);
                 if (remainingAvailableOptionalMethods.isEmpty()) {
                 if (remainingAvailableOptionalMethods.isEmpty()) {
                     final String errorMsg = "additional optional verification methods are needed, however all available optional verification methods have been satisified by user";
                     final String errorMsg = "additional optional verification methods are needed, however all available optional verification methods have been satisified by user";
                     final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
                     final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
@@ -731,7 +731,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                     throw new PwmUnrecoverableException(errorInformation);
                     throw new PwmUnrecoverableException(errorInformation);
                 } else {
                 } else {
                     if (remainingAvailableOptionalMethods.size() == 1) {
                     if (remainingAvailableOptionalMethods.size() == 1) {
-                        final RecoveryVerificationMethods remainingMethod = remainingAvailableOptionalMethods.iterator().next();
+                        final IdentityVerificationMethod remainingMethod = remainingAvailableOptionalMethods.iterator().next();
                         LOGGER.debug(pwmRequest, "only 1 remaining available optional verification method, will redirect to " + remainingMethod.toString());
                         LOGGER.debug(pwmRequest, "only 1 remaining available optional verification method, will redirect to " + remainingMethod.toString());
                         forwardUserBasedOnRecoveryMethod(pwmRequest, remainingMethod);
                         forwardUserBasedOnRecoveryMethod(pwmRequest, remainingMethod);
                         progress.setInProgressVerificationMethod(remainingMethod);
                         progress.setInProgressVerificationMethod(remainingMethod);
@@ -1134,7 +1134,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
     private static void verifyRequirementsForAuthMethod(
     private static void verifyRequirementsForAuthMethod(
             final ForgottenPasswordBean forgottenPasswordBean,
             final ForgottenPasswordBean forgottenPasswordBean,
-            final RecoveryVerificationMethods recoveryVerificationMethods) throws PwmUnrecoverableException
+            final IdentityVerificationMethod recoveryVerificationMethods) throws PwmUnrecoverableException
     {
     {
         switch (recoveryVerificationMethods) {
         switch (recoveryVerificationMethods) {
             case TOKEN: {
             case TOKEN: {
@@ -1228,8 +1228,8 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
         final ResponseSet responseSet;
         final ResponseSet responseSet;
         final ChallengeSet challengeSet;
         final ChallengeSet challengeSet;
-        if (recoveryFlags.getRequiredAuthMethods().contains(RecoveryVerificationMethods.CHALLENGE_RESPONSES)
-                || recoveryFlags.getOptionalAuthMethods().contains(RecoveryVerificationMethods.CHALLENGE_RESPONSES)) {
+        if (recoveryFlags.getRequiredAuthMethods().contains(IdentityVerificationMethod.CHALLENGE_RESPONSES)
+                || recoveryFlags.getOptionalAuthMethods().contains(IdentityVerificationMethod.CHALLENGE_RESPONSES)) {
             try {
             try {
                 final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userInfoBean.getUserIdentity());
                 final ChaiUser theUser = pwmApplication.getProxiedChaiUser(userInfoBean.getUserIdentity());
                 responseSet = pwmApplication.getCrService().readUserResponseSet(
                 responseSet = pwmApplication.getCrService().readUserResponseSet(
@@ -1283,7 +1283,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
         forgottenPasswordBean.setRecoveryFlags(recoveryFlags);
         forgottenPasswordBean.setRecoveryFlags(recoveryFlags);
         forgottenPasswordBean.setProgress(new ForgottenPasswordBean.Progress());
         forgottenPasswordBean.setProgress(new ForgottenPasswordBean.Progress());
 
 
-        for (final RecoveryVerificationMethods recoveryVerificationMethods : recoveryFlags.getRequiredAuthMethods()) {
+        for (final IdentityVerificationMethod recoveryVerificationMethods : recoveryFlags.getRequiredAuthMethods()) {
             verifyRequirementsForAuthMethod(forgottenPasswordBean, recoveryVerificationMethods);
             verifyRequirementsForAuthMethod(forgottenPasswordBean, recoveryVerificationMethods);
         }
         }
     }
     }
@@ -1297,8 +1297,8 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
         final MessageSendMethod tokenSendMethod = config.getForgottenPasswordProfiles().get(forgottenPasswordProfileID).readSettingAsEnum(PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class);
         final MessageSendMethod tokenSendMethod = config.getForgottenPasswordProfiles().get(forgottenPasswordProfileID).readSettingAsEnum(PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class);
 
 
-        final Set<RecoveryVerificationMethods> requiredRecoveryVerificationMethods = forgottenPasswordProfile.requiredRecoveryAuthenticationMethods();
-        final Set<RecoveryVerificationMethods> optionalRecoveryVerificationMethods = forgottenPasswordProfile.optionalRecoveryAuthenticationMethods();
+        final Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods = forgottenPasswordProfile.requiredRecoveryAuthenticationMethods();
+        final Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods = forgottenPasswordProfile.optionalRecoveryAuthenticationMethods();
         final int minimumOptionalRecoveryAuthMethods = forgottenPasswordProfile.getMinOptionalRequired();
         final int minimumOptionalRecoveryAuthMethods = forgottenPasswordProfile.getMinOptionalRequired();
         final boolean allowWhenLdapIntruderLocked = forgottenPasswordProfile.readSettingAsBoolean(PwmSetting.RECOVERY_ALLOW_WHEN_LOCKED);
         final boolean allowWhenLdapIntruderLocked = forgottenPasswordProfile.readSettingAsBoolean(PwmSetting.RECOVERY_ALLOW_WHEN_LOCKED);
 
 
@@ -1404,27 +1404,27 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
         throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT));
         throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT));
     }
     }
 
 
-    private static Set<RecoveryVerificationMethods> figureSatisfiedOptionalAuthMethods(
+    private static Set<IdentityVerificationMethod> figureSatisfiedOptionalAuthMethods(
             ForgottenPasswordBean.RecoveryFlags recoveryFlags,
             ForgottenPasswordBean.RecoveryFlags recoveryFlags,
             ForgottenPasswordBean.Progress progress)
             ForgottenPasswordBean.Progress progress)
     {
     {
-        final Set<RecoveryVerificationMethods> result = new HashSet<>();
+        final Set<IdentityVerificationMethod> result = new HashSet<>();
         result.addAll(recoveryFlags.getOptionalAuthMethods());
         result.addAll(recoveryFlags.getOptionalAuthMethods());
         result.retainAll(progress.getSatisfiedMethods());
         result.retainAll(progress.getSatisfiedMethods());
         return Collections.unmodifiableSet(result);
         return Collections.unmodifiableSet(result);
     }
     }
 
 
-    private static Set<RecoveryVerificationMethods> figureRemainingAvailableOptionalAuthMethods(
+    private static Set<IdentityVerificationMethod> figureRemainingAvailableOptionalAuthMethods(
             final ForgottenPasswordBean forgottenPasswordBean
             final ForgottenPasswordBean forgottenPasswordBean
     )
     )
     {
     {
         ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
         ForgottenPasswordBean.RecoveryFlags recoveryFlags = forgottenPasswordBean.getRecoveryFlags();
         ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
         ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
-        final Set<RecoveryVerificationMethods> result = new HashSet<>();
+        final Set<IdentityVerificationMethod> result = new HashSet<>();
         result.addAll(recoveryFlags.getOptionalAuthMethods());
         result.addAll(recoveryFlags.getOptionalAuthMethods());
         result.removeAll(progress.getSatisfiedMethods());
         result.removeAll(progress.getSatisfiedMethods());
 
 
-        for (final RecoveryVerificationMethods recoveryVerificationMethods : new HashSet<>(result)) {
+        for (final IdentityVerificationMethod recoveryVerificationMethods : new HashSet<>(result)) {
             try {
             try {
                 verifyRequirementsForAuthMethod(forgottenPasswordBean, recoveryVerificationMethods);
                 verifyRequirementsForAuthMethod(forgottenPasswordBean, recoveryVerificationMethods);
             } catch (PwmUnrecoverableException e) {
             } catch (PwmUnrecoverableException e) {
@@ -1443,7 +1443,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
     private void forwardUserBasedOnRecoveryMethod(
     private void forwardUserBasedOnRecoveryMethod(
             final PwmRequest pwmRequest,
             final PwmRequest pwmRequest,
-            final RecoveryVerificationMethods method)
+            final IdentityVerificationMethod method)
             throws ServletException, PwmUnrecoverableException, IOException
             throws ServletException, PwmUnrecoverableException, IOException
     {
     {
         LOGGER.debug(pwmRequest,"attempting to forward request to handle verification method " + method.toString());
         LOGGER.debug(pwmRequest,"attempting to forward request to handle verification method " + method.toString());
@@ -1487,7 +1487,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                     progress.setTokenSent(true);
                     progress.setTokenSent(true);
                 }
                 }
 
 
-                if (!progress.getSatisfiedMethods().contains(RecoveryVerificationMethods.TOKEN)) {
+                if (!progress.getSatisfiedMethods().contains(IdentityVerificationMethod.TOKEN)) {
                     pwmRequest.forwardToJsp(PwmConstants.JSP_URL.RECOVER_PASSWORD_ENTER_TOKEN);
                     pwmRequest.forwardToJsp(PwmConstants.JSP_URL.RECOVER_PASSWORD_ENTER_TOKEN);
                     return;
                     return;
                 }
                 }
@@ -1495,7 +1495,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
             break;
             break;
 
 
             case REMOTE_RESPONSES: {
             case REMOTE_RESPONSES: {
-                final RecoveryVerificationMethod remoteMethod;
+                final VerificationMethodSystem remoteMethod;
                 if (forgottenPasswordBean.getProgress().getRemoteRecoveryMethod() == null) {
                 if (forgottenPasswordBean.getProgress().getRemoteRecoveryMethod() == null) {
                     remoteMethod = new RemoteVerificationMethod();
                     remoteMethod = new RemoteVerificationMethod();
                     remoteMethod.init(
                     remoteMethod.init(
@@ -1509,7 +1509,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                     remoteMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
                     remoteMethod = forgottenPasswordBean.getProgress().getRemoteRecoveryMethod();
                 }
                 }
 
 
-                final List<RecoveryVerificationMethod.UserPrompt> prompts = remoteMethod.getCurrentPrompts();
+                final List<VerificationMethodSystem.UserPrompt> prompts = remoteMethod.getCurrentPrompts();
                 final String displayInstructions = remoteMethod.getCurrentDisplayInstructions();
                 final String displayInstructions = remoteMethod.getCurrentDisplayInstructions();
 
 
                 pwmRequest.setAttribute(PwmRequest.Attribute.ForgottenPasswordPrompts, new ArrayList<>(prompts));
                 pwmRequest.setAttribute(PwmRequest.Attribute.ForgottenPasswordPrompts, new ArrayList<>(prompts));
@@ -1520,7 +1520,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
 
 
 
             case NAAF: {
             case NAAF: {
-                final RecoveryVerificationMethod naafMethod;
+                final VerificationMethodSystem naafMethod;
                 if (forgottenPasswordBean.getProgress().getNaafRecoveryMethod() == null) {
                 if (forgottenPasswordBean.getProgress().getNaafRecoveryMethod() == null) {
                     naafMethod = new PwmNAAFVerificationMethod();
                     naafMethod = new PwmNAAFVerificationMethod();
                     naafMethod.init(
                     naafMethod.init(
@@ -1534,7 +1534,7 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                     naafMethod = forgottenPasswordBean.getProgress().getNaafRecoveryMethod();
                     naafMethod = forgottenPasswordBean.getProgress().getNaafRecoveryMethod();
                 }
                 }
 
 
-                final List<RecoveryVerificationMethod.UserPrompt> prompts = naafMethod.getCurrentPrompts();
+                final List<VerificationMethodSystem.UserPrompt> prompts = naafMethod.getCurrentPrompts();
                 final String displayInstructions = naafMethod.getCurrentDisplayInstructions();
                 final String displayInstructions = naafMethod.getCurrentDisplayInstructions();
 
 
                 pwmRequest.setAttribute(PwmRequest.Attribute.ForgottenPasswordPrompts, new ArrayList<>(prompts));
                 pwmRequest.setAttribute(PwmRequest.Attribute.ForgottenPasswordPrompts, new ArrayList<>(prompts));

+ 2 - 2
src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java

@@ -24,7 +24,7 @@ package password.pwm.http.servlet.forgottenpw;
 
 
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.*;
 import password.pwm.bean.*;
 import password.pwm.bean.pub.PublicUserInfoBean;
 import password.pwm.bean.pub.PublicUserInfoBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
@@ -43,7 +43,7 @@ import password.pwm.util.secure.PwmRandom;
 
 
 import java.util.*;
 import java.util.*;
 
 
-public class RemoteVerificationMethod implements RecoveryVerificationMethod {
+public class RemoteVerificationMethod implements VerificationMethodSystem {
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(RemoteVerificationMethod.class);
     private static final PwmLogger LOGGER = PwmLogger.forClass(RemoteVerificationMethod.class);
 
 

+ 11 - 0
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskClientDataBean.java

@@ -25,8 +25,10 @@ package password.pwm.http.servlet.helpdesk;
 import password.pwm.config.option.HelpdeskClearResponseMode;
 import password.pwm.config.option.HelpdeskClearResponseMode;
 import password.pwm.config.option.HelpdeskUIMode;
 import password.pwm.config.option.HelpdeskUIMode;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.option.IdentityVerificationMethod;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
@@ -37,6 +39,7 @@ public class HelpdeskClientDataBean implements Serializable {
     private HelpdeskUIMode helpdesk_setting_PwUiMode;
     private HelpdeskUIMode helpdesk_setting_PwUiMode;
     private MessageSendMethod helpdesk_setting_tokenSendMethod;
     private MessageSendMethod helpdesk_setting_tokenSendMethod;
     private Map<String, ActionInformation> actions = new HashMap<>();
     private Map<String, ActionInformation> actions = new HashMap<>();
+    private Map<String, Collection<IdentityVerificationMethod>> verificationMethods = new HashMap<>();
 
 
     public Map<String, String> getHelpdesk_search_columns() {
     public Map<String, String> getHelpdesk_search_columns() {
         return helpdesk_search_columns;
         return helpdesk_search_columns;
@@ -86,6 +89,14 @@ public class HelpdeskClientDataBean implements Serializable {
         this.actions = actions;
         this.actions = actions;
     }
     }
 
 
+    public Map<String, Collection<IdentityVerificationMethod>> getVerificationMethods() {
+        return verificationMethods;
+    }
+
+    public void setVerificationMethods(Map<String, Collection<IdentityVerificationMethod>> verificationMethods) {
+        this.verificationMethods = verificationMethods;
+    }
+
     public static class ActionInformation implements Serializable {
     public static class ActionInformation implements Serializable {
         private String name;
         private String name;
         private String description;
         private String description;

+ 141 - 17
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -32,13 +32,13 @@ import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.EmailItemBean;
-import password.pwm.bean.LocalSessionStateBean;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.*;
 import password.pwm.config.*;
 import password.pwm.config.option.HelpdeskClearResponseMode;
 import password.pwm.config.option.HelpdeskClearResponseMode;
 import password.pwm.config.option.HelpdeskUIMode;
 import password.pwm.config.option.HelpdeskUIMode;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.config.profile.HelpdeskProfile;
 import password.pwm.error.*;
 import password.pwm.error.*;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.HttpMethod;
@@ -102,7 +102,8 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         sendVerificationToken(HttpMethod.POST),
         sendVerificationToken(HttpMethod.POST),
         verifyVerificationToken(HttpMethod.POST),
         verifyVerificationToken(HttpMethod.POST),
         clientData(HttpMethod.GET),
         clientData(HttpMethod.GET),
-
+        checkVerification(HttpMethod.POST),
+        showVerifications(HttpMethod.POST),
         ;
         ;
 
 
         private final HttpMethod method;
         private final HttpMethod method;
@@ -133,7 +134,6 @@ public class HelpdeskServlet extends AbstractPwmServlet {
     {
     {
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmSession pwmSession = pwmRequest.getPwmSession();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
 
 
         if (!pwmRequest.isAuthenticated()) {
         if (!pwmRequest.isAuthenticated()) {
             pwmRequest.respondWithError(PwmError.ERROR_AUTHENTICATION_REQUIRED.toInfo());
             pwmRequest.respondWithError(PwmError.ERROR_AUTHENTICATION_REQUIRED.toInfo());
@@ -200,9 +200,18 @@ public class HelpdeskServlet extends AbstractPwmServlet {
                 case clientData:
                 case clientData:
                     restClientData(pwmRequest, helpdeskProfile);
                     restClientData(pwmRequest, helpdeskProfile);
                     return;
                     return;
+
+                case checkVerification:
+                    restCheckVerification(pwmRequest, helpdeskProfile);
+                    return;
+
+                case showVerifications:
+                    restShowVerifications(pwmRequest);
+                    return;
             }
             }
         }
         }
 
 
+        pwmRequest.setAttribute(PwmRequest.Attribute.HelpdeskVerificationEnabled, !helpdeskProfile.readRequiredVerificationMethods().isEmpty());
         pwmRequest.forwardToJsp(PwmConstants.JSP_URL.HELPDESK_SEARCH);
         pwmRequest.forwardToJsp(PwmConstants.JSP_URL.HELPDESK_SEARCH);
     }
     }
 
 
@@ -235,6 +244,12 @@ public class HelpdeskServlet extends AbstractPwmServlet {
 
 
             returnValues.setActions(actions);
             returnValues.setActions(actions);
         }
         }
+        {
+            final Map<String,Collection<IdentityVerificationMethod>> verificationMethodsMap = new HashMap<>();
+            verificationMethodsMap.put("optional", helpdeskProfile.readOptionalVerificationMethods());
+            verificationMethodsMap.put("required", helpdeskProfile.readRequiredVerificationMethods());
+            returnValues.setVerificationMethods(verificationMethodsMap);
+        }
         final RestResultBean restResultBean = new RestResultBean(returnValues);
         final RestResultBean restResultBean = new RestResultBean(returnValues);
         LOGGER.trace(pwmRequest, "returning clientData: " + JsonUtil.serialize(restResultBean));
         LOGGER.trace(pwmRequest, "returning clientData: " + JsonUtil.serialize(restResultBean));
         pwmRequest.outputJsonResult(restResultBean);
         pwmRequest.outputJsonResult(restResultBean);
@@ -418,6 +433,19 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         }
         }
         LOGGER.trace(pwmRequest, "helpdesk detail view request for user details of " + userIdentity.toString() + " by actor " + actorUserIdentity.toString());
         LOGGER.trace(pwmRequest, "helpdesk detail view request for user details of " + userIdentity.toString() + " by actor " + actorUserIdentity.toString());
 
 
+        final HelpdeskVerificationStateBean verificationStateBean = HelpdeskVerificationStateBean.fromClientString(
+                pwmRequest,
+                pwmRequest.readParameterAsString(HelpdeskVerificationStateBean.PARAMETER_VERIFICATION_STATE_KEY, PwmHttpRequestWrapper.Flag.BypassValidation)
+        );
+
+        if (!checkIfRequiredVerificationPassed(userIdentity, verificationStateBean, helpdeskProfile)) {
+            final String errorMsg = "selected user has not been verified";
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNAUTHORIZED,errorMsg);
+            LOGGER.debug(pwmRequest, errorInformation);
+            pwmRequest.respondWithError(errorInformation, false);
+            return;
+        }
+
         final HelpdeskDetailInfoBean helpdeskDetailInfoBean = makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
         final HelpdeskDetailInfoBean helpdeskDetailInfoBean = makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
         pwmRequest.setAttribute(PwmRequest.Attribute.HelpdeskDetail, helpdeskDetailInfoBean);
         pwmRequest.setAttribute(PwmRequest.Attribute.HelpdeskDetail, helpdeskDetailInfoBean);
 
 
@@ -428,6 +456,7 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         }
         }
 
 
         StatisticsManager.incrementStat(pwmRequest, Statistic.HELPDESK_USER_LOOKUP);
         StatisticsManager.incrementStat(pwmRequest, Statistic.HELPDESK_USER_LOOKUP);
+        pwmRequest.setAttribute(PwmRequest.Attribute.HelpdeskVerificationEnabled, !helpdeskProfile.readOptionalVerificationMethods().isEmpty());
         pwmRequest.forwardToJsp(PwmConstants.JSP_URL.HELPDESK_DETAIL);
         pwmRequest.forwardToJsp(PwmConstants.JSP_URL.HELPDESK_DETAIL);
     }
     }
 
 
@@ -485,7 +514,7 @@ public class HelpdeskServlet extends AbstractPwmServlet {
 
 
         final RestResultBean restResultBean = new RestResultBean();
         final RestResultBean restResultBean = new RestResultBean();
         final HelpdeskSearchResultsBean outputData = new HelpdeskSearchResultsBean();
         final HelpdeskSearchResultsBean outputData = new HelpdeskSearchResultsBean();
-        outputData.setSearchResults(results.resultsAsJsonOutput(pwmRequest.getPwmApplication()));
+        outputData.setSearchResults(results.resultsAsJsonOutput(pwmRequest.getPwmApplication(),pwmRequest.getUserInfoIfLoggedIn()));
         outputData.setSizeExceeded(sizeExceeded);
         outputData.setSizeExceeded(sizeExceeded);
         restResultBean.setData(outputData);
         restResultBean.setData(outputData);
         pwmRequest.outputJsonResult(restResultBean);
         pwmRequest.outputJsonResult(restResultBean);
@@ -650,11 +679,13 @@ public class HelpdeskServlet extends AbstractPwmServlet {
     )
     )
             throws IOException, PwmUnrecoverableException, ServletException, ChaiUnavailableException
             throws IOException, PwmUnrecoverableException, ServletException, ChaiUnavailableException
     {
     {
-        final long DELAY_MS = 1000;
         final Date startTime = new Date();
         final Date startTime = new Date();
 
 
-        final Map<String,String> inputRecord = pwmRequest.readBodyAsJsonStringMap();
-        final String userKey = inputRecord.get("userKey");
+        final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean = JsonUtil.deserialize(
+                pwmRequest.readRequestBodyAsString(),
+                HelpdeskVerificationRequestBean.class
+        );
+        final String userKey = helpdeskVerificationRequestBean.getUserKey();
         if (userKey == null || userKey.isEmpty()) {
         if (userKey == null || userKey.isEmpty()) {
             final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,"userKey parameter is missing");
             final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,"userKey parameter is missing");
             pwmRequest.respondWithError(errorInformation, false);
             pwmRequest.respondWithError(errorInformation, false);
@@ -669,7 +700,7 @@ public class HelpdeskServlet extends AbstractPwmServlet {
             return;
             return;
         }
         }
 
 
-        final String code = inputRecord.get("code");
+        final String code = helpdeskVerificationRequestBean.getCode();
         final OTPUserRecord otpUserRecord = pwmRequest.getPwmApplication().getOtpService().readOTPUserConfiguration(pwmRequest.getSessionLabel(), userIdentity);
         final OTPUserRecord otpUserRecord = pwmRequest.getPwmApplication().getOtpService().readOTPUserConfiguration(pwmRequest.getSessionLabel(), userIdentity);
         try {
         try {
             final boolean passed = pwmRequest.getPwmApplication().getOtpService().validateToken(
             final boolean passed = pwmRequest.getPwmApplication().getOtpService().validateToken(
@@ -679,9 +710,10 @@ public class HelpdeskServlet extends AbstractPwmServlet {
                     code,
                     code,
                     false
                     false
             );
             );
+
+            final HelpdeskVerificationStateBean verificationStateBean = HelpdeskVerificationStateBean.fromClientString(pwmRequest, helpdeskVerificationRequestBean.getVerificationState());
+
             if (passed) {
             if (passed) {
-                // mark the event log
-                {
                     final PwmSession pwmSession = pwmRequest.getPwmSession();
                     final PwmSession pwmSession = pwmRequest.getPwmSession();
                     final HelpdeskAuditRecord auditRecord = pwmRequest.getPwmApplication().getAuditManager().createHelpdeskAuditRecord(
                     final HelpdeskAuditRecord auditRecord = pwmRequest.getPwmApplication().getAuditManager().createHelpdeskAuditRecord(
                             AuditEvent.HELPDESK_VERIFY_OTP,
                             AuditEvent.HELPDESK_VERIFY_OTP,
@@ -692,18 +724,30 @@ public class HelpdeskServlet extends AbstractPwmServlet {
                             pwmSession.getSessionStateBean().getSrcHostname()
                             pwmSession.getSessionStateBean().getSrcHostname()
                     );
                     );
                     pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
                     pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
-                }
 
 
                 StatisticsManager.incrementStat(pwmRequest, Statistic.HELPDESK_VERIFY_OTP);
                 StatisticsManager.incrementStat(pwmRequest, Statistic.HELPDESK_VERIFY_OTP);
+                verificationStateBean.addRecord(userIdentity, IdentityVerificationMethod.OTP);
+            } else {
+                    final PwmSession pwmSession = pwmRequest.getPwmSession();
+                    final HelpdeskAuditRecord auditRecord = pwmRequest.getPwmApplication().getAuditManager().createHelpdeskAuditRecord(
+                            AuditEvent.HELPDESK_VERIFY_OTP_INCORRECT,
+                            pwmSession.getUserInfoBean().getUserIdentity(),
+                            null,
+                            userIdentity,
+                            pwmSession.getSessionStateBean().getSrcAddress(),
+                            pwmSession.getSessionStateBean().getSrcHostname()
+                    );
+                    pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
             }
             }
 
 
             // add a delay to prevent continuous checks
             // add a delay to prevent continuous checks
-            while (TimeDuration.fromCurrent(startTime).isShorterThan(DELAY_MS)) {
+            final long delayMs = Long.parseLong(pwmRequest.getConfig().readAppProperty(AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS));
+            while (TimeDuration.fromCurrent(startTime).isShorterThan(delayMs)) {
                 Helper.pause(100);
                 Helper.pause(100);
             }
             }
 
 
-            final RestResultBean restResultBean = new RestResultBean();
-            restResultBean.setData(passed);
+            final HelpdeskVerificationResponseBean responseBean = new HelpdeskVerificationResponseBean(passed, verificationStateBean.toClientString(pwmRequest.getPwmApplication()));
+            final RestResultBean restResultBean = new RestResultBean(responseBean);
             pwmRequest.outputJsonResult(restResultBean);
             pwmRequest.outputJsonResult(restResultBean);
         } catch (PwmOperationalException e) {
         } catch (PwmOperationalException e) {
             pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
             pwmRequest.outputJsonResult(RestResultBean.fromError(e.getErrorInformation(), pwmRequest));
@@ -733,7 +777,7 @@ public class HelpdeskServlet extends AbstractPwmServlet {
                 }
                 }
             }
             }
             if (tokenSendMethod == MessageSendMethod.CHOICE_SMS_EMAIL) {
             if (tokenSendMethod == MessageSendMethod.CHOICE_SMS_EMAIL) {
-                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT, "unable to determine appropriate send method, missing method parameter indicaton from operator");
+                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_TOKEN_MISSING_CONTACT, "unable to determine appropriate send method, missing method parameter indication from operator");
                 LOGGER.error(pwmRequest,errorInformation);
                 LOGGER.error(pwmRequest,errorInformation);
                 pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation,pwmRequest));
                 pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation,pwmRequest));
                 return;
                 return;
@@ -743,6 +787,12 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         final UserIdentity userIdentity = UserIdentity.fromKey(bodyParams.get("userKey"), pwmRequest.getPwmApplication());
         final UserIdentity userIdentity = UserIdentity.fromKey(bodyParams.get("userKey"), pwmRequest.getPwmApplication());
 
 
         final HelpdeskDetailInfoBean helpdeskDetailInfoBean = makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
         final HelpdeskDetailInfoBean helpdeskDetailInfoBean = makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
+        if (helpdeskDetailInfoBean == null) {
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER, "unable to read helpdesk detail data for specified user");
+            LOGGER.error(pwmRequest,errorInformation);
+            pwmRequest.outputJsonResult(RestResultBean.fromError(errorInformation,pwmRequest));
+            return;
+        }
         final UserInfoBean userInfoBean = helpdeskDetailInfoBean.getUserInfoBean();
         final UserInfoBean userInfoBean = helpdeskDetailInfoBean.getUserInfoBean();
         final UserDataReader userDataReader = LdapUserDataReader.appProxiedReader(pwmRequest.getPwmApplication(), userIdentity);
         final UserDataReader userDataReader = LdapUserDataReader.appProxiedReader(pwmRequest.getPwmApplication(), userIdentity);
         final MacroMachine macroMachine = new MacroMachine(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfoBean, null, userDataReader);
         final MacroMachine macroMachine = new MacroMachine(pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfoBean, null, userDataReader);
@@ -816,7 +866,8 @@ public class HelpdeskServlet extends AbstractPwmServlet {
     )
     )
             throws IOException, PwmUnrecoverableException, ServletException
             throws IOException, PwmUnrecoverableException, ServletException
     {
     {
-        final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean =JsonUtil.deserialize(
+        final Date startTime = new Date();
+        final HelpdeskVerificationRequestBean helpdeskVerificationRequestBean = JsonUtil.deserialize(
                 pwmRequest.readRequestBodyAsString(),
                 pwmRequest.readRequestBodyAsString(),
                 HelpdeskVerificationRequestBean.class
                 HelpdeskVerificationRequestBean.class
         );
         );
@@ -843,7 +894,8 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         }
         }
 
 
         final boolean passed = tokenData.getToken().equals(token);
         final boolean passed = tokenData.getToken().equals(token);
-        final RestResultBean restResultBean = new RestResultBean(passed);
+
+        final HelpdeskVerificationStateBean verificationStateBean = HelpdeskVerificationStateBean.fromClientString(pwmRequest, helpdeskVerificationRequestBean.getVerificationState());
 
 
         if (passed) {
         if (passed) {
             final PwmSession pwmSession = pwmRequest.getPwmSession();
             final PwmSession pwmSession = pwmRequest.getPwmSession();
@@ -856,8 +908,28 @@ public class HelpdeskServlet extends AbstractPwmServlet {
                     pwmSession.getSessionStateBean().getSrcHostname()
                     pwmSession.getSessionStateBean().getSrcHostname()
             );
             );
             pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
             pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
+            verificationStateBean.addRecord(userIdentity, IdentityVerificationMethod.TOKEN);
+        } else {
+            final PwmSession pwmSession = pwmRequest.getPwmSession();
+            final HelpdeskAuditRecord auditRecord = pwmRequest.getPwmApplication().getAuditManager().createHelpdeskAuditRecord(
+                    AuditEvent.HELPDESK_VERIFY_TOKEN_INCORRECT,
+                    pwmSession.getUserInfoBean().getUserIdentity(),
+                    null,
+                    userIdentity,
+                    pwmSession.getSessionStateBean().getSrcAddress(),
+                    pwmSession.getSessionStateBean().getSrcHostname()
+            );
+            pwmRequest.getPwmApplication().getAuditManager().submit(auditRecord);
         }
         }
 
 
+        // add a delay to prevent continuous checks
+        final long delayMs = Long.parseLong(pwmRequest.getConfig().readAppProperty(AppProperty.HELPDESK_VERIFICATION_INVALID_DELAY_MS));
+        while (TimeDuration.fromCurrent(startTime).isShorterThan(delayMs)) {
+            Helper.pause(100);
+        }
+
+        final HelpdeskVerificationResponseBean responseBean = new HelpdeskVerificationResponseBean(passed, verificationStateBean.toClientString(pwmRequest.getPwmApplication()));
+        final RestResultBean restResultBean = new RestResultBean(responseBean);
         pwmRequest.outputJsonResult(restResultBean);
         pwmRequest.outputJsonResult(restResultBean);
     }
     }
 
 
@@ -972,4 +1044,56 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         }
         }
     }
     }
 
 
+    private void restCheckVerification(final PwmRequest pwmRequest, final HelpdeskProfile helpdeskProfile)
+            throws IOException, PwmUnrecoverableException, ServletException {
+        final Map<String,String> bodyMap = pwmRequest.readBodyAsJsonStringMap(PwmHttpRequestWrapper.Flag.BypassValidation);
+
+        final String userKey = bodyMap.get("userKey");
+        if (userKey == null || userKey.length() < 1) {
+            final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_MISSING_PARAMETER,"userKey parameter is missing");
+            pwmRequest.setResponseError(errorInformation);
+            pwmRequest.respondWithError(errorInformation, false);
+            return;
+        }
+
+        final UserIdentity userIdentity = UserIdentity.fromObfuscatedKey(userKey, pwmRequest.getPwmApplication());
+        checkIfUserIdentityViewable(pwmRequest, helpdeskProfile, userIdentity);
+
+        final String rawVerificationStr = bodyMap.get(HelpdeskVerificationStateBean.PARAMETER_VERIFICATION_STATE_KEY);
+        final HelpdeskVerificationStateBean state = HelpdeskVerificationStateBean.fromClientString(pwmRequest, rawVerificationStr);
+        final boolean passed = checkIfRequiredVerificationPassed(userIdentity, state, helpdeskProfile);
+        final HashMap<String,Object> results = new HashMap<>();
+        results.put("passed",passed);
+        RestResultBean restResultBean = new RestResultBean(results);
+        pwmRequest.outputJsonResult(restResultBean);
+    }
+
+    private boolean checkIfRequiredVerificationPassed(final UserIdentity userIdentity, final HelpdeskVerificationStateBean verificationStateBean, final HelpdeskProfile helpdeskProfile) {
+        final Collection<IdentityVerificationMethod> requiredMethods = helpdeskProfile.readRequiredVerificationMethods();
+        if (requiredMethods == null || requiredMethods.isEmpty()) {
+            return true;
+        }
+        for (final IdentityVerificationMethod method : requiredMethods) {
+            if (!verificationStateBean.hasRecord(userIdentity, method)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void restShowVerifications(final PwmRequest pwmRequest)
+            throws IOException, PwmUnrecoverableException, ServletException, ChaiUnavailableException
+    {
+        final Map<String,String> bodyMap = pwmRequest.readBodyAsJsonStringMap(PwmHttpRequestWrapper.Flag.BypassValidation);
+        final String rawVerificationStr = bodyMap.get(HelpdeskVerificationStateBean.PARAMETER_VERIFICATION_STATE_KEY);
+        final HelpdeskVerificationStateBean state = HelpdeskVerificationStateBean.fromClientString(pwmRequest, rawVerificationStr);
+        final HashMap<String,Object> results = new HashMap<>();
+        try {
+            results.put("records",state.asViewableValidationRecords(pwmRequest.getPwmApplication(), pwmRequest.getLocale()));
+        } catch (ChaiOperationException e) {
+            throw PwmUnrecoverableException.fromChaiException(e);
+        }
+        RestResultBean restResultBean = new RestResultBean(results);
+        pwmRequest.outputJsonResult(restResultBean);
+    }
 }
 }

+ 10 - 1
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationRequestBean.java

@@ -30,7 +30,8 @@ public class HelpdeskVerificationRequestBean implements Serializable {
     private String destination;
     private String destination;
     private String userKey;
     private String userKey;
     private String code;
     private String code;
-    private String tokenData;
+    private String tokenData; // encrypted during transport
+    private String verificationState;
 
 
     public String getDestination() {
     public String getDestination() {
         return destination;
         return destination;
@@ -64,6 +65,14 @@ public class HelpdeskVerificationRequestBean implements Serializable {
         this.tokenData = tokenData;
         this.tokenData = tokenData;
     }
     }
 
 
+    public String getVerificationState() {
+        return verificationState;
+    }
+
+    public void setVerificationState(String verificationState) {
+        this.verificationState = verificationState;
+    }
+
     static class TokenData implements Serializable {
     static class TokenData implements Serializable {
         private String token;
         private String token;
         private Date issueDate;
         private Date issueDate;

+ 3 - 3
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationResponseBean.java

@@ -26,9 +26,9 @@ import java.io.Serializable;
 
 
 public class HelpdeskVerificationResponseBean implements Serializable {
 public class HelpdeskVerificationResponseBean implements Serializable {
     private boolean passed;
     private boolean passed;
-    private HelpdeskVerificationStateBean verificationState;
+    private String verificationState;
 
 
-    public HelpdeskVerificationResponseBean(boolean passed, HelpdeskVerificationStateBean verificationState) {
+    public HelpdeskVerificationResponseBean(boolean passed, String verificationState) {
         this.passed = passed;
         this.passed = passed;
         this.verificationState = verificationState;
         this.verificationState = verificationState;
     }
     }
@@ -37,7 +37,7 @@ public class HelpdeskVerificationResponseBean implements Serializable {
         return passed;
         return passed;
     }
     }
 
 
-    public HelpdeskVerificationStateBean getVerificationState() {
+    public String getVerificationState() {
         return verificationState;
         return verificationState;
     }
     }
 }
 }

+ 140 - 9
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskVerificationStateBean.java

@@ -22,19 +22,34 @@
 
 
 package password.pwm.http.servlet.helpdesk;
 package password.pwm.http.servlet.helpdesk;
 
 
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.AppProperty;
+import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.option.IdentityVerificationMethod;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.ldap.LdapOperationsHelper;
+import password.pwm.util.JsonUtil;
+import password.pwm.util.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
 
 
 import java.io.Serializable;
 import java.io.Serializable;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
 
 
-public class HelpdeskVerificationStateBean implements Serializable {
-    private UserIdentity actor;
-    private List<HelpdeskValidationRecord> records;
+class HelpdeskVerificationStateBean implements Serializable {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(HelpdeskVerificationStateBean.class);
+    private final UserIdentity actor;
+    private final List<HelpdeskValidationRecord> records = new ArrayList<>();
 
 
-    public HelpdeskVerificationStateBean(UserIdentity actor, List<HelpdeskValidationRecord> records) {
+    public static final String PARAMETER_VERIFICATION_STATE_KEY = "verificationState";
+
+    private transient TimeDuration maximumAge;
+
+    private HelpdeskVerificationStateBean(final UserIdentity actor) {
         this.actor = actor;
         this.actor = actor;
-        this.records = records;
     }
     }
 
 
     public UserIdentity getActor() {
     public UserIdentity getActor() {
@@ -45,13 +60,92 @@ public class HelpdeskVerificationStateBean implements Serializable {
         return records;
         return records;
     }
     }
 
 
-    static class HelpdeskValidationRecord {
+    public void addRecord(final UserIdentity identity, final IdentityVerificationMethod method) {
+        purgeOldRecords();
+
+        final HelpdeskValidationRecord record = getRecord(identity, method);
+        if (record != null) {
+            records.remove(record);
+        }
+        records.add(new HelpdeskValidationRecord(new Date(), identity, method));
+    }
+
+    public boolean hasRecord(final UserIdentity identity, final IdentityVerificationMethod method) {
+        purgeOldRecords();
+        return getRecord(identity,method) != null;
+    }
+
+    private HelpdeskValidationRecord getRecord(final UserIdentity identity, final IdentityVerificationMethod method) {
+        for (final HelpdeskValidationRecord record : records) {
+            if (record.getIdentity().equals(identity) && (method == null || record.getMethod() == method)) {
+                return record;
+            }
+        }
+        return null;
+    }
+
+
+    void purgeOldRecords() {
+        for (Iterator<HelpdeskValidationRecord> iterator = records.iterator(); iterator.hasNext(); ) {
+            final HelpdeskValidationRecord record = iterator.next();
+            final Date timestamp = record.getTimestamp();
+            final TimeDuration age = TimeDuration.fromCurrent(timestamp);
+            if (age.isLongerThan(maximumAge)) {
+                iterator.remove();
+            }
+        }
+    }
+
+    List<ViewableValidationRecord> asViewableValidationRecords(final PwmApplication pwmApplication, final Locale locale) throws ChaiOperationException, ChaiUnavailableException, PwmUnrecoverableException {
+        final Map<Date,ViewableValidationRecord> returnRecords = new TreeMap<>();
+        for (final HelpdeskValidationRecord record : records) {
+            final String username = LdapOperationsHelper.readLdapUsernameValue(pwmApplication, record.getIdentity());
+            final String profile = pwmApplication.getConfig().getLdapProfiles().get(record.getIdentity().getLdapProfileID()).getDisplayName(locale);
+            final String method = record.getMethod().getLabel(pwmApplication.getConfig(), locale);
+            returnRecords.put(record.getTimestamp(), new ViewableValidationRecord(record.getTimestamp(), profile, username, method));
+        }
+        return Collections.unmodifiableList(new ArrayList<>(returnRecords.values()));
+    }
+
+    static class ViewableValidationRecord implements Serializable {
+        private Date timestamp;
+        private String profile;
+        private String username;
+        private String method;
+
+        public ViewableValidationRecord(Date timestamp, String profile, String username, String method) {
+            this.timestamp = timestamp;
+            this.profile = profile;
+            this.username = username;
+            this.method = method;
+        }
+
+        public Date getTimestamp() {
+            return timestamp;
+        }
+
+        public String getProfile() {
+            return profile;
+        }
+
+        public String getUsername() {
+            return username;
+        }
+
+        public String getMethod() {
+            return method;
+        }
+    }
+
+    static class HelpdeskValidationRecord implements Serializable {
         private Date timestamp;
         private Date timestamp;
         private UserIdentity identity;
         private UserIdentity identity;
+        private IdentityVerificationMethod method;
 
 
-        public HelpdeskValidationRecord(Date timestamp, UserIdentity identity) {
+        public HelpdeskValidationRecord(Date timestamp, UserIdentity identity, IdentityVerificationMethod method) {
             this.timestamp = timestamp;
             this.timestamp = timestamp;
             this.identity = identity;
             this.identity = identity;
+            this.method = method;
         }
         }
 
 
         public Date getTimestamp() {
         public Date getTimestamp() {
@@ -61,5 +155,42 @@ public class HelpdeskVerificationStateBean implements Serializable {
         public UserIdentity getIdentity() {
         public UserIdentity getIdentity() {
             return identity;
             return identity;
         }
         }
+
+        public IdentityVerificationMethod getMethod() {
+            return method;
+        }
+    }
+
+    String toClientString(final PwmApplication pwmApplication) throws PwmUnrecoverableException {
+        return pwmApplication.getSecureService().encryptObjectToString(this);
+    }
+
+    static HelpdeskVerificationStateBean fromClientString(
+            final PwmRequest pwmRequest,
+            final String rawValue
+    )
+            throws PwmUnrecoverableException
+    {
+        final int maxAgeSeconds = Integer.parseInt(pwmRequest.getConfig().readAppProperty(AppProperty.HELPDESK_VERIFICATION_TIMEOUT_SECONDS));
+        final TimeDuration maxAge = new TimeDuration(maxAgeSeconds, TimeUnit.SECONDS);
+        final UserIdentity actor = pwmRequest.getUserInfoIfLoggedIn();
+
+        HelpdeskVerificationStateBean state = null;
+        if (rawValue != null && !rawValue.isEmpty()) {
+            state = pwmRequest.getPwmApplication().getSecureService().decryptObject(rawValue, HelpdeskVerificationStateBean.class);
+            if (!state.getActor().equals(actor)) {
+                state = null;
+            }
+        }
+
+        state = state != null ? state : new HelpdeskVerificationStateBean(actor);
+        state.maximumAge = maxAge;
+        state.purgeOldRecords();
+
+        LOGGER.debug(pwmRequest, "read current state: " + JsonUtil.serialize(state));
+
+        return state;
     }
     }
 }
 }
+
+

+ 1 - 1
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -273,7 +273,7 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
                 searchDuration.asCompactString() + " not using cache, size=" + results.getResults().size());
                 searchDuration.asCompactString() + " not using cache, size=" + results.getResults().size());
 
 
         final SearchResultBean searchResultBean = new SearchResultBean();
         final SearchResultBean searchResultBean = new SearchResultBean();
-        searchResultBean.setSearchResults(new ArrayList<>(results.resultsAsJsonOutput(pwmRequest.getPwmApplication())));
+        searchResultBean.setSearchResults(new ArrayList<>(results.resultsAsJsonOutput(pwmRequest.getPwmApplication(),null)));
         searchResultBean.setSizeExceeded(sizeExceeded);
         searchResultBean.setSizeExceeded(sizeExceeded);
         final String aboutMessage = LocaleHelper.getLocalizedMessage(
         final String aboutMessage = LocaleHelper.getLocalizedMessage(
                 pwmRequest.getLocale(),
                 pwmRequest.getLocale(),

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

@@ -56,6 +56,7 @@ public enum Display implements PwmDisplayBundle {
     Button_Logout,
     Button_Logout,
     Button_More,
     Button_More,
     Button_OrgChart,
     Button_OrgChart,
+    Button_OTP,
     Button_RecoverPassword,
     Button_RecoverPassword,
     Button_Reset,
     Button_Reset,
     Button_Search,
     Button_Search,
@@ -295,6 +296,7 @@ public enum Display implements PwmDisplayBundle {
     Value_True,
     Value_True,
     Value_NotApplicable,
     Value_NotApplicable,
     Value_Default,
     Value_Default,
+    Placeholder_Search,
     
     
     ;
     ;
 
 

+ 2 - 0
src/main/java/password/pwm/i18n/Message.java

@@ -84,7 +84,9 @@ public enum Message implements PwmDisplayBundle {
     EventLog_HelpdeskDeleteUser(null),
     EventLog_HelpdeskDeleteUser(null),
     EventLog_HelpdeskViewDetail(null),
     EventLog_HelpdeskViewDetail(null),
     EventLog_HelpdeskVerifyOtp(null),
     EventLog_HelpdeskVerifyOtp(null),
+    EventLog_HelpdeskVerifyOtpIncorrect(null),
     EventLog_HelpdeskVerifyToken(null),
     EventLog_HelpdeskVerifyToken(null),
+    EventLog_HelpdeskVerifyTokenIncorrect(null),
 
 
     Requirement_MinLengthPlural(null),
     Requirement_MinLengthPlural(null),
     Requirement_MinLength(Requirement_MinLengthPlural),
     Requirement_MinLength(Requirement_MinLengthPlural),

+ 10 - 8
src/main/java/password/pwm/ldap/UserSearchEngine.java

@@ -617,20 +617,22 @@ public class UserSearchEngine {
             return sizeExceeded;
             return sizeExceeded;
         }
         }
 
 
-        public List<Map<String,Object>> resultsAsJsonOutput(final PwmApplication pwmApplication)
+        public List<Map<String,Object>> resultsAsJsonOutput(final PwmApplication pwmApplication, final UserIdentity ignoreUser)
                 throws PwmUnrecoverableException
                 throws PwmUnrecoverableException
         {
         {
             final List<Map<String,Object>> outputList = new ArrayList<>();
             final List<Map<String,Object>> outputList = new ArrayList<>();
             int idCounter = 0;
             int idCounter = 0;
             for (final UserIdentity userIdentity : this.getResults().keySet()) {
             for (final UserIdentity userIdentity : this.getResults().keySet()) {
-                final Map<String,Object> rowMap = new LinkedHashMap<>();
-                for (final String attribute : this.getHeaderAttributeMap().keySet()) {
-                    rowMap.put(attribute,this.getResults().get(userIdentity).get(attribute));
+                if (ignoreUser == null || !ignoreUser.equals(userIdentity)) {
+                    final Map<String, Object> rowMap = new LinkedHashMap<>();
+                    for (final String attribute : this.getHeaderAttributeMap().keySet()) {
+                        rowMap.put(attribute, this.getResults().get(userIdentity).get(attribute));
+                    }
+                    rowMap.put("userKey", userIdentity.toObfuscatedKey(pwmApplication));
+                    rowMap.put("id", idCounter);
+                    outputList.add(rowMap);
+                    idCounter++;
                 }
                 }
-                rowMap.put("userKey",userIdentity.toObfuscatedKey(pwmApplication));
-                rowMap.put("id",idCounter);
-                outputList.add(rowMap);
-                idCounter++;
             }
             }
             return outputList;
             return outputList;
         }
         }

+ 2 - 0
src/main/java/password/pwm/svc/event/AuditEvent.java

@@ -64,7 +64,9 @@ public enum AuditEvent {
     HELPDESK_DELETE_USER(Message.EventLog_HelpdeskDeleteUser, Type.HELPDESK),
     HELPDESK_DELETE_USER(Message.EventLog_HelpdeskDeleteUser, Type.HELPDESK),
     HELPDESK_VIEW_DETAIL(Message.EventLog_HelpdeskViewDetail, Type.HELPDESK),
     HELPDESK_VIEW_DETAIL(Message.EventLog_HelpdeskViewDetail, Type.HELPDESK),
     HELPDESK_VERIFY_OTP(Message.EventLog_HelpdeskVerifyOtp, Type.HELPDESK),
     HELPDESK_VERIFY_OTP(Message.EventLog_HelpdeskVerifyOtp, Type.HELPDESK),
+    HELPDESK_VERIFY_OTP_INCORRECT(Message.EventLog_HelpdeskVerifyOtpIncorrect, Type.HELPDESK),
     HELPDESK_VERIFY_TOKEN(Message.EventLog_HelpdeskVerifyToken, Type.HELPDESK),
     HELPDESK_VERIFY_TOKEN(Message.EventLog_HelpdeskVerifyToken, Type.HELPDESK),
+    HELPDESK_VERIFY_TOKEN_INCORRECT(Message.EventLog_HelpdeskVerifyOtpIncorrect, Type.HELPDESK),
 
 
 
 
     ;
     ;

+ 0 - 2089
src/main/java/password/pwm/util/Base64Util.java

@@ -1,2089 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2016 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;
-
-/**
- * <p>Encodes and decodes to and from Base64Util notation.</p>
- * <p>Homepage: <a href="http://iharder.net/base64">http://iharder.net/base64</a>.</p>
- *
- * <p>Example:</p>
- *
- * <code>String encoded = Base64Util.encode( myByteArray );</code>
- * <br />
- * <code>byte[] myByteArray = Base64Util.decode( encoded );</code>
- *
- * <p>The <tt>options</tt> parameter, which appears in a few places, is used to pass
- * several pieces of information to the encoder. In the "higher level" methods such as
- * encodeBytes( bytes, options ) the options parameter can be used to indicate such
- * things as first gzipping the bytes before encoding them, not inserting linefeeds,
- * and encoding using the URL-safe and Ordered dialects.</p>
- *
- * <p>Note, according to <a href="http://www.faqs.org/rfcs/rfc3548.html">RFC3548</a>,
- * Section 2.1, implementations should not add line feeds unless explicitly told
- * to do so. I've got Base64Util set to this behavior now, although earlier versions
- * broke lines by default.</p>
- *
- * <p>The constants defined in Base64Util can be OR-ed together to combine options, so you
- * might make a call like this:</p>
- *
- * <code>String encoded = Base64Util.encodeBytes( mybytes, Base64Util.GZIP | Base64Util.DO_BREAK_LINES );</code>
- * <p>to compress the data before encoding it and then making the output have newline characters.</p>
- * <p>Also...</p>
- * <code>String encoded = Base64Util.encodeBytes( crazyString.getBytes() );</code>
- *
- *
- *
- * <p>
- * Change Log:
- * </p>
- * <ul>
- *  <li>v2.3.7 - Fixed subtle bug when base 64 input stream contained the
- *   value 01111111, which is an invalid base 64 character but should not
- *   throw an ArrayIndexOutOfBoundsException either. Led to discovery of
- *   mishandling (or potential for better handling) of other bad input
- *   characters. You should now read an IOException if you try decoding
- *   something that has bad characters in it.</li>
- *  <li>v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded
- *   string ended in the last column; the buffer was not properly shrunk and
- *   contained an extra (null) byte that made it into the string.</li>
- *  <li>v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size
- *   was wrong for files of size 31, 34, and 37 bytes.</li>
- *  <li>v2.3.4 - Fixed bug when working with gzipped streams whereby flushing
- *   the Base64Util.OutputStream closed the Base64Util encoding (by padding with equals
- *   signs) too soon. Also added an option to suppress the automatic decoding
- *   of gzipped streams. Also added experimental support for specifying a
- *   class loader when using the
- *   {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)}
- *   method.</li>
- *  <li>v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java
- *   footprint with its CharEncoders and so forth. Fixed some javadocs that were
- *   inconsistent. Removed imports and specified things like java.io.IOException
- *   explicitly inline.</li>
- *  <li>v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the
- *   final encoded data will be so that the code doesn't have to create two output
- *   arrays: an oversized initial one and then a final, exact-sized one. Big win
- *   when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not
- *   using the gzip options which uses a different mechanism with streams and stuff).</li>
- *  <li>v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some
- *   similar helper methods to be more efficient with memory by not returning a
- *   String but just a byte array.</li>
- *  <li>v2.3 - <strong>This is not a drop-in replacement!</strong> This is two years of comments
- *   and bug fixes queued up and finally executed. Thanks to everyone who sent
- *   me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else.
- *   Much bad coding was cleaned up including throwing exceptions where necessary
- *   instead of returning null values or something similar. Here are some changes
- *   that may affect you:
- *   <ul>
- *    <li><em>Does not break lines, by default.</em> This is to keep in compliance with
- *      <a href="http://www.faqs.org/rfcs/rfc3548.html">RFC3548</a>.</li>
- *    <li><em>Throws exceptions instead of returning null values.</em> Because some operations
- *      (especially those that may permit the GZIP option) use IO streams, there
- *      is a possiblity of an java.io.IOException being thrown. After some discussion and
- *      thought, I've changed the behavior of the methods to throw java.io.IOExceptions
- *      rather than return null if ever there's an error. I think this is more
- *      appropriate, though it will require some changes to your code. Sorry,
- *      it should have been done this way to begin with.</li>
- *    <li><em>Removed all references to System.out, System.err, and the like.</em>
- *      Shame on me. All I can say is sorry they were ever there.</li>
- *    <li><em>Throws NullPointerExceptions and IllegalArgumentExceptions</em> as needed
- *      such as when passed arrays are null or offsets are invalid.</li>
- *    <li>Cleaned up as much javadoc as I could to avoid any javadoc warnings.
- *      This was especially annoying before for people who were thorough in their
- *      own projects and then had gobs of javadoc warnings on this file.</li>
- *   </ul>
- *  <li>v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug
- *   when using very small files (~&lt; 40 bytes).</li>
- *  <li>v2.2 - Added some helper methods for encoding/decoding directly from
- *   one file to the next. Also added a main() method to support command line
- *   encoding/decoding from one file to the next. Also added these Base64Util dialects:
- *   <ol>
- *   <li>The default is RFC3548 format.</li>
- *   <li>Calling Base64Util.setFormat(Base64Util.BASE64_FORMAT.URLSAFE_FORMAT) generates
- *   URL and file name friendly format as described in Section 4 of RFC3548.
- *   http://www.faqs.org/rfcs/rfc3548.html</li>
- *   <li>Calling Base64Util.setFormat(Base64Util.BASE64_FORMAT.ORDERED_FORMAT) generates
- *   URL and file name friendly format that preserves lexical ordering as described
- *   in http://www.faqs.org/qa/rfcc-1940.html</li>
- *   </ol>
- *   Special thanks to Jim Kellerman at <a href="http://www.powerset.com/">http://www.powerset.com/</a>
- *   for contributing the new Base64Util dialects.
- *  </li>
- *
- *  <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
- *   some convenience methods for reading and writing to and from files.</li>
- *  <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
- *   with other encodings (like EBCDIC).</li>
- *  <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
- *   encoded data was a single byte.</li>
- *  <li>v2.0 - I got rid of methods that used booleans to set options.
- *   Now everything is more consolidated and cleaner. The code now detects
- *   when data that's being decoded is gzip-compressed and will decompress it
- *   automatically. Generally things are cleaner. You'll probably have to
- *   change some method calls that you were making to support the new
- *   options format (<tt>int</tt>s that you "OR" together).</li>
- *  <li>v1.5.1 - Fixed bug when decompressing and decoding to a
- *   byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>.
- *   Added the ability to "suspend" encoding in the Output Stream so
- *   you can turn on and off the encoding if you need to embed base64
- *   data in an otherwise "normal" stream (like an XML file).</li>
- *  <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself.
- *      This helps when using GZIP streams.
- *      Added the ability to GZip-compress objects before encoding them.</li>
- *  <li>v1.4 - Added helper methods to read/write files.</li>
- *  <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
- *  <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
- *      where last buffer being read, if not completely full, was not returned.</li>
- *  <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li>
- *  <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
- * </ul>
- *
- * <p>
- * I am placing this code in the Public Domain. Do with it as you will.
- * This software comes with no guarantees or warranties but with
- * plenty of well-wishing instead!
- * Please visit <a href="http://iharder.net/base64">http://iharder.net/base64</a>
- * periodically to check for updates or to contribute improvements.
- * </p>
- *
- * @author Robert Harder
- * @author rob@iharder.net
- * @version 2.3.7
- */
-public class Base64Util
-{
-
-/* ********  P U B L I C   F I E L D S  ******** */
-
-
-    /** No options specified. Value is zero. */
-    public final static int NO_OPTIONS = 0;
-
-    /** Specify encoding in first bit. Value is one. */
-    public final static int ENCODE = 1;
-
-
-    /** Specify decoding in first bit. Value is zero. */
-    public final static int DECODE = 0;
-
-
-    /** Specify that data should be gzip-compressed in second bit. Value is two. */
-    public final static int GZIP = 2;
-
-    /** Specify that gzipped data should <em>not</em> be automatically gunzipped. */
-    public final static int DONT_GUNZIP = 4;
-
-
-    /** Do break lines when encoding. Value is 8. */
-    public final static int DO_BREAK_LINES = 8;
-
-    /**
-     * Encode using Base64Util-like encoding that is URL- and Filename-safe as described
-     * in Section 4 of RFC3548:
-     * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
-     * It is important to note that data encoded this way is <em>not</em> officially valid Base64Util,
-     * or at the very least should not be called Base64Util without also specifying that is
-     * was encoded using the URL- and Filename-safe dialect.
-     */
-    public final static int URL_SAFE = 16;
-
-
-    /**
-     * Encode using the special "ordered" dialect of Base64Util described here:
-     * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
-     */
-    public final static int ORDERED = 32;
-
-
-/* ********  P R I V A T E   F I E L D S  ******** */
-
-
-    /** Maximum line length (76) of Base64Util output. */
-    private final static int MAX_LINE_LENGTH = 76;
-
-
-    /** The equals sign (=) as a byte. */
-    private final static byte EQUALS_SIGN = (byte)'=';
-
-
-    /** The new line character (\n) as a byte. */
-    private final static byte NEW_LINE = (byte)'\n';
-
-
-    /** Preferred encoding. */
-    private final static String PREFERRED_ENCODING = "US-ASCII";
-
-
-    private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
-    private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
-
-
-/* ********  S T A N D A R D   B A S E 6 4   A L P H A B E T  ******** */
-
-    /** The 64 valid Base64Util values. */
-    /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
-    private final static byte[] _STANDARD_ALPHABET = {
-            (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
-            (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
-            (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
-            (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
-            (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
-            (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
-            (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
-            (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
-            (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
-            (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
-    };
-
-
-    /**
-     * Translates a Base64Util value to either its 6-bit reconstruction value
-     * or a negative number indicating some other meaning.
-     **/
-    private final static byte[] _STANDARD_DECODABET = {
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,                 // Decimal  0 -  8
-            -5,-5,                                      // Whitespace: Tab and Linefeed
-            -9,-9,                                      // Decimal 11 - 12
-            -5,                                         // Whitespace: Carriage Return
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 14 - 26
-            -9,-9,-9,-9,-9,                             // Decimal 27 - 31
-            -5,                                         // Whitespace: Space
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,              // Decimal 33 - 42
-            62,                                         // Plus sign at decimal 43
-            -9,-9,-9,                                   // Decimal 44 - 46
-            63,                                         // Slash at decimal 47
-            52,53,54,55,56,57,58,59,60,61,              // Numbers zero through nine
-            -9,-9,-9,                                   // Decimal 58 - 60
-            -1,                                         // Equals sign at decimal 61
-            -9,-9,-9,                                      // Decimal 62 - 64
-            0,1,2,3,4,5,6,7,8,9,10,11,12,13,            // Letters 'A' through 'N'
-            14,15,16,17,18,19,20,21,22,23,24,25,        // Letters 'O' through 'Z'
-            -9,-9,-9,-9,-9,-9,                          // Decimal 91 - 96
-            26,27,28,29,30,31,32,33,34,35,36,37,38,     // Letters 'a' through 'm'
-            39,40,41,42,43,44,45,46,47,48,49,50,51,     // Letters 'n' through 'z'
-            -9,-9,-9,-9,-9                              // Decimal 123 - 127
-            ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,       // Decimal 128 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255
-    };
-
-
-/* ********  U R L   S A F E   B A S E 6 4   A L P H A B E T  ******** */
-
-    /**
-     * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
-     * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
-     * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
-     */
-    private final static byte[] _URL_SAFE_ALPHABET = {
-            (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
-            (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
-            (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
-            (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
-            (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
-            (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
-            (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
-            (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
-            (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
-            (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_'
-    };
-
-    /**
-     * Used in decoding URL- and Filename-safe dialects of Base64Util.
-     */
-    private final static byte[] _URL_SAFE_DECODABET = {
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,                 // Decimal  0 -  8
-            -5,-5,                                      // Whitespace: Tab and Linefeed
-            -9,-9,                                      // Decimal 11 - 12
-            -5,                                         // Whitespace: Carriage Return
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 14 - 26
-            -9,-9,-9,-9,-9,                             // Decimal 27 - 31
-            -5,                                         // Whitespace: Space
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,              // Decimal 33 - 42
-            -9,                                         // Plus sign at decimal 43
-            -9,                                         // Decimal 44
-            62,                                         // Minus sign at decimal 45
-            -9,                                         // Decimal 46
-            -9,                                         // Slash at decimal 47
-            52,53,54,55,56,57,58,59,60,61,              // Numbers zero through nine
-            -9,-9,-9,                                   // Decimal 58 - 60
-            -1,                                         // Equals sign at decimal 61
-            -9,-9,-9,                                   // Decimal 62 - 64
-            0,1,2,3,4,5,6,7,8,9,10,11,12,13,            // Letters 'A' through 'N'
-            14,15,16,17,18,19,20,21,22,23,24,25,        // Letters 'O' through 'Z'
-            -9,-9,-9,-9,                                // Decimal 91 - 94
-            63,                                         // Underscore at decimal 95
-            -9,                                         // Decimal 96
-            26,27,28,29,30,31,32,33,34,35,36,37,38,     // Letters 'a' through 'm'
-            39,40,41,42,43,44,45,46,47,48,49,50,51,     // Letters 'n' through 'z'
-            -9,-9,-9,-9,-9                              // Decimal 123 - 127
-            ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255
-    };
-
-
-
-/* ********  O R D E R E D   B A S E 6 4   A L P H A B E T  ******** */
-
-    /**
-     * I don't read the point of this technique, but someone requested it,
-     * and it is described here:
-     * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
-     */
-    private final static byte[] _ORDERED_ALPHABET = {
-            (byte)'-',
-            (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
-            (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
-            (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
-            (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
-            (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
-            (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
-            (byte)'_',
-            (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
-            (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
-            (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
-            (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z'
-    };
-
-    /**
-     * Used in decoding the "ordered" dialect of Base64Util.
-     */
-    private final static byte[] _ORDERED_DECODABET = {
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,                 // Decimal  0 -  8
-            -5,-5,                                      // Whitespace: Tab and Linefeed
-            -9,-9,                                      // Decimal 11 - 12
-            -5,                                         // Whitespace: Carriage Return
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 14 - 26
-            -9,-9,-9,-9,-9,                             // Decimal 27 - 31
-            -5,                                         // Whitespace: Space
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,              // Decimal 33 - 42
-            -9,                                         // Plus sign at decimal 43
-            -9,                                         // Decimal 44
-            0,                                          // Minus sign at decimal 45
-            -9,                                         // Decimal 46
-            -9,                                         // Slash at decimal 47
-            1,2,3,4,5,6,7,8,9,10,                       // Numbers zero through nine
-            -9,-9,-9,                                   // Decimal 58 - 60
-            -1,                                         // Equals sign at decimal 61
-            -9,-9,-9,                                   // Decimal 62 - 64
-            11,12,13,14,15,16,17,18,19,20,21,22,23,     // Letters 'A' through 'M'
-            24,25,26,27,28,29,30,31,32,33,34,35,36,     // Letters 'N' through 'Z'
-            -9,-9,-9,-9,                                // Decimal 91 - 94
-            37,                                         // Underscore at decimal 95
-            -9,                                         // Decimal 96
-            38,39,40,41,42,43,44,45,46,47,48,49,50,     // Letters 'a' through 'm'
-            51,52,53,54,55,56,57,58,59,60,61,62,63,     // Letters 'n' through 'z'
-            -9,-9,-9,-9,-9                                 // Decimal 123 - 127
-            ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
-            -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255
-    };
-
-
-/* ********  D E T E R M I N E   W H I C H   A L H A B E T  ******** */
-
-
-    /**
-     * Returns one of the _SOMETHING_ALPHABET byte arrays depending on
-     * the options specified.
-     * It's possible, though silly, to specify ORDERED <b>and</b> URLSAFE
-     * in which case one of them will be picked, though there is
-     * no guarantee as to which one will be picked.
-     */
-    private final static byte[] getAlphabet( int options ) {
-        if ((options & URL_SAFE) == URL_SAFE) {
-            return _URL_SAFE_ALPHABET;
-        } else if ((options & ORDERED) == ORDERED) {
-            return _ORDERED_ALPHABET;
-        } else {
-            return _STANDARD_ALPHABET;
-        }
-    }	// end getAlphabet
-
-
-    /**
-     * Returns one of the _SOMETHING_DECODABET byte arrays depending on
-     * the options specified.
-     * It's possible, though silly, to specify ORDERED and URL_SAFE
-     * in which case one of them will be picked, though there is
-     * no guarantee as to which one will be picked.
-     */
-    private final static byte[] getDecodabet( int options ) {
-        if( (options & URL_SAFE) == URL_SAFE) {
-            return _URL_SAFE_DECODABET;
-        } else if ((options & ORDERED) == ORDERED) {
-            return _ORDERED_DECODABET;
-        } else {
-            return _STANDARD_DECODABET;
-        }
-    }	// end getAlphabet
-
-
-
-    /** Defeats instantiation. */
-    private Base64Util(){}
-
-
-
-
-/* ********  E N C O D I N G   M E T H O D S  ******** */
-
-
-    /**
-     * Encodes up to the first three bytes of array <var>threeBytes</var>
-     * and returns a four-byte array in Base64Util notation.
-     * The actual number of significant bytes in your array is
-     * given by <var>numSigBytes</var>.
-     * The array <var>threeBytes</var> needs only be as big as
-     * <var>numSigBytes</var>.
-     * Code can reuse a byte array by passing a four-byte array as <var>b4</var>.
-     *
-     * @param b4 A reusable byte array to reduce array instantiation
-     * @param threeBytes the array to convert
-     * @param numSigBytes the number of significant bytes in your array
-     * @return four byte array in Base64Util notation.
-     * @since 1.5.1
-     */
-    private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) {
-        encode3to4( threeBytes, 0, numSigBytes, b4, 0, options );
-        return b4;
-    }   // end encode3to4
-
-
-    /**
-     * <p>Encodes up to three bytes of the array <var>source</var>
-     * and writes the resulting four Base64Util bytes to <var>destination</var>.
-     * The source and destination arrays can be manipulated
-     * anywhere along their length by specifying
-     * <var>srcOffset</var> and <var>destOffset</var>.
-     * This method does not check to make sure your arrays
-     * are large enough to accomodate <var>srcOffset</var> + 3 for
-     * the <var>source</var> array or <var>destOffset</var> + 4 for
-     * the <var>destination</var> array.
-     * The actual number of significant bytes in your array is
-     * given by <var>numSigBytes</var>.</p>
-     * <p>This is the lowest level of the encoding methods with
-     * all possible parameters.</p>
-     *
-     * @param source the array to convert
-     * @param srcOffset the index where conversion begins
-     * @param numSigBytes the number of significant bytes in your array
-     * @param destination the array to hold the conversion
-     * @param destOffset the index where output will be put
-     * @return the <var>destination</var> array
-     * @since 1.3
-     */
-    private static byte[] encode3to4(
-            byte[] source, int srcOffset, int numSigBytes,
-            byte[] destination, int destOffset, int options ) {
-
-        byte[] ALPHABET = getAlphabet( options );
-
-        //           1         2         3
-        // 01234567890123456789012345678901 Bit position
-        // --------000000001111111122222222 Array position from threeBytes
-        // --------|    ||    ||    ||    | Six bit groups to index ALPHABET
-        //          >>18  >>12  >> 6  >> 0  Right shift necessary
-        //                0x3f  0x3f  0x3f  Additional AND
-
-        // Create buffer with zero-padding if there are only one or two
-        // significant bytes passed in the array.
-        // We have to shift left 24 in order to flush out the 1's that appear
-        // when Java treats a value as negative that is cast from a byte to an int.
-        int inBuff =   ( numSigBytes > 0 ? ((source[ srcOffset     ] << 24) >>>  8) : 0 )
-                | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
-                | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
-
-        switch( numSigBytes )
-        {
-            case 3:
-                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
-                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
-                destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
-                destination[ destOffset + 3 ] = ALPHABET[ (inBuff       ) & 0x3f ];
-                return destination;
-
-            case 2:
-                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
-                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
-                destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>>  6) & 0x3f ];
-                destination[ destOffset + 3 ] = EQUALS_SIGN;
-                return destination;
-
-            case 1:
-                destination[ destOffset     ] = ALPHABET[ (inBuff >>> 18)        ];
-                destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
-                destination[ destOffset + 2 ] = EQUALS_SIGN;
-                destination[ destOffset + 3 ] = EQUALS_SIGN;
-                return destination;
-
-            default:
-                return destination;
-        }   // end switch
-    }   // end encode3to4
-
-
-
-    /**
-     * Performs Base64Util encoding on the <code>raw</code> ByteBuffer,
-     * writing it to the <code>encoded</code> ByteBuffer.
-     * This is an experimental feature. Currently it does not
-     * pass along any options (such as {@link #DO_BREAK_LINES}
-     * or {@link #GZIP}.
-     *
-     * @param raw input buffer
-     * @param encoded output buffer
-     * @since 2.3
-     */
-    public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){
-        byte[] raw3 = new byte[3];
-        byte[] enc4 = new byte[4];
-
-        while( raw.hasRemaining() ){
-            int rem = Math.min(3,raw.remaining());
-            raw.get(raw3,0,rem);
-            Base64Util.encode3to4(enc4, raw3, rem, Base64Util.NO_OPTIONS);
-            encoded.put(enc4);
-        }   // end input remaining
-    }
-
-
-    /**
-     * Performs Base64Util encoding on the <code>raw</code> ByteBuffer,
-     * writing it to the <code>encoded</code> CharBuffer.
-     * This is an experimental feature. Currently it does not
-     * pass along any options (such as {@link #DO_BREAK_LINES}
-     * or {@link #GZIP}.
-     *
-     * @param raw input buffer
-     * @param encoded output buffer
-     * @since 2.3
-     */
-    public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){
-        byte[] raw3 = new byte[3];
-        byte[] enc4 = new byte[4];
-
-        while( raw.hasRemaining() ){
-            int rem = Math.min(3,raw.remaining());
-            raw.get(raw3,0,rem);
-            Base64Util.encode3to4(enc4, raw3, rem, Base64Util.NO_OPTIONS);
-            for( int i = 0; i < 4; i++ ){
-                encoded.put( (char)(enc4[i] & 0xFF) );
-            }
-        }   // end input remaining
-    }
-
-
-
-
-    /**
-     * Serializes an object and returns the Base64Util-encoded
-     * version of that serialized object.
-     *
-     * <p>As of v 2.3, if the object
-     * cannot be serialized or there is another error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned a null value, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * The object is not GZip-compressed before being encoded.
-     *
-     * @param serializableObject The object to encode
-     * @return The Base64Util-encoded object
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if serializedObject is null
-     * @since 1.4
-     */
-    public static String encodeObject( java.io.Serializable serializableObject )
-            throws java.io.IOException {
-        return encodeObject( serializableObject, NO_OPTIONS );
-    }   // end encodeObject
-
-
-
-    /**
-     * Serializes an object and returns the Base64Util-encoded
-     * version of that serialized object.
-     *
-     * <p>As of v 2.3, if the object
-     * cannot be serialized or there is another error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned a null value, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * The object is not GZip-compressed before being encoded.
-     * <p>
-     * Example options:<pre>
-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     * </pre>
-     * <p>
-     * Example: <code>encodeObject( myObj, Base64Util.GZIP )</code> or
-     * <p>
-     * Example: <code>encodeObject( myObj, Base64Util.GZIP | Base64Util.DO_BREAK_LINES )</code>
-     *
-     * @param serializableObject The object to encode
-     * @param options Specified options
-     * @return The Base64Util-encoded object
-     * @see Base64Util#GZIP
-     * @see Base64Util#DO_BREAK_LINES
-     * @throws java.io.IOException if there is an error
-     * @since 2.0
-     */
-    public static String encodeObject( java.io.Serializable serializableObject, int options )
-            throws java.io.IOException {
-
-        if( serializableObject == null ){
-            throw new NullPointerException( "Cannot serialize a null object." );
-        }   // end if: null
-
-        // Streams
-        java.io.ByteArrayOutputStream  baos  = null;
-        java.io.OutputStream           b64os = null;
-        java.util.zip.GZIPOutputStream gzos  = null;
-        java.io.ObjectOutputStream     oos   = null;
-
-
-        try {
-            // ObjectOutputStream -> (GZIP) -> Base64Util -> ByteArrayOutputStream
-            baos  = new java.io.ByteArrayOutputStream();
-            b64os = new Base64Util.OutputStream( baos, ENCODE | options );
-            if( (options & GZIP) != 0 ){
-                // Gzip
-                gzos = new java.util.zip.GZIPOutputStream(b64os);
-                oos = new java.io.ObjectOutputStream( gzos );
-            } else {
-                // Not gzipped
-                oos = new java.io.ObjectOutputStream( b64os );
-            }
-            oos.writeObject( serializableObject );
-        }   // end try
-        catch( java.io.IOException e ) {
-            // Catch it and then throw it immediately so that
-            // the finally{} block is called for cleanup.
-            throw e;
-        }   // end catch
-        finally {
-            try{ oos.close();   } catch( Exception e ){}
-            try{ gzos.close();  } catch( Exception e ){}
-            try{ b64os.close(); } catch( Exception e ){}
-            try{ baos.close();  } catch( Exception e ){}
-        }   // end finally
-
-        // Return value according to relevant encoding.
-        try {
-            return new String( baos.toByteArray(), PREFERRED_ENCODING );
-        }   // end try
-        catch (java.io.UnsupportedEncodingException uue){
-            // Fall back to some Java default
-            return new String( baos.toByteArray() );
-        }   // end catch
-
-    }   // end encode
-
-
-
-    /**
-     * Encodes a byte array into Base64Util notation.
-     * Does not GZip-compress data.
-     *
-     * @param source The data to convert
-     * @return The data in Base64Util-encoded form
-     * @throws NullPointerException if source array is null
-     * @since 1.4
-     */
-    public static String encodeBytes( byte[] source ) {
-        // Since we're not going to have the GZIP encoding turned on,
-        // we're not going to have an java.io.IOException thrown, so
-        // we should not force the user to have to catch it.
-        String encoded = null;
-        try {
-            encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
-        } catch (java.io.IOException ex) {
-            assert false : ex.getMessage();
-        }   // end catch
-        assert encoded != null;
-        return encoded;
-    }   // end encodeBytes
-
-
-
-    /**
-     * Encodes a byte array into Base64Util notation.
-     * <p>
-     * Example options:<pre>
-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
-     * </pre>
-     * <p>
-     * Example: <code>encodeBytes( myData, Base64Util.GZIP )</code> or
-     * <p>
-     * Example: <code>encodeBytes( myData, Base64Util.GZIP | Base64Util.DO_BREAK_LINES )</code>
-     *
-     *
-     * <p>As of v 2.3, if there is an error with the GZIP stream,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned a null value, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     *
-     * @param source The data to convert
-     * @param options Specified options
-     * @return The Base64Util-encoded data as a String
-     * @see Base64Util#GZIP
-     * @see Base64Util#DO_BREAK_LINES
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if source array is null
-     * @since 2.0
-     */
-    public static String encodeBytes( byte[] source, int options ) throws java.io.IOException {
-        return encodeBytes( source, 0, source.length, options );
-    }   // end encodeBytes
-
-
-    /**
-     * Encodes a byte array into Base64Util notation.
-     * Does not GZip-compress data.
-     *
-     * <p>As of v 2.3, if there is an error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned a null value, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     *
-     * @param source The data to convert
-     * @param off Offset in array where conversion should begin
-     * @param len Length of data to convert
-     * @return The Base64Util-encoded data as a String
-     * @throws NullPointerException if source array is null
-     * @throws IllegalArgumentException if source array, offset, or length are invalid
-     * @since 1.4
-     */
-    public static String encodeBytes( byte[] source, int off, int len ) {
-        // Since we're not going to have the GZIP encoding turned on,
-        // we're not going to have an java.io.IOException thrown, so
-        // we should not force the user to have to catch it.
-        String encoded = null;
-        try {
-            encoded = encodeBytes( source, off, len, NO_OPTIONS );
-        } catch (java.io.IOException ex) {
-            assert false : ex.getMessage();
-        }   // end catch
-        assert encoded != null;
-        return encoded;
-    }   // end encodeBytes
-
-
-
-    /**
-     * Encodes a byte array into Base64Util notation.
-     * <p>
-     * Example options:<pre>
-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     <i>Note: Technically, this makes your encoding non-compliant.</i>
-     * </pre>
-     * <p>
-     * Example: <code>encodeBytes( myData, Base64Util.GZIP )</code> or
-     * <p>
-     * Example: <code>encodeBytes( myData, Base64Util.GZIP | Base64Util.DO_BREAK_LINES )</code>
-     *
-     *
-     * <p>As of v 2.3, if there is an error with the GZIP stream,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned a null value, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     *
-     * @param source The data to convert
-     * @param off Offset in array where conversion should begin
-     * @param len Length of data to convert
-     * @param options Specified options
-     * @return The Base64Util-encoded data as a String
-     * @see Base64Util#GZIP
-     * @see Base64Util#DO_BREAK_LINES
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if source array is null
-     * @throws IllegalArgumentException if source array, offset, or length are invalid
-     * @since 2.0
-     */
-    public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException {
-        byte[] encoded = encodeBytesToBytes( source, off, len, options );
-
-        // Return value according to relevant encoding.
-        try {
-            return new String( encoded, PREFERRED_ENCODING );
-        }   // end try
-        catch (java.io.UnsupportedEncodingException uue) {
-            return new String( encoded );
-        }   // end catch
-
-    }   // end encodeBytes
-
-
-
-
-    /**
-     * Similar to {@link #encodeBytes(byte[])} but returns
-     * a byte array instead of instantiating a String. This is more efficient
-     * if you're working with I/O streams and have large data sets to encode.
-     *
-     *
-     * @param source The data to convert
-     * @return The Base64Util-encoded data as a byte[] (of ASCII characters)
-     * @throws NullPointerException if source array is null
-     * @since 2.3.1
-     */
-    public static byte[] encodeBytesToBytes( byte[] source ) {
-        byte[] encoded = null;
-        try {
-            encoded = encodeBytesToBytes( source, 0, source.length, Base64Util.NO_OPTIONS );
-        } catch( java.io.IOException ex ) {
-            assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
-        }
-        return encoded;
-    }
-
-
-    /**
-     * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns
-     * a byte array instead of instantiating a String. This is more efficient
-     * if you're working with I/O streams and have large data sets to encode.
-     *
-     *
-     * @param source The data to convert
-     * @param off Offset in array where conversion should begin
-     * @param len Length of data to convert
-     * @param options Specified options
-     * @return The Base64Util-encoded data as a String
-     * @see Base64Util#GZIP
-     * @see Base64Util#DO_BREAK_LINES
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if source array is null
-     * @throws IllegalArgumentException if source array, offset, or length are invalid
-     * @since 2.3.1
-     */
-    public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException {
-
-        if( source == null ){
-            throw new NullPointerException( "Cannot serialize a null array." );
-        }   // end if: null
-
-        if( off < 0 ){
-            throw new IllegalArgumentException( "Cannot have negative offset: " + off );
-        }   // end if: off < 0
-
-        if( len < 0 ){
-            throw new IllegalArgumentException( "Cannot have length offset: " + len );
-        }   // end if: len < 0
-
-        if( off + len > source.length  ){
-            throw new IllegalArgumentException(
-                    String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length));
-        }   // end if: off < 0
-
-
-
-        // Compress?
-        if( (options & GZIP) != 0 ) {
-            java.io.ByteArrayOutputStream  baos  = null;
-            java.util.zip.GZIPOutputStream gzos  = null;
-            Base64Util.OutputStream            b64os = null;
-
-            try {
-                // GZip -> Base64Util -> ByteArray
-                baos = new java.io.ByteArrayOutputStream();
-                b64os = new Base64Util.OutputStream( baos, ENCODE | options );
-                gzos  = new java.util.zip.GZIPOutputStream( b64os );
-
-                gzos.write( source, off, len );
-                gzos.close();
-            }   // end try
-            catch( java.io.IOException e ) {
-                // Catch it and then throw it immediately so that
-                // the finally{} block is called for cleanup.
-                throw e;
-            }   // end catch
-            finally {
-                try{ gzos.close();  } catch( Exception e ){}
-                try{ b64os.close(); } catch( Exception e ){}
-                try{ baos.close();  } catch( Exception e ){}
-            }   // end finally
-
-            return baos.toByteArray();
-        }   // end if: compress
-
-        // Else, don't compress. Better not to use streams at all then.
-        else {
-            boolean breakLines = (options & DO_BREAK_LINES) != 0;
-
-            //int    len43   = len * 4 / 3;
-            //byte[] outBuff = new byte[   ( len43 )                      // Main 4:3
-            //                           + ( (len % 3) > 0 ? 4 : 0 )      // Account for padding
-            //                           + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
-            // Try to determine more precisely how big the array needs to be.
-            // If we read it right, we don't have to do an array copy, and
-            // we save a bunch of memory.
-            int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding
-            if( breakLines ){
-                encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
-            }
-            byte[] outBuff = new byte[ encLen ];
-
-
-            int d = 0;
-            int e = 0;
-            int len2 = len - 2;
-            int lineLength = 0;
-            for( ; d < len2; d+=3, e+=4 ) {
-                encode3to4( source, d+off, 3, outBuff, e, options );
-
-                lineLength += 4;
-                if( breakLines && lineLength >= MAX_LINE_LENGTH )
-                {
-                    outBuff[e+4] = NEW_LINE;
-                    e++;
-                    lineLength = 0;
-                }   // end if: end of line
-            }   // en dfor: each piece of array
-
-            if( d < len ) {
-                encode3to4( source, d+off, len - d, outBuff, e, options );
-                e += 4;
-            }   // end if: some padding needed
-
-
-            // Only resize array if we didn't guess it right.
-            if( e <= outBuff.length - 1 ){
-                // If breaking lines and the last byte falls right at
-                // the line length (76 bytes per line), there will be
-                // one extra byte, and the array will need to be resized.
-                // Not too bad of an estimate on array size, I'd say.
-                byte[] finalOut = new byte[e];
-                System.arraycopy(outBuff,0, finalOut,0,e);
-                //System.err.println("Having to resize array from " + outBuff.length + " to " + e );
-                return finalOut;
-            } else {
-                //System.err.println("No need to resize array.");
-                return outBuff;
-            }
-
-        }   // end else: don't compress
-
-    }   // end encodeBytesToBytes
-
-
-
-
-
-/* ********  D E C O D I N G   M E T H O D S  ******** */
-
-
-    /**
-     * Decodes four bytes from array <var>source</var>
-     * and writes the resulting bytes (up to three of them)
-     * to <var>destination</var>.
-     * The source and destination arrays can be manipulated
-     * anywhere along their length by specifying
-     * <var>srcOffset</var> and <var>destOffset</var>.
-     * This method does not check to make sure your arrays
-     * are large enough to accomodate <var>srcOffset</var> + 4 for
-     * the <var>source</var> array or <var>destOffset</var> + 3 for
-     * the <var>destination</var> array.
-     * This method returns the actual number of bytes that
-     * were converted from the Base64Util encoding.
-     * <p>This is the lowest level of the decoding methods with
-     * all possible parameters.</p>
-     *
-     *
-     * @param source the array to convert
-     * @param srcOffset the index where conversion begins
-     * @param destination the array to hold the conversion
-     * @param destOffset the index where output will be put
-     * @param options alphabet type is pulled from this (standard, url-safe, ordered)
-     * @return the number of decoded bytes converted
-     * @throws NullPointerException if source or destination arrays are null
-     * @throws IllegalArgumentException if srcOffset or destOffset are invalid
-     *         or there is not enough room in the array.
-     * @since 1.3
-     */
-    private static int decode4to3(
-            byte[] source, int srcOffset,
-            byte[] destination, int destOffset, int options ) {
-
-        // Lots of error checking and exception throwing
-        if( source == null ){
-            throw new NullPointerException( "Source array was null." );
-        }   // end if
-        if( destination == null ){
-            throw new NullPointerException( "Destination array was null." );
-        }   // end if
-        if( srcOffset < 0 || srcOffset + 3 >= source.length ){
-            throw new IllegalArgumentException( String.format(
-                    "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) );
-        }   // end if
-        if( destOffset < 0 || destOffset +2 >= destination.length ){
-            throw new IllegalArgumentException( String.format(
-                    "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) );
-        }   // end if
-
-
-        byte[] DECODABET = getDecodabet( options );
-
-        // Example: Dk==
-        if( source[ srcOffset + 2] == EQUALS_SIGN ) {
-            // Two ways to do the same thing. Don't know which way I like best.
-            //int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] << 24 ) >>>  6 )
-            //              | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
-            int outBuff =   ( ( DECODABET[ source[ srcOffset    ] ] & 0xFF ) << 18 )
-                    | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
-
-            destination[ destOffset ] = (byte)( outBuff >>> 16 );
-            return 1;
-        }
-
-        // Example: DkL=
-        else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
-            // Two ways to do the same thing. Don't know which way I like best.
-            //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
-            //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
-            //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
-            int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
-                    | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
-                    | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6 );
-
-            destination[ destOffset     ] = (byte)( outBuff >>> 16 );
-            destination[ destOffset + 1 ] = (byte)( outBuff >>>  8 );
-            return 2;
-        }
-
-        // Example: DkLE
-        else {
-            // Two ways to do the same thing. Don't know which way I like best.
-            //int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] << 24 ) >>>  6 )
-            //              | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
-            //              | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
-            //              | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
-            int outBuff =   ( ( DECODABET[ source[ srcOffset     ] ] & 0xFF ) << 18 )
-                    | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
-                    | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) <<  6)
-                    | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF )      );
-
-
-            destination[ destOffset     ] = (byte)( outBuff >> 16 );
-            destination[ destOffset + 1 ] = (byte)( outBuff >>  8 );
-            destination[ destOffset + 2 ] = (byte)( outBuff       );
-
-            return 3;
-        }
-    }   // end decodeToBytes
-
-
-
-
-
-    /**
-     * Low-level access to decoding ASCII characters in
-     * the form of a byte array. <strong>Ignores GUNZIP option, if
-     * it's set.</strong> This is not generally a recommended method,
-     * although it is used internally as part of the decoding process.
-     * Special case: if len = 0, an empty array is returned. Still,
-     * if you need more speed and reduced memory footprint (and aren't
-     * gzipping), consider this method.
-     *
-     * @param source The Base64Util encoded data
-     * @return decoded data
-     * @since 2.3.1
-     */
-    public static byte[] decode( byte[] source )
-            throws java.io.IOException {
-        byte[] decoded = null;
-//        try {
-        decoded = decode( source, 0, source.length, Base64Util.NO_OPTIONS );
-//        } catch( java.io.IOException ex ) {
-//            assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
-//        }
-        return decoded;
-    }
-
-
-
-    /**
-     * Low-level access to decoding ASCII characters in
-     * the form of a byte array. <strong>Ignores GUNZIP option, if
-     * it's set.</strong> This is not generally a recommended method,
-     * although it is used internally as part of the decoding process.
-     * Special case: if len = 0, an empty array is returned. Still,
-     * if you need more speed and reduced memory footprint (and aren't
-     * gzipping), consider this method.
-     *
-     * @param source The Base64Util encoded data
-     * @param off    The offset of where to begin decoding
-     * @param len    The length of characters to decode
-     * @param options Can specify options such as alphabet type to use
-     * @return decoded data
-     * @throws java.io.IOException If bogus characters exist in source data
-     * @since 1.3
-     */
-    public static byte[] decode( byte[] source, int off, int len, int options )
-            throws java.io.IOException {
-
-        // Lots of error checking and exception throwing
-        if( source == null ){
-            throw new NullPointerException( "Cannot decode null source array." );
-        }   // end if
-        if( off < 0 || off + len > source.length ){
-            throw new IllegalArgumentException( String.format(
-                    "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) );
-        }   // end if
-
-        if( len == 0 ){
-            return new byte[0];
-        }else if( len < 4 ){
-            throw new IllegalArgumentException(
-                    "Base64Util-encoded string must have at least four characters, but length specified was " + len );
-        }   // end if
-
-        byte[] DECODABET = getDecodabet( options );
-
-        int    len34   = len * 3 / 4;       // Estimate on array size
-        byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
-        int    outBuffPosn = 0;             // Keep track of where we're writing
-
-        byte[] b4        = new byte[4];     // Four byte buffer from source, eliminating white space
-        int    b4Posn    = 0;               // Keep track of four byte input buffer
-        int    i         = 0;               // Source array counter
-        byte   sbiDecode = 0;               // Special value from DECODABET
-
-        for( i = off; i < off+len; i++ ) {  // Loop through source
-
-            sbiDecode = DECODABET[ source[i]&0xFF ];
-
-            // White space, Equals sign, or legit Base64Util character
-            // Note the values such as -5 and -9 in the
-            // DECODABETs at the top of the file.
-            if( sbiDecode >= WHITE_SPACE_ENC )  {
-                if( sbiDecode >= EQUALS_SIGN_ENC ) {
-                    b4[ b4Posn++ ] = source[i];         // Save non-whitespace
-                    if( b4Posn > 3 ) {                  // Time to decode?
-                        outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options );
-                        b4Posn = 0;
-
-                        // If that was the equals sign, break out of 'for' loop
-                        if( source[i] == EQUALS_SIGN ) {
-                            break;
-                        }   // end if: equals sign
-                    }   // end if: quartet built
-                }   // end if: equals sign or better
-            }   // end if: white space, equals sign or better
-            else {
-                // There's a bad input character in the Base64Util stream.
-                throw new java.io.IOException( String.format(
-                        "Bad Base64Util input character decimal %d in array position %d", ((int)source[i])&0xFF, i ) );
-            }   // end else:
-        }   // each input character
-
-        byte[] out = new byte[ outBuffPosn ];
-        System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
-        return out;
-    }   // end decode
-
-
-
-
-    /**
-     * Decodes data from Base64Util notation, automatically
-     * detecting gzip-compressed data and decompressing it.
-     *
-     * @param s the string to decode
-     * @return the decoded data
-     * @throws java.io.IOException If there is a problem
-     * @since 1.4
-     */
-    public static byte[] decode( String s ) throws java.io.IOException {
-        return decode( s, NO_OPTIONS );
-    }
-
-
-
-    /**
-     * Decodes data from Base64Util notation, automatically
-     * detecting gzip-compressed data and decompressing it.
-     *
-     * @param s the string to decode
-     * @param options encode options such as URL_SAFE
-     * @return the decoded data
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if <tt>s</tt> is null
-     * @since 1.4
-     */
-    public static byte[] decode( String s, int options ) throws java.io.IOException {
-
-        if( s == null ){
-            throw new NullPointerException( "Input string was null." );
-        }   // end if
-
-        byte[] bytes;
-        try {
-            bytes = s.getBytes( PREFERRED_ENCODING );
-        }   // end try
-        catch( java.io.UnsupportedEncodingException uee ) {
-            bytes = s.getBytes();
-        }   // end catch
-        //</change>
-
-        // Decode
-        bytes = decode( bytes, 0, bytes.length, options );
-
-        // Check to see if it's gzip-compressed
-        // GZIP Magic Two-Byte Number: 0x8b1f (35615)
-        boolean dontGunzip = (options & DONT_GUNZIP) != 0;
-        if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) {
-
-            int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
-            if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head )  {
-                java.io.ByteArrayInputStream  bais = null;
-                java.util.zip.GZIPInputStream gzis = null;
-                java.io.ByteArrayOutputStream baos = null;
-                byte[] buffer = new byte[2048];
-                int    length = 0;
-
-                try {
-                    baos = new java.io.ByteArrayOutputStream();
-                    bais = new java.io.ByteArrayInputStream( bytes );
-                    gzis = new java.util.zip.GZIPInputStream( bais );
-
-                    while( ( length = gzis.read( buffer ) ) >= 0 ) {
-                        baos.write(buffer,0,length);
-                    }   // end while: reading input
-
-                    // No error? Get new bytes.
-                    bytes = baos.toByteArray();
-
-                }   // end try
-                /* jdr
-                catch( java.io.IOException e ) {
-                    e.printStackTrace();
-                    // Just return originally-decoded bytes
-                }   // end catch
-                */
-                finally {
-                    try{ baos.close(); } catch( Exception e ){}
-                    try{ gzis.close(); } catch( Exception e ){}
-                    try{ bais.close(); } catch( Exception e ){}
-                }   // end finally
-
-            }   // end if: gzipped
-        }   // end if: bytes.length >= 2
-
-        return bytes;
-    }   // end decode
-
-
-
-    /**
-     * Attempts to decode Base64Util data and deserialize a Java
-     * Object within. Returns <tt>null</tt> if there was an error.
-     *
-     * @param encodedObject The Base64Util data to decode
-     * @return The decoded and deserialized object
-     * @throws NullPointerException if encodedObject is null
-     * @throws java.io.IOException if there is a general error
-     * @throws ClassNotFoundException if the decoded object is of a
-     *         class that cannot be found by the JVM
-     * @since 1.5
-     */
-    public static Object decodeToObject( String encodedObject )
-            throws java.io.IOException, java.lang.ClassNotFoundException {
-        return decodeToObject(encodedObject,NO_OPTIONS,null);
-    }
-
-
-    /**
-     * Attempts to decode Base64Util data and deserialize a Java
-     * Object within. Returns <tt>null</tt> if there was an error.
-     * If <tt>loader</tt> is not null, it will be the class loader
-     * used when deserializing.
-     *
-     * @param encodedObject The Base64Util data to decode
-     * @param options Various parameters related to decoding
-     * @param loader Optional class loader to use in deserializing classes.
-     * @return The decoded and deserialized object
-     * @throws NullPointerException if encodedObject is null
-     * @throws java.io.IOException if there is a general error
-     * @throws ClassNotFoundException if the decoded object is of a
-     *         class that cannot be found by the JVM
-     * @since 2.3.4
-     */
-    public static Object decodeToObject(
-            String encodedObject, int options, final ClassLoader loader )
-            throws java.io.IOException, java.lang.ClassNotFoundException {
-
-        // Decode and gunzip if necessary
-        byte[] objBytes = decode( encodedObject, options );
-
-        java.io.ByteArrayInputStream  bais = null;
-        java.io.ObjectInputStream     ois  = null;
-        Object obj = null;
-
-        try {
-            bais = new java.io.ByteArrayInputStream( objBytes );
-
-            // If no custom class loader is provided, use Java's builtin OIS.
-            if( loader == null ){
-                ois  = new java.io.ObjectInputStream( bais );
-            }   // end if: no loader provided
-
-            // Else make a customized object input stream that uses
-            // the provided class loader.
-            else {
-                ois = new java.io.ObjectInputStream(bais){
-                    @Override
-                    public Class<?> resolveClass(java.io.ObjectStreamClass streamClass)
-                            throws java.io.IOException, ClassNotFoundException {
-                        Class c = Class.forName(streamClass.getName(), false, loader);
-                        if( c == null ){
-                            return super.resolveClass(streamClass);
-                        } else {
-                            return c;   // Class loader knows of this class.
-                        }   // end else: not null
-                    }   // end resolveClass
-                };  // end ois
-            }   // end else: no custom class loader
-
-            obj = ois.readObject();
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e;    // Catch and throw in order to execute finally{}
-        }   // end catch
-        catch( java.lang.ClassNotFoundException e ) {
-            throw e;    // Catch and throw in order to execute finally{}
-        }   // end catch
-        finally {
-            try{ bais.close(); } catch( Exception e ){}
-            try{ ois.close();  } catch( Exception e ){}
-        }   // end finally
-
-        return obj;
-    }   // end decodeObject
-
-
-
-    /**
-     * Convenience method for encoding data to a file.
-     *
-     * <p>As of v 2.3, if there is a error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned false, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * @param dataToEncode byte array of data to encode in base64 form
-     * @param filename Filename for saving encoded data
-     * @throws java.io.IOException if there is an error
-     * @throws NullPointerException if dataToEncode is null
-     * @since 2.1
-     */
-    public static void encodeToFile( byte[] dataToEncode, String filename )
-            throws java.io.IOException {
-
-        if( dataToEncode == null ){
-            throw new NullPointerException( "Data to encode was null." );
-        }   // end iff
-
-        Base64Util.OutputStream bos = null;
-        try {
-            bos = new Base64Util.OutputStream(
-                    new java.io.FileOutputStream( filename ), Base64Util.ENCODE );
-            bos.write( dataToEncode );
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and throw to execute finally{} block
-        }   // end catch: java.io.IOException
-        finally {
-            try{ bos.close(); } catch( Exception e ){}
-        }   // end finally
-
-    }   // end encodeToFile
-
-
-    /**
-     * Convenience method for decoding data to a file.
-     *
-     * <p>As of v 2.3, if there is a error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned false, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * @param dataToDecode Base64Util-encoded data as a string
-     * @param filename Filename for saving decoded data
-     * @throws java.io.IOException if there is an error
-     * @since 2.1
-     */
-    public static void decodeToFile( String dataToDecode, String filename )
-            throws java.io.IOException {
-
-        Base64Util.OutputStream bos = null;
-        try{
-            bos = new Base64Util.OutputStream(
-                    new java.io.FileOutputStream( filename ), Base64Util.DECODE );
-            bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and throw to execute finally{} block
-        }   // end catch: java.io.IOException
-        finally {
-            try{ bos.close(); } catch( Exception e ){}
-        }   // end finally
-
-    }   // end decodeToFile
-
-
-
-
-    /**
-     * Convenience method for reading a base64-encoded
-     * file and decoding it.
-     *
-     * <p>As of v 2.3, if there is a error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned false, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * @param filename Filename for reading encoded data
-     * @return decoded byte array
-     * @throws java.io.IOException if there is an error
-     * @since 2.1
-     */
-    public static byte[] decodeFromFile( String filename )
-            throws java.io.IOException {
-
-        byte[] decodedData = null;
-        Base64Util.InputStream bis = null;
-        try
-        {
-            // Set up some useful variables
-            java.io.File file = new java.io.File( filename );
-            byte[] buffer = null;
-            int length   = 0;
-            int numBytes = 0;
-
-            // Check for size of file
-            if( file.length() > Integer.MAX_VALUE )
-            {
-                throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." );
-            }   // end if: file too big for int index
-            buffer = new byte[ (int)file.length() ];
-
-            // Open a stream
-            bis = new Base64Util.InputStream(
-                    new java.io.BufferedInputStream(
-                            new java.io.FileInputStream( file ) ), Base64Util.DECODE );
-
-            // Read until done
-            while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
-                length += numBytes;
-            }   // end while
-
-            // Save in a variable to return
-            decodedData = new byte[ length ];
-            System.arraycopy( buffer, 0, decodedData, 0, length );
-
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and release to execute finally{}
-        }   // end catch: java.io.IOException
-        finally {
-            try{ bis.close(); } catch( Exception e) {}
-        }   // end finally
-
-        return decodedData;
-    }   // end decodeFromFile
-
-
-
-    /**
-     * Convenience method for reading a binary file
-     * and base64-encoding it.
-     *
-     * <p>As of v 2.3, if there is a error,
-     * the method will throw an java.io.IOException. <b>This is new to v2.3!</b>
-     * In earlier versions, it just returned false, but
-     * in retrospect that's a pretty poor way to handle it.</p>
-     *
-     * @param filename Filename for reading binary data
-     * @return base64-encoded string
-     * @throws java.io.IOException if there is an error
-     * @since 2.1
-     */
-    public static String encodeFromFile( String filename )
-            throws java.io.IOException {
-
-        String encodedData = null;
-        Base64Util.InputStream bis = null;
-        try
-        {
-            // Set up some useful variables
-            java.io.File file = new java.io.File( filename );
-            byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5)
-            int length   = 0;
-            int numBytes = 0;
-
-            // Open a stream
-            bis = new Base64Util.InputStream(
-                    new java.io.BufferedInputStream(
-                            new java.io.FileInputStream( file ) ), Base64Util.ENCODE );
-
-            // Read until done
-            while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
-                length += numBytes;
-            }   // end while
-
-            // Save in a variable to return
-            encodedData = new String( buffer, 0, length, Base64Util.PREFERRED_ENCODING );
-
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and release to execute finally{}
-        }   // end catch: java.io.IOException
-        finally {
-            try{ bis.close(); } catch( Exception e) {}
-        }   // end finally
-
-        return encodedData;
-    }   // end encodeFromFile
-
-    /**
-     * Reads <tt>infile</tt> and encodes it to <tt>outfile</tt>.
-     *
-     * @param infile Input file
-     * @param outfile Output file
-     * @throws java.io.IOException if there is an error
-     * @since 2.2
-     */
-    public static void encodeFileToFile( String infile, String outfile )
-            throws java.io.IOException {
-
-        String encoded = Base64Util.encodeFromFile(infile);
-        java.io.OutputStream out = null;
-        try{
-            out = new java.io.BufferedOutputStream(
-                    new java.io.FileOutputStream( outfile ) );
-            out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and release to execute finally{}
-        }   // end catch
-        finally {
-            try { out.close(); }
-            catch( Exception ex ){}
-        }   // end finally
-    }   // end encodeFileToFile
-
-
-    /**
-     * Reads <tt>infile</tt> and decodes it to <tt>outfile</tt>.
-     *
-     * @param infile Input file
-     * @param outfile Output file
-     * @throws java.io.IOException if there is an error
-     * @since 2.2
-     */
-    public static void decodeFileToFile( String infile, String outfile )
-            throws java.io.IOException {
-
-        byte[] decoded = Base64Util.decodeFromFile(infile);
-        java.io.OutputStream out = null;
-        try{
-            out = new java.io.BufferedOutputStream(
-                    new java.io.FileOutputStream( outfile ) );
-            out.write( decoded );
-        }   // end try
-        catch( java.io.IOException e ) {
-            throw e; // Catch and release to execute finally{}
-        }   // end catch
-        finally {
-            try { out.close(); }
-            catch( Exception ex ){}
-        }   // end finally
-    }   // end decodeFileToFile
-
-
-    /* ********  I N N E R   C L A S S   I N P U T S T R E A M  ******** */
-
-
-
-    /**
-     * A {@link Base64Util.InputStream} will read data from another
-     * <tt>java.io.InputStream</tt>, given in the constructor,
-     * and encode/decode to/from Base64Util notation on the fly.
-     *
-     * @see Base64Util
-     * @since 1.3
-     */
-    public static class InputStream extends java.io.FilterInputStream {
-
-        private boolean encode;         // Encoding or decoding
-        private int     position;       // Current position in the buffer
-        private byte[]  buffer;         // Small buffer holding converted data
-        private int     bufferLength;   // Length of buffer (3 or 4)
-        private int     numSigBytes;    // Number of meaningful bytes in the buffer
-        private int     lineLength;
-        private boolean breakLines;     // Break lines at less than 80 characters
-        private int     options;        // Record options used to create the stream.
-        private byte[]  decodabet;      // Local copies to avoid extra method calls
-
-
-        /**
-         * Constructs a {@link Base64Util.InputStream} in DECODE mode.
-         *
-         * @param in the <tt>java.io.InputStream</tt> from which to read data.
-         * @since 1.3
-         */
-        public InputStream( java.io.InputStream in ) {
-            this( in, DECODE );
-        }   // end constructor
-
-
-        /**
-         * Constructs a {@link Base64Util.InputStream} in
-         * either ENCODE or DECODE mode.
-         * <p>
-         * Valid options:<pre>
-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: break lines at 76 characters
-         *     (only meaningful when encoding)</i>
-         * </pre>
-         * <p>
-         * Example: <code>new Base64Util.InputStream( in, Base64Util.DECODE )</code>
-         *
-         *
-         * @param in the <tt>java.io.InputStream</tt> from which to read data.
-         * @param options Specified options
-         * @see Base64Util#ENCODE
-         * @see Base64Util#DECODE
-         * @see Base64Util#DO_BREAK_LINES
-         * @since 2.0
-         */
-        public InputStream( java.io.InputStream in, int options ) {
-
-            super( in );
-            this.options      = options; // Record for later
-            this.breakLines   = (options & DO_BREAK_LINES) > 0;
-            this.encode       = (options & ENCODE) > 0;
-            this.bufferLength = encode ? 4 : 3;
-            this.buffer       = new byte[ bufferLength ];
-            this.position     = -1;
-            this.lineLength   = 0;
-            this.decodabet    = getDecodabet(options);
-        }   // end constructor
-
-        /**
-         * Reads enough of the input stream to convert
-         * to/from Base64Util and returns the next byte.
-         *
-         * @return next byte
-         * @since 1.3
-         */
-        @Override
-        public int read() throws java.io.IOException  {
-
-            // Do we need to read data?
-            if( position < 0 ) {
-                if( encode ) {
-                    byte[] b3 = new byte[3];
-                    int numBinaryBytes = 0;
-                    for( int i = 0; i < 3; i++ ) {
-                        int b = in.read();
-
-                        // If end of stream, b is -1.
-                        if( b >= 0 ) {
-                            b3[i] = (byte)b;
-                            numBinaryBytes++;
-                        } else {
-                            break; // out of for loop
-                        }   // end else: end of stream
-
-                    }   // end for: each needed input byte
-
-                    if( numBinaryBytes > 0 ) {
-                        encode3to4( b3, 0, numBinaryBytes, buffer, 0, options );
-                        position = 0;
-                        numSigBytes = 4;
-                    }   // end if: got data
-                    else {
-                        return -1;  // Must be end of stream
-                    }   // end else
-                }   // end if: encoding
-
-                // Else decoding
-                else {
-                    byte[] b4 = new byte[4];
-                    int i = 0;
-                    for( i = 0; i < 4; i++ ) {
-                        // Read four "meaningful" bytes:
-                        int b = 0;
-                        do{ b = in.read(); }
-                        while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC );
-
-                        if( b < 0 ) {
-                            break; // Reads a -1 if end of stream
-                        }   // end if: end of stream
-
-                        b4[i] = (byte)b;
-                    }   // end for: each needed input byte
-
-                    if( i == 4 ) {
-                        numSigBytes = decode4to3( b4, 0, buffer, 0, options );
-                        position = 0;
-                    }   // end if: got four characters
-                    else if( i == 0 ){
-                        return -1;
-                    }   // end else if: also padded correctly
-                    else {
-                        // Must have broken out from above.
-                        throw new java.io.IOException( "Improperly padded Base64Util input." );
-                    }   // end
-
-                }   // end else: decode
-            }   // end else: read data
-
-            // Got data?
-            if( position >= 0 ) {
-                // End of relevant data?
-                if( /*!encode &&*/ position >= numSigBytes ){
-                    return -1;
-                }   // end if: got data
-
-                if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) {
-                    lineLength = 0;
-                    return '\n';
-                }   // end if
-                else {
-                    lineLength++;   // This isn't important when decoding
-                    // but throwing an extra "if" seems
-                    // just as wasteful.
-
-                    int b = buffer[ position++ ];
-
-                    if( position >= bufferLength ) {
-                        position = -1;
-                    }   // end if: end
-
-                    return b & 0xFF; // This is how you "cast" a byte that's
-                    // intended to be unsigned.
-                }   // end else
-            }   // end if: position >= 0
-
-            // Else error
-            else {
-                throw new java.io.IOException( "Error in Base64Util code reading stream." );
-            }   // end else
-        }   // end read
-
-
-        /**
-         * Calls {@link #read()} repeatedly until the end of stream
-         * is reached or <var>len</var> bytes are read.
-         * Returns number of bytes read into array or -1 if
-         * end of stream is encountered.
-         *
-         * @param dest array to hold values
-         * @param off offset for array
-         * @param len max number of bytes to read into array
-         * @return bytes read into array or -1 if end of stream is encountered.
-         * @since 1.3
-         */
-        @Override
-        public int read( byte[] dest, int off, int len )
-                throws java.io.IOException {
-            int i;
-            int b;
-            for( i = 0; i < len; i++ ) {
-                b = read();
-
-                if( b >= 0 ) {
-                    dest[off + i] = (byte) b;
-                }
-                else if( i == 0 ) {
-                    return -1;
-                }
-                else {
-                    break; // Out of 'for' loop
-                } // Out of 'for' loop
-            }   // end for: each byte read
-            return i;
-        }   // end read
-
-    }   // end inner class InputStream
-
-
-
-
-
-
-    /* ********  I N N E R   C L A S S   O U T P U T S T R E A M  ******** */
-
-
-
-    /**
-     * A {@link Base64Util.OutputStream} will write data to another
-     * <tt>java.io.OutputStream</tt>, given in the constructor,
-     * and encode/decode to/from Base64Util notation on the fly.
-     *
-     * @see Base64Util
-     * @since 1.3
-     */
-    public static class OutputStream extends java.io.FilterOutputStream {
-
-        private boolean encode;
-        private int     position;
-        private byte[]  buffer;
-        private int     bufferLength;
-        private int     lineLength;
-        private boolean breakLines;
-        private byte[]  b4;         // Scratch used in a few places
-        private boolean suspendEncoding;
-        private int     options;    // Record for later
-        private byte[]  decodabet;  // Local copies to avoid extra method calls
-
-        /**
-         * Constructs a {@link Base64Util.OutputStream} in ENCODE mode.
-         *
-         * @param out the <tt>java.io.OutputStream</tt> to which data will be written.
-         * @since 1.3
-         */
-        public OutputStream( java.io.OutputStream out ) {
-            this( out, ENCODE );
-        }   // end constructor
-
-
-        /**
-         * Constructs a {@link Base64Util.OutputStream} in
-         * either ENCODE or DECODE mode.
-         * <p>
-         * Valid options:<pre>
-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)</i>
-         * </pre>
-         * <p>
-         * Example: <code>new Base64Util.OutputStream( out, Base64Util.ENCODE )</code>
-         *
-         * @param out the <tt>java.io.OutputStream</tt> to which data will be written.
-         * @param options Specified options.
-         * @see Base64Util#ENCODE
-         * @see Base64Util#DECODE
-         * @see Base64Util#DO_BREAK_LINES
-         * @since 1.3
-         */
-        public OutputStream( java.io.OutputStream out, int options ) {
-            super( out );
-            this.breakLines   = (options & DO_BREAK_LINES) != 0;
-            this.encode       = (options & ENCODE) != 0;
-            this.bufferLength = encode ? 3 : 4;
-            this.buffer       = new byte[ bufferLength ];
-            this.position     = 0;
-            this.lineLength   = 0;
-            this.suspendEncoding = false;
-            this.b4           = new byte[4];
-            this.options      = options;
-            this.decodabet    = getDecodabet(options);
-        }   // end constructor
-
-
-        /**
-         * Writes the byte to the output stream after
-         * converting to/from Base64Util notation.
-         * When encoding, bytes are buffered three
-         * at a time before the output stream actually
-         * gets a write() call.
-         * When decoding, bytes are buffered four
-         * at a time.
-         *
-         * @param theByte the byte to write
-         * @since 1.3
-         */
-        @Override
-        public void write(int theByte)
-                throws java.io.IOException {
-            // Encoding suspended?
-            if( suspendEncoding ) {
-                this.out.write( theByte );
-                return;
-            }   // end if: supsended
-
-            // Encode?
-            if( encode ) {
-                buffer[ position++ ] = (byte)theByte;
-                if( position >= bufferLength ) { // Enough to encode.
-
-                    this.out.write( encode3to4( b4, buffer, bufferLength, options ) );
-
-                    lineLength += 4;
-                    if( breakLines && lineLength >= MAX_LINE_LENGTH ) {
-                        this.out.write( NEW_LINE );
-                        lineLength = 0;
-                    }   // end if: end of line
-
-                    position = 0;
-                }   // end if: enough to output
-            }   // end if: encoding
-
-            // Else, Decoding
-            else {
-                // Meaningful Base64Util character?
-                if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) {
-                    buffer[ position++ ] = (byte)theByte;
-                    if( position >= bufferLength ) { // Enough to output.
-
-                        int len = Base64Util.decode4to3(buffer, 0, b4, 0, options);
-                        out.write( b4, 0, len );
-                        position = 0;
-                    }   // end if: enough to output
-                }   // end if: meaningful base64 character
-                else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) {
-                    throw new java.io.IOException( "Invalid character in Base64Util data." );
-                }   // end else: not white space either
-            }   // end else: decoding
-        }   // end write
-
-
-
-        /**
-         * Calls {@link #write(int)} repeatedly until <var>len</var>
-         * bytes are written.
-         *
-         * @param theBytes array from which to read bytes
-         * @param off offset for array
-         * @param len max number of bytes to read into array
-         * @since 1.3
-         */
-        @Override
-        public void write( byte[] theBytes, int off, int len )
-                throws java.io.IOException {
-            // Encoding suspended?
-            if( suspendEncoding ) {
-                this.out.write( theBytes, off, len );
-                return;
-            }   // end if: supsended
-
-            for( int i = 0; i < len; i++ ) {
-                write( theBytes[ off + i ] );
-            }   // end for: each byte written
-
-        }   // end write
-
-
-
-        /**
-         * Method added by PHIL. [Thanks, PHIL. -Rob]
-         * This pads the buffer without closing the stream.
-         * @throws java.io.IOException  if there's an error.
-         */
-        public void flushBase64() throws java.io.IOException  {
-            if( position > 0 ) {
-                if( encode ) {
-                    out.write( encode3to4( b4, buffer, position, options ) );
-                    position = 0;
-                }   // end if: encoding
-                else {
-                    throw new java.io.IOException( "Base64Util input not properly padded." );
-                }   // end else: decoding
-            }   // end if: buffer partially full
-
-        }   // end flush
-
-
-        /**
-         * Flushes and closes (I think, in the superclass) the stream.
-         *
-         * @since 1.3
-         */
-        @Override
-        public void close() throws java.io.IOException {
-            // 1. Ensure that pending characters are written
-            flushBase64();
-
-            // 2. Actually close the stream
-            // Base class both flushes and closes.
-            super.close();
-
-            buffer = null;
-            out    = null;
-        }   // end close
-
-
-
-        /**
-         * Suspends encoding of the stream.
-         * May be helpful if you need to embed a piece of
-         * base64-encoded data in a stream.
-         *
-         * @throws java.io.IOException  if there's an error flushing
-         * @since 1.5.1
-         */
-        public void suspendEncoding() throws java.io.IOException  {
-            flushBase64();
-            this.suspendEncoding = true;
-        }   // end suspendEncoding
-
-
-        /**
-         * Resumes encoding of the stream.
-         * May be helpful if you need to embed a piece of
-         * base64-encoded data in a stream.
-         *
-         * @since 1.5.1
-         */
-        public void resumeEncoding() {
-            this.suspendEncoding = false;
-        }   // end resumeEncoding
-
-
-
-    }   // end inner class OutputStream
-
-
-}   // end class Base64Util

+ 8 - 8
src/main/java/password/pwm/util/StringUtil.java

@@ -23,9 +23,9 @@
 package password.pwm.util;
 package password.pwm.util;
 
 
 import org.apache.commons.codec.binary.Base32;
 import org.apache.commons.codec.binary.Base32;
+import net.iharder.Base64;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
-
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
@@ -153,10 +153,10 @@ public abstract class StringUtil {
             optionsEnum.addAll(Arrays.asList(options));
             optionsEnum.addAll(Arrays.asList(options));
 
 
             if (optionsEnum.contains(Base64Options.GZIP)) {
             if (optionsEnum.contains(Base64Options.GZIP)) {
-                b64UtilOptions = b64UtilOptions | Base64Util.GZIP;
+                b64UtilOptions = b64UtilOptions | Base64.GZIP;
             }
             }
             if (optionsEnum.contains(Base64Options.URL_SAFE)) {
             if (optionsEnum.contains(Base64Options.URL_SAFE)) {
-                b64UtilOptions = b64UtilOptions | Base64Util.URL_SAFE;
+                b64UtilOptions = b64UtilOptions | Base64.URL_SAFE;
             }
             }
             return b64UtilOptions;
             return b64UtilOptions;
         }
         }
@@ -207,7 +207,7 @@ public abstract class StringUtil {
     public static byte[] base64Decode(final String input)
     public static byte[] base64Decode(final String input)
             throws IOException
             throws IOException
     {
     {
-        return Base64Util.decode(input);
+        return Base64.decode(input);
     }
     }
 
 
     public static String base32Encode(final byte[] input)
     public static String base32Encode(final byte[] input)
@@ -222,12 +222,12 @@ public abstract class StringUtil {
     {
     {
         final int b64UtilOptions = Base64Options.asBase64UtilOptions(options);
         final int b64UtilOptions = Base64Options.asBase64UtilOptions(options);
 
 
-        return Base64Util.decode(input, b64UtilOptions);
+        return Base64.decode(input, b64UtilOptions);
     }
     }
 
 
     public static String base64Encode(final byte[] input)
     public static String base64Encode(final byte[] input)
     {
     {
-        return Base64Util.encodeBytes(input);
+        return Base64.encodeBytes(input);
     }
     }
 
 
     public static String base64Encode(final byte[] input, final StringUtil.Base64Options... options)
     public static String base64Encode(final byte[] input, final StringUtil.Base64Options... options)
@@ -236,9 +236,9 @@ public abstract class StringUtil {
         final int b64UtilOptions = Base64Options.asBase64UtilOptions(options);
         final int b64UtilOptions = Base64Options.asBase64UtilOptions(options);
 
 
         if (b64UtilOptions > 0) {
         if (b64UtilOptions > 0) {
-            return Base64Util.encodeBytes(input, b64UtilOptions);
+            return Base64.encodeBytes(input, b64UtilOptions);
         } else {
         } else {
-            return Base64Util.encodeBytes(input);
+            return Base64.encodeBytes(input);
         }
         }
     }
     }
 
 

+ 3 - 3
src/main/java/password/pwm/util/logging/PwmLogEvent.java

@@ -22,9 +22,9 @@
 
 
 package password.pwm.util.logging;
 package password.pwm.util.logging;
 
 
+import net.iharder.Base64;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
-import password.pwm.util.Base64Util;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.StringUtil;
 import password.pwm.util.StringUtil;
 
 
@@ -86,7 +86,7 @@ public class PwmLogEvent implements Serializable, Comparable {
 
 
         Throwable throwable = null;
         Throwable throwable = null;
         if (srcMap.containsKey(KEY_THROWABLE)) {
         if (srcMap.containsKey(KEY_THROWABLE)) {
-            throwable = (Throwable) Base64Util.decodeToObject(srcMap.get(KEY_THROWABLE));
+            throwable = (Throwable) Base64.decodeToObject(srcMap.get(KEY_THROWABLE));
         }
         }
 
 
         PwmLogLevel level = null;
         PwmLogLevel level = null;
@@ -300,7 +300,7 @@ public class PwmLogEvent implements Serializable, Comparable {
         }
         }
 
 
         if (throwable != null) {
         if (throwable != null) {
-            tempMap.put(KEY_THROWABLE, Base64Util.encodeObject(throwable, Base64Util.NO_OPTIONS));
+            tempMap.put(KEY_THROWABLE, Base64.encodeObject(throwable, Base64.NO_OPTIONS));
         }
         }
 
 
         return JsonUtil.serializeMap(tempMap);
         return JsonUtil.serializeMap(tempMap);

+ 8 - 10
src/main/java/password/pwm/util/operations/cr/NMASCrOperator.java

@@ -132,7 +132,7 @@ public class NMASCrOperator implements CrOperator {
     }
     }
 
 
     public void close() {
     public void close() {
-        final List<NMASSessionThread> threads = new ArrayList(sessionMonitorThreads);
+        final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads);
         for (final NMASSessionThread thread : threads) {
         for (final NMASSessionThread thread : threads) {
             LOGGER.debug("killing thread due to NMASCrOperator service closing: " + thread.toDebugString());
             LOGGER.debug("killing thread due to NMASCrOperator service closing: " + thread.toDebugString());
             thread.abort();
             thread.abort();
@@ -146,7 +146,6 @@ public class NMASCrOperator implements CrOperator {
     )
     )
             throws PwmUnrecoverableException
             throws PwmUnrecoverableException
     {
     {
-        final String userDN = theUser.getEntryDN();
         pwmApplication.getIntruderManager().convenience().checkUserIdentity(userIdentity);
         pwmApplication.getIntruderManager().convenience().checkUserIdentity(userIdentity);
 
 
         try {
         try {
@@ -657,7 +656,7 @@ public class NMASCrOperator implements CrOperator {
                             this.loginDN,
                             this.loginDN,
                             "dn:" + this.loginDN,
                             "dn:" + this.loginDN,
                             new String[] { "NMAS_LOGIN" },
                             new String[] { "NMAS_LOGIN" },
-                            new HashMap(CR_OPTIONS_MAP),
+                            new HashMap<>(CR_OPTIONS_MAP),
                             this.callbackHandler
                             this.callbackHandler
                     );
                     );
                 } catch (NullPointerException e) {
                 } catch (NullPointerException e) {
@@ -726,8 +725,8 @@ public class NMASCrOperator implements CrOperator {
     private class ThreadWatchdogTask extends TimerTask {
     private class ThreadWatchdogTask extends TimerTask {
         @Override
         @Override
         public void run() {
         public void run() {
-            //logThreadInfo();
-            final List<NMASSessionThread> threads = new ArrayList(sessionMonitorThreads);
+            logThreadInfo();
+            final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads);
             for (final NMASSessionThread thread : threads) {
             for (final NMASSessionThread thread : threads) {
                 final TimeDuration idleTime = TimeDuration.fromCurrent(thread.getLastActivityTimestamp());
                 final TimeDuration idleTime = TimeDuration.fromCurrent(thread.getLastActivityTimestamp());
                 if (idleTime.isLongerThan(maxThreadIdleTime)) {
                 if (idleTime.isLongerThan(maxThreadIdleTime)) {
@@ -737,9 +736,9 @@ public class NMASCrOperator implements CrOperator {
             }
             }
         }
         }
 
 
-        /*
+
         private void logThreadInfo() {
         private void logThreadInfo() {
-            final List<NMASSessionThread> threads = new ArrayList(sessionMonitorThreads);
+            final List<NMASSessionThread> threads = new ArrayList<>(sessionMonitorThreads);
             final StringBuilder threadDebugInfo = new StringBuilder();
             final StringBuilder threadDebugInfo = new StringBuilder();
             threadDebugInfo.append("NMASCrOperator watchdog timer, activeCount=").append(threads.size());
             threadDebugInfo.append("NMASCrOperator watchdog timer, activeCount=").append(threads.size());
             threadDebugInfo.append(", maxIdleThreadTime=").append(maxThreadIdleTime.asCompactString());
             threadDebugInfo.append(", maxIdleThreadTime=").append(maxThreadIdleTime.asCompactString());
@@ -748,8 +747,7 @@ public class NMASCrOperator implements CrOperator {
             }
             }
             LOGGER.trace(threadDebugInfo.toString());
             LOGGER.trace(threadDebugInfo.toString());
         }
         }
-        */
-    }
-
 
 
+    }
 }
 }
+

+ 5 - 5
src/main/java/password/pwm/ws/client/rest/naaf/NAAFLoginSequence.java

@@ -22,7 +22,7 @@
 
 
 package password.pwm.ws.client.rest.naaf;
 package password.pwm.ws.client.rest.naaf;
 
 
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -178,14 +178,14 @@ public class NAAFLoginSequence {
         }
         }
     }
     }
 
 
-    public RecoveryVerificationMethod.VerificationState status() {
+    public VerificationMethodSystem.VerificationState status() {
         if (lastError != null) {
         if (lastError != null) {
-            return RecoveryVerificationMethod.VerificationState.FAILED;
+            return VerificationMethodSystem.VerificationState.FAILED;
         }
         }
         if (completedMethods.containsAll(requiredMethods)) {
         if (completedMethods.containsAll(requiredMethods)) {
-            return RecoveryVerificationMethod.VerificationState.COMPLETE;
+            return VerificationMethodSystem.VerificationState.COMPLETE;
         }
         }
-        return RecoveryVerificationMethod.VerificationState.INPROGRESS;
+        return VerificationMethodSystem.VerificationState.INPROGRESS;
     }
     }
 
 
 
 

+ 2 - 2
src/main/java/password/pwm/ws/client/rest/naaf/PwmNAAFVerificationMethod.java

@@ -24,7 +24,7 @@ package password.pwm.ws.client.rest.naaf;
 
 
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.PwmConstants;
-import password.pwm.RecoveryVerificationMethod;
+import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
@@ -37,7 +37,7 @@ import password.pwm.util.macro.MacroMachine;
 
 
 import java.util.*;
 import java.util.*;
 
 
-public class PwmNAAFVerificationMethod implements RecoveryVerificationMethod {
+public class PwmNAAFVerificationMethod implements VerificationMethodSystem {
     private PwmApplication pwmApplication;
     private PwmApplication pwmApplication;
     private NAAFLoginSequence naafLoginSequence;
     private NAAFLoginSequence naafLoginSequence;
     private Locale locale;
     private Locale locale;

+ 3 - 1
src/main/resources/password/pwm/AppProperty.properties

@@ -65,8 +65,10 @@ health.certificate.warnSeconds=2592000
 health.ldap.cautionDurationMS=10800000
 health.ldap.cautionDurationMS=10800000
 health.java.maxThreads=1000
 health.java.maxThreads=1000
 health.java.minHeapBytes=67108864
 health.java.minHeapBytes=67108864
-helpdesk.token.maxAgeSeconds=120
+helpdesk.token.maxAgeSeconds=300
 helpdesk.token.value=@RandomChar:6:0123456789@
 helpdesk.token.value=@RandomChar:6:0123456789@
+helpdesk.verification.invalid.delayMs=2000
+helpdesk.verification.timeoutSeconds=3600
 http.resources.maxCacheItems=500
 http.resources.maxCacheItems=500
 http.resources.maxCacheBytes=500000
 http.resources.maxCacheBytes=500000
 http.resources.expirationSeconds=30240000
 http.resources.expirationSeconds=30240000

+ 11 - 10
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -1469,7 +1469,9 @@
             <value>HELPDESK_VIEW_DETAIL</value>
             <value>HELPDESK_VIEW_DETAIL</value>
             <value>HELPDESK_ACTION</value>
             <value>HELPDESK_ACTION</value>
             <value>HELPDESK_VERIFY_OTP</value>
             <value>HELPDESK_VERIFY_OTP</value>
+            <value>HELPDESK_VERIFY_OTP_INCORRECT</value>
             <value>HELPDESK_VERIFY_TOKEN</value>
             <value>HELPDESK_VERIFY_TOKEN</value>
+            <value>HELPDESK_VERIFY_TOKEN_INCORRECT</value>
         </default>
         </default>
         <options>
         <options>
             <option value="AUTHENTICATE">Authenticate</option>
             <option value="AUTHENTICATE">Authenticate</option>
@@ -1494,7 +1496,9 @@
             <option value="HELPDESK_VIEW_DETAIL">Helpdesk View Detail</option>
             <option value="HELPDESK_VIEW_DETAIL">Helpdesk View Detail</option>
             <option value="HELPDESK_ACTION">Helpdesk Action</option>
             <option value="HELPDESK_ACTION">Helpdesk Action</option>
             <option value="HELPDESK_VERIFY_OTP">Helpdesk Verify OTP</option>
             <option value="HELPDESK_VERIFY_OTP">Helpdesk Verify OTP</option>
+            <option value="HELPDESK_VERIFY_OTP_INCORRECT">Helpdesk Incorrect Verify OTP</option>
             <option value="HELPDESK_VERIFY_TOKEN">Helpdesk Verify Token</option>
             <option value="HELPDESK_VERIFY_TOKEN">Helpdesk Verify Token</option>
+            <option value="HELPDESK_VERIFY_TOKEN_INCORRECT">Helpdesk Incorrect Verify Token</option>
         </options>
         </options>
     </setting>
     </setting>
     <setting hidden="false" key="audit.syslog.servers" level="1" required="false">
     <setting hidden="false" key="audit.syslog.servers" level="1" required="false">
@@ -3144,25 +3148,17 @@
             <option value="NONE">None - Token verification will not be available</option>
             <option value="NONE">None - Token verification will not be available</option>
             <option value="EMAILONLY">Email Only - Send to email address</option>
             <option value="EMAILONLY">Email Only - Send to email address</option>
             <option value="SMSONLY">SMS Only - Send via SMS</option>
             <option value="SMSONLY">SMS Only - Send via SMS</option>
-            <option value="BOTH">Both - Send token to both email and SMS</option>
-            <option value="EMAILFIRST">Email First - Try to send token via email; if no email address is available, send via SMS</option>
-            <option value="SMSFIRST">SMS First - Try to send token via SMS; if no SMS number is available, send via email</option>
             <option value="CHOICE_SMS_EMAIL">Operator Choice - If both SMS and email address is available, helpdesk operator decides</option>
             <option value="CHOICE_SMS_EMAIL">Operator Choice - If both SMS and email address is available, helpdesk operator decides</option>
         </options>
         </options>
     </setting>
     </setting>
-    <setting hidden="false" key="helpdesk.otp.verify" level="1">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="false" key="helpdesk.setPassword.maskValue" level="1">
     <setting hidden="false" key="helpdesk.setPassword.maskValue" level="1">
         <default>
         <default>
             <value>false</value>
             <value>false</value>
         </default>
         </default>
     </setting>
     </setting>
-    <setting hidden="true" key="helpdesk.verificationMethods" level="1">
+    <setting hidden="false" key="helpdesk.verificationMethods" level="1">
         <default>
         <default>
-            <value>{"methodSettings":{"CHALLENGE_RESPONSES":{"enabledState":"required"}},"minOptionalRequired":0}</value>
+            <value>{"methodSettings":{},"minOptionalRequired":0}</value>
         </default>
         </default>
         <options>
         <options>
             <!--<option value="ATTRIBUTES">LDAP Attributes</option>-->
             <!--<option value="ATTRIBUTES">LDAP Attributes</option>-->
@@ -3471,6 +3467,11 @@
             <value>true</value>
             <value>true</value>
         </default>
         </default>
     </setting>
     </setting>
+    <setting hidden="true" key="helpdesk.otp.verify" level="1">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <category hidden="false" key="TEMPLATES">
     <category hidden="false" key="TEMPLATES">
     </category>
     </category>
     <category hidden="false" key="NOTES">
     <category hidden="false" key="NOTES">

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

@@ -45,6 +45,7 @@ Button_Login=Sign in
 Button_Logout=Sign out
 Button_Logout=Sign out
 Button_More=More
 Button_More=More
 Button_OrgChart=Organizational Chart
 Button_OrgChart=Organizational Chart
+Button_OTP=OTP
 Button_RecoverPassword=Check Answers
 Button_RecoverPassword=Check Answers
 Button_Reset=Clear
 Button_Reset=Clear
 Button_Search=Search
 Button_Search=Search
@@ -294,3 +295,4 @@ Value_False=False
 Value_True=True
 Value_True=True
 Value_NotApplicable=n/a
 Value_NotApplicable=n/a
 Value_Default=Default
 Value_Default=Default
+Placeholder_Search=Search

+ 2 - 0
src/main/resources/password/pwm/i18n/Message.properties

@@ -40,7 +40,9 @@ EventLog_HelpdeskUnlockPassword=Helpdesk Unlock Password
 EventLog_HelpdeskDeleteUser=Helpdesk Delete User
 EventLog_HelpdeskDeleteUser=Helpdesk Delete User
 EventLog_HelpdeskViewDetail=Helpdesk View Detail
 EventLog_HelpdeskViewDetail=Helpdesk View Detail
 EventLog_HelpdeskVerifyOtp=Helpdesk Verify OTP
 EventLog_HelpdeskVerifyOtp=Helpdesk Verify OTP
+EventLog_HelpdeskVerifyOtpIncorrect=Helpdesk Incorrect OTP Verify
 EventLog_HelpdeskVerifyToken=Helpdesk Verify Token
 EventLog_HelpdeskVerifyToken=Helpdesk Verify Token
+EventLog_HelpdeskVerifyTokenIncorrect=Helpdesk Incorrect Token Verify
 EventLog_IntruderUser=Intruder User Lockout
 EventLog_IntruderUser=Intruder User Lockout
 EventLog_TokenIssued=Token Issued
 EventLog_TokenIssued=Token Issued
 EventLog_TokenClaimed=Token Claimed
 EventLog_TokenClaimed=Token Claimed

+ 1 - 1
src/main/webapp/WEB-INF/jsp/configeditor.jsp

@@ -78,7 +78,7 @@
                         <span id="settingSearchIcon" class="pwm-icon pwm-icon-search" title="<pwm:display key="Tooltip_IconSettingsSearch" bundle="Config"/>"></span>
                         <span id="settingSearchIcon" class="pwm-icon pwm-icon-search" title="<pwm:display key="Tooltip_IconSettingsSearch" bundle="Config"/>"></span>
                     </td>
                     </td>
                     <td>
                     <td>
-                        <input type="search" id="homeSettingSearch" name="homeSettingSearch" class="inputfield" <pwm:autofocus/>/>
+                        <input placeholder="<pwm:display key="Placeholder_Search"/>" type="search" id="homeSettingSearch" name="homeSettingSearch" class="inputfield" <pwm:autofocus/>/>
                     </td>
                     </td>
                     <td>
                     <td>
                         <div style="margin-top:5px; width:20px; max-width: 20px;">
                         <div style="margin-top:5px; width:20px; max-width: 20px;">

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

@@ -1,4 +1,4 @@
-<%@ page import="password.pwm.config.option.RecoveryVerificationMethods" %>
+<%@ page import="password.pwm.config.option.IdentityVerificationMethod" %>
 <%@ page import="java.util.HashSet" %>
 <%@ page import="java.util.HashSet" %>
 <%@ page import="java.util.Set" %>
 <%@ page import="java.util.Set" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
@@ -29,7 +29,7 @@
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%
 <%
     final PwmRequest pwmRequest = PwmRequest.forRequest(request, response);
     final PwmRequest pwmRequest = PwmRequest.forRequest(request, response);
-    final Set<RecoveryVerificationMethods> methods = new HashSet<RecoveryVerificationMethods>((Set<RecoveryVerificationMethods>) JspUtility.getAttribute(pageContext, PwmRequest.Attribute.AvailableAuthMethods));
+    final Set<IdentityVerificationMethod> methods = new HashSet<IdentityVerificationMethod>((Set<IdentityVerificationMethod>) JspUtility.getAttribute(pageContext, PwmRequest.Attribute.AvailableAuthMethods));
 %>
 %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
 <%@ include file="fragment/header.jsp" %>
@@ -47,7 +47,7 @@
             <colgroup>
             <colgroup>
 
 
             </colgroup>
             </colgroup>
-            <% for (RecoveryVerificationMethods method : methods) { %>
+            <% for (IdentityVerificationMethod method : methods) { %>
             <% if (method.isUserSelectable()) { %>
             <% if (method.isUserSelectable()) { %>
             <tr>
             <tr>
                 <td>
                 <td>

+ 3 - 3
src/main/webapp/WEB-INF/jsp/forgottenpassword-naaf.jsp

@@ -22,7 +22,7 @@
 
 
 <!DOCTYPE html>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
-<%@ page import="password.pwm.RecoveryVerificationMethod" %>
+<%@ page import="password.pwm.VerificationMethodSystem" %>
 <%@ page import="java.util.List" %>
 <%@ page import="java.util.List" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -35,14 +35,14 @@
     </jsp:include>
     </jsp:include>
     <div id="centerbody">
     <div id="centerbody">
         <%
         <%
-            final List<RecoveryVerificationMethod.UserPrompt> prompts = (List<RecoveryVerificationMethod.UserPrompt>)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordPrompts);
+            final List<VerificationMethodSystem.UserPrompt> prompts = (List<VerificationMethodSystem.UserPrompt>)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordPrompts);
             final String instructions = (String)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordInstructions);
             final String instructions = (String)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordInstructions);
         %>
         %>
         <p><%=instructions%></p>
         <p><%=instructions%></p>
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
             <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
             <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
             <br/>
             <br/>
-            <% for (final RecoveryVerificationMethod.UserPrompt userPrompt : prompts) { %>
+            <% for (final VerificationMethodSystem.UserPrompt userPrompt : prompts) { %>
             <div class="formFieldLabel">
             <div class="formFieldLabel">
                 <%= userPrompt.getDisplayPrompt() %>
                 <%= userPrompt.getDisplayPrompt() %>
             </div>
             </div>

+ 3 - 3
src/main/webapp/WEB-INF/jsp/forgottenpassword-remote.jsp

@@ -22,7 +22,7 @@
 
 
 <!DOCTYPE html>
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
-<%@ page import="password.pwm.RecoveryVerificationMethod" %>
+<%@ page import="password.pwm.VerificationMethodSystem" %>
 <%@ page import="java.util.List" %>
 <%@ page import="java.util.List" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
@@ -35,14 +35,14 @@
     </jsp:include>
     </jsp:include>
     <div id="centerbody">
     <div id="centerbody">
         <%
         <%
-            final List<RecoveryVerificationMethod.UserPrompt> prompts = (List<RecoveryVerificationMethod.UserPrompt>)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordPrompts);
+            final List<VerificationMethodSystem.UserPrompt> prompts = (List<VerificationMethodSystem.UserPrompt>)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordPrompts);
             final String instructions = (String)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordInstructions);
             final String instructions = (String)JspUtility.getAttribute(pageContext, PwmRequest.Attribute.ForgottenPasswordInstructions);
         %>
         %>
         <p><%=instructions%></p>
         <p><%=instructions%></p>
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
         <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
             <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
             <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
             <br/>
             <br/>
-            <% for (final RecoveryVerificationMethod.UserPrompt userPrompt : prompts) { %>
+            <% for (final VerificationMethodSystem.UserPrompt userPrompt : prompts) { %>
             <div class="formFieldLabel">
             <div class="formFieldLabel">
                 <%= userPrompt.getDisplayPrompt() %>
                 <%= userPrompt.getDisplayPrompt() %>
             </div>
             </div>

+ 2 - 8
src/main/webapp/WEB-INF/jsp/helpdesk-detail.jsp

@@ -579,16 +579,10 @@
                         </button>
                         </button>
                         <% } %>
                         <% } %>
                         <% } %>
                         <% } %>
-                        <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_ENABLE_OTP_VERIFY)) { %>
-                        <button id="helpdesk_verifyOtpButton" <%=hasOtp?"":" disabled=\"true\""%>class="helpdesk-detail-btn btn">
-                            <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-mobile-phone"></span></pwm:if>
-                            Verify OTP
-                        </button>
-                        <% } %>
-                        <% if (helpdeskProfile.readSettingAsEnum(PwmSetting.HELPDESK_TOKEN_SEND_METHOD, MessageSendMethod.class) != MessageSendMethod.NONE) { %>
+                        <% if ((Boolean)JspUtility.getPwmRequest(pageContext).getAttribute(PwmRequest.Attribute.HelpdeskVerificationEnabled) == true) { %>
                         <button id="sendTokenButton" class="helpdesk-detail-btn btn">
                         <button id="sendTokenButton" class="helpdesk-detail-btn btn">
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-mobile-phone"></span></pwm:if>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-mobile-phone"></span></pwm:if>
-                            Send Verification
+                            <pwm:display key="Button_Verify"/>
                         </button>
                         </button>
                         <% } %>
                         <% } %>
                         <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_DELETE_USER_BUTTON)) { %>
                         <% if (helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_DELETE_USER_BUTTON)) { %>

+ 7 - 1
src/main/webapp/WEB-INF/jsp/helpdesk.jsp

@@ -1,3 +1,4 @@
+<%@ page import="password.pwm.http.JspUtility" %>
 <%--
 <%--
   ~ Password Management Servlets (PWM)
   ~ Password Management Servlets (PWM)
   ~ http://www.pwm-project.org
   ~ http://www.pwm-project.org
@@ -38,7 +39,7 @@
                         <span class="pwm-icon pwm-icon-search"></span>
                         <span class="pwm-icon pwm-icon-search"></span>
                     </td>
                     </td>
                     <td style="width:400px">
                     <td style="width:400px">
-                        <input type="search" id="username" name="username" class="helpdesk-input-username" style="width: 400px" <pwm:autofocus/> autocomplete="off"/>
+                        <input placeholder="<pwm:display key="Placeholder_Search"/>" type="search" id="username" name="username" class="helpdesk-input-username" style="width: 400px" <pwm:autofocus/> autocomplete="off"/>
                     </td>
                     </td>
                     <td style="width:20px">
                     <td style="width:20px">
                         <div id="searchIndicator" style="display:none">
                         <div id="searchIndicator" style="display:none">
@@ -48,6 +49,11 @@
                             <span style="color: #ffcd59;" class="pwm-icon pwm-icon-lg pwm-icon-exclamation-circle"></span>
                             <span style="color: #ffcd59;" class="pwm-icon pwm-icon-lg pwm-icon-exclamation-circle"></span>
                         </div>
                         </div>
                     </td>
                     </td>
+                    <% if ((Boolean)JspUtility.getPwmRequest(pageContext).getAttribute(PwmRequest.Attribute.HelpdeskVerificationEnabled) == true) { %>
+                    <td style="width: 45px">
+                        <button class="btn" id="button-show-current-verifications">Verifications</button>
+                    </td>
+                    <% } %>
                 </tr>
                 </tr>
             </table>
             </table>
             <noscript>
             <noscript>

+ 1 - 1
src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

@@ -34,7 +34,7 @@
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
 
 
         <div id="panel-searchbar">
         <div id="panel-searchbar">
-	        <input id="username" name="username" placeholder="People Search" class="peoplesearch-input-username" <pwm:autofocus/> autocomplete="off" />
+	        <input id="username" name="username" placeholder="<pwm:display key="Placeholder_Search"/>" class="peoplesearch-input-username" <pwm:autofocus/> autocomplete="off" />
             <div style="width:20px; max-width: 20px; display: inline-block;">
             <div style="width:20px; max-width: 20px; display: inline-block;">
                 <div id="searchIndicator" style="display: none">
                 <div id="searchIndicator" style="display: none">
                     <span style="" class="pwm-icon pwm-icon-lg pwm-icon-spin pwm-icon-spinner"></span>
                     <span style="" class="pwm-icon pwm-icon-lg pwm-icon-spin pwm-icon-spinner"></span>

+ 2 - 2
src/main/webapp/private/config/index.jsp

@@ -27,14 +27,14 @@
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <jsp:include page="/WEB-INF/jsp/fragment/header.jsp"/>
 <jsp:include page="/WEB-INF/jsp/fragment/header.jsp"/>
 <body>
 <body>
-<meta http-equiv="refresh" content="0;url=<pwm:context/><pwm:url url="/private/config/ConfigManager"/>"/>
+<meta http-equiv="refresh" content="0;url=<pwm:context/><pwm:url url="/private/config/manager"/>"/>
 <div id="wrapper">
 <div id="wrapper">
     <jsp:include page="/WEB-INF/jsp/fragment/header-body.jsp">
     <jsp:include page="/WEB-INF/jsp/fragment/header-body.jsp">
         <jsp:param name="pwm.PageName" value="Configuration"/>
         <jsp:param name="pwm.PageName" value="Configuration"/>
     </jsp:include>
     </jsp:include>
     <div id="content">
     <div id="content">
         <div id="centerbody">
         <div id="centerbody">
-            <pwm:display key="Display_PleaseWait"/> <a href="<pwm:context/><pwm:url url="/private/config/ConfigManage"/>"><pwm:display bundle="Admin" key="MenuItem_ConfigManager"/></a>
+            <pwm:display key="Display_PleaseWait"/> <a href="<pwm:context/><pwm:url url="/private/config/manager"/>"><pwm:display bundle="Admin" key="MenuItem_ConfigManager"/></a>
         </div>
         </div>
     </div>
     </div>
     <br class="clear"/>
     <br class="clear"/>

+ 1 - 1
src/main/webapp/public/resources/js/admin.js

@@ -65,7 +65,7 @@ PWM_ADMIN.initAdminNavMenu = function() {
                     label: '<span class="pwm-icon pwm-icon-external-link"></span> Application Reference',
                     label: '<span class="pwm-icon pwm-icon-external-link"></span> Application Reference',
                     id: 'applictionReference_dropitem',
                     id: 'applictionReference_dropitem',
                     onClick: function() {
                     onClick: function() {
-                        PWM_MAIN.newWindowOpen(PWM_GLOBAL['url-context'] + '/public/reference','referencedoc');
+                        PWM_MAIN.newWindowOpen(PWM_GLOBAL['url-context'] + '/public/reference/','referencedoc');
                     }
                     }
                 }));
                 }));
                 if (PWM_GLOBAL['setting-displayEula'] == true) {
                 if (PWM_GLOBAL['setting-displayEula'] == true) {

+ 4 - 4
src/main/webapp/public/resources/js/configeditor.js

@@ -311,7 +311,7 @@ PWM_CFGEDIT.initConfigEditor = function(nextFunction) {
             text:PWM_CONFIG.showString('Display_ConfigOpenInfo'),
             text:PWM_CONFIG.showString('Display_ConfigOpenInfo'),
             loadFunction:function(){
             loadFunction:function(){
                 PWM_MAIN.addEventHandler('link-configManager','click',function(){
                 PWM_MAIN.addEventHandler('link-configManager','click',function(){
-                    PWM_MAIN.goto('/private/config/ConfigManager');
+                    PWM_MAIN.goto('/private/config/manager');
                 });
                 });
             }
             }
         });
         });
@@ -325,7 +325,7 @@ PWM_CFGEDIT.initConfigEditor = function(nextFunction) {
     PWM_MAIN.addEventHandler('saveButton_icon','click',function(){PWM_CFGEDIT.saveConfiguration()});
     PWM_MAIN.addEventHandler('saveButton_icon','click',function(){PWM_CFGEDIT.saveConfiguration()});
     PWM_MAIN.addEventHandler('setPassword_icon','click',function(){PWM_CFGEDIT.setConfigurationPassword()});
     PWM_MAIN.addEventHandler('setPassword_icon','click',function(){PWM_CFGEDIT.setConfigurationPassword()});
     PWM_MAIN.addEventHandler('referenceDoc_icon','click',function(){
     PWM_MAIN.addEventHandler('referenceDoc_icon','click',function(){
-        PWM_MAIN.newWindowOpen(PWM_GLOBAL['url-context'] + '/public/reference','referencedoc');
+        PWM_MAIN.newWindowOpen(PWM_GLOBAL['url-context'] + '/public/reference/','referencedoc');
     });
     });
     PWM_MAIN.addEventHandler('macroDoc_icon','click',function(){ PWM_CFGEDIT.showMacroHelp(); });
     PWM_MAIN.addEventHandler('macroDoc_icon','click',function(){ PWM_CFGEDIT.showMacroHelp(); });
     PWM_MAIN.addEventHandler('settingFilter_icon','click',function(){ PWM_CFGEDIT.showSettingFilter(); });
     PWM_MAIN.addEventHandler('settingFilter_icon','click',function(){ PWM_CFGEDIT.showSettingFilter(); });
@@ -587,13 +587,13 @@ PWM_CFGEDIT.cancelEditing = function() {
                         function () {
                         function () {
                             PWM_MAIN.showWaitDialog({loadFunction: function () {
                             PWM_MAIN.showWaitDialog({loadFunction: function () {
                                 PWM_MAIN.ajaxRequest('ConfigEditor?processAction=cancelEditing',function(){
                                 PWM_MAIN.ajaxRequest('ConfigEditor?processAction=cancelEditing',function(){
-                                    PWM_MAIN.goto('ConfigManager', {addFormID: true});
+                                    PWM_MAIN.goto('manager', {addFormID: true});
                                 });
                                 });
                             }});
                             }});
                         }
                         }
                     });
                     });
                 } else {
                 } else {
-                    PWM_MAIN.goto('ConfigManager', {addFormID: true});
+                    PWM_MAIN.goto('manager', {addFormID: true});
                 }
                 }
             }
             }
         };
         };

+ 131 - 47
src/main/webapp/public/resources/js/helpdesk.js

@@ -36,7 +36,8 @@ PWM_HELPDESK.executeAction = function(actionName) {
             var inputValues = {};
             var inputValues = {};
             inputValues['userKey'] = PWM_VAR['helpdesk_obfuscatedDN'];
             inputValues['userKey'] = PWM_VAR['helpdesk_obfuscatedDN'];
             PWM_MAIN.showWaitDialog({loadFunction:function() {
             PWM_MAIN.showWaitDialog({loadFunction:function() {
-                var url = "Helpdesk?processAction=executeAction&name=" + actionName;
+                var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "executeAction");
+                url = PWM_MAIN.addParamToUrl(url, "name", actionName);
                 var loadFunction = function(data) {
                 var loadFunction = function(data) {
                     PWM_MAIN.closeWaitDialog();
                     PWM_MAIN.closeWaitDialog();
                     if (data['error'] == true) {
                     if (data['error'] == true) {
@@ -245,24 +246,85 @@ PWM_HELPDESK.setRandomPasswordPopup = function() {
 };
 };
 
 
 PWM_HELPDESK.loadSearchDetails = function(userKey) {
 PWM_HELPDESK.loadSearchDetails = function(userKey) {
-
-    var gotoDetailFunction = function() {
+    var gotoDetailFunction = function(userKey) {
         PWM_MAIN.showWaitDialog({loadFunction:function() {
         PWM_MAIN.showWaitDialog({loadFunction:function() {
-            PWM_MAIN.submitPostAction('helpdesk','detail',{userKey:userKey});
+            var contents = {};
+            contents['userKey'] = userKey;
+            if (PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE)) {
+                contents[PARAM_VERIFICATION_STATE] = PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE);
+            }
+            PWM_MAIN.submitPostAction('helpdesk','detail',contents);
         }});
         }});
     };
     };
 
 
-//    PWM_HELPDESK.validateOtpCode(userKey,gotoDetailFunction);
+    var handleVerificationResult = function(data) {
+        if (data['error']) {
+            PWM_MAIN.showErrorDialog(data);
+        } else {
+            if (data['data']['passed']) {
+                gotoDetailFunction(userKey);
+            } else {
+                var verificationMethods = PWM_VAR['verificationMethods']['required'];
+                PWM_HELPDESK.sendVerificationToken(userKey,verificationMethods);
+            }
+        }
+    };
+
+    var checkVerificationFunction = function() {
+        var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "checkVerification");
+        var content = {};
+        content['userKey'] = userKey;
+        content[PARAM_VERIFICATION_STATE] = PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE);
+        PWM_MAIN.ajaxRequest(url, handleVerificationResult, {content:content});
+    };
+
+
+    PWM_MAIN.showWaitDialog({loadFunction:checkVerificationFunction});
+};
+
+PWM_HELPDESK.showRecentVerifications = function() {
+    var handleVerificationResult = function(data) {
+        if (data['error']) {
+            PWM_MAIN.showErrorDialog(data);
+            return;
+        } else {
+            var records = data['data']['records'];
+            var html = '';
+            if (PWM_MAIN.JSLibrary.isEmpty(records)) {
+                html += PWM_MAIN.showString('Display_SearchResultsNone');
+            } else {
+                html += '<table>';
+                html += '<tr><td class="title">Profile</td><td class="title">Username</td><td class="title">Time</td><td class="title">Method</td>';
+                for (var i in records) {
+                    var record = records[i];
+                    html += '<tr>';
+                    html += '<td>' + record['profile'] + '</td>';
+                    html += '<td>' + record['username'] + '</td>';
+                    html += '<td class="timestamp">' + record['timestamp'] + '</td>';
+                    html += '<td>' + record['method'] + '</td>';
+                    html += '</tr>';
+                }
+            }
+
+            html += '</table>';
+            PWM_MAIN.showDialog({'title':'Recent Verifications','text':html,loadFunction:function(){PWM_MAIN.TimestampHandler.initAllElements()}});
+        }
+    };
+
+    var loadVerificationsFunction = function() {
+        var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "showVerifications");
+        var content = {};
+        content[PARAM_VERIFICATION_STATE] = PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE);
+        PWM_MAIN.ajaxRequest(url, handleVerificationResult, {content:content});
+    };
 
 
-    /*
-     */
 
 
-    gotoDetailFunction();
+    PWM_MAIN.showWaitDialog({loadFunction:loadVerificationsFunction});
 };
 };
 
 
 PWM_HELPDESK.processHelpdeskSearch = function() {
 PWM_HELPDESK.processHelpdeskSearch = function() {
     var validationProps = {};
     var validationProps = {};
-    validationProps['serviceURL'] = "Helpdesk?processAction=search";
+    validationProps['serviceURL'] = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "search");
     validationProps['showMessage'] = false;
     validationProps['showMessage'] = false;
     validationProps['ajaxTimeout'] = 120 * 1000;
     validationProps['ajaxTimeout'] = 120 * 1000;
     validationProps['usernameField'] = PWM_MAIN.getObject('username').value;
     validationProps['usernameField'] = PWM_MAIN.getObject('username').value;
@@ -350,13 +412,16 @@ PWM_HELPDESK.deleteUser = function() {
     })
     })
 };
 };
 
 
-PWM_HELPDESK.validateOtpCode = function(userKey, successFunction) {
+PWM_HELPDESK.validateOtpCode = function(userKey) {
     var dialogText = 'Instruct the user to load their mobile authentication app and share the current pass code.';
     var dialogText = 'Instruct the user to load their mobile authentication app and share the current pass code.';
 
 
-    PWM_HELPDESK.validateCode(userKey, successFunction, 'validateOtpCode', dialogText, {})
+    PWM_HELPDESK.validateCode(userKey, 'validateOtpCode', dialogText, {})
 };
 };
 
 
-PWM_HELPDESK.validateCode = function(userKey, successFunction, processAction, dialogText, extraPayload) {
+var PARAM_VERIFICATION_STATE = 'verificationState';
+var PREF_KEY_VERIFICATION_STATE = 'verificiationState';
+
+PWM_HELPDESK.validateCode = function(userKey, processAction, dialogText, extraPayload) {
     var validateOtpCodeFunction = function(){
     var validateOtpCodeFunction = function(){
         PWM_MAIN.getObject('icon-working').style.display = 'inherit';
         PWM_MAIN.getObject('icon-working').style.display = 'inherit';
         PWM_MAIN.getObject('icon-cross').style.display = 'none';
         PWM_MAIN.getObject('icon-cross').style.display = 'none';
@@ -365,6 +430,7 @@ PWM_HELPDESK.validateCode = function(userKey, successFunction, processAction, di
 
 
         content['userKey'] = userKey;
         content['userKey'] = userKey;
         content['code'] = PWM_MAIN.getObject('code').value;
         content['code'] = PWM_MAIN.getObject('code').value;
+        content[PARAM_VERIFICATION_STATE] = PWM_MAIN.Preferences.readSessionStorage(PREF_KEY_VERIFICATION_STATE);
         var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", processAction);
         var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", processAction);
         var loadFunction = function(data) {
         var loadFunction = function(data) {
             PWM_MAIN.getObject('icon-working').style.display = 'none';
             PWM_MAIN.getObject('icon-working').style.display = 'none';
@@ -374,7 +440,11 @@ PWM_HELPDESK.validateCode = function(userKey, successFunction, processAction, di
                 return;
                 return;
             }
             }
 
 
-            var passed =  data['data'];
+            var verificationState = data['data'][PARAM_VERIFICATION_STATE];
+            PWM_MAIN.Preferences.writeSessionStorage(PREF_KEY_VERIFICATION_STATE,verificationState);
+            console.log(verificationState);
+
+            var passed =  data['data']['passed'];
             if (passed) {
             if (passed) {
                 PWM_MAIN.getObject('icon-check').style.display = 'inherit';
                 PWM_MAIN.getObject('icon-check').style.display = 'inherit';
                 PWM_MAIN.getObject('dialog_ok_button').disabled = false;
                 PWM_MAIN.getObject('dialog_ok_button').disabled = false;
@@ -394,7 +464,11 @@ PWM_HELPDESK.validateCode = function(userKey, successFunction, processAction, di
         + '<button type="button" class="btn" id="button-checkCode"><span class="btn-icon pwm-icon pwm-icon-check"></span>' + PWM_MAIN.showString('Button_CheckCode') + '</button>'
         + '<button type="button" class="btn" id="button-checkCode"><span class="btn-icon pwm-icon pwm-icon-check"></span>' + PWM_MAIN.showString('Button_CheckCode') + '</button>'
         + '</td></table></div>';
         + '</td></table></div>';
 
 
-    var successFunction = successFunction === undefined ? function(){} : successFunction;
+    var successFunction = function() {
+        if (PWM_MAIN.getObject('application-info').getAttribute('data-jsp-name') == 'helpdesk.jsp') {
+            PWM_HELPDESK.loadSearchDetails(userKey);
+        }
+    };
     PWM_MAIN.showDialog({
     PWM_MAIN.showDialog({
         showClose:true,
         showClose:true,
         allowMove:true,
         allowMove:true,
@@ -410,22 +484,22 @@ PWM_HELPDESK.validateCode = function(userKey, successFunction, processAction, di
     });
     });
 };
 };
 
 
-PWM_HELPDESK.sendVerificationToken = function(userKey) {
+PWM_HELPDESK.sendVerificationToken = function(userKey, methods) {
     var sendMethodSetting = PWM_VAR["helpdesk_setting_tokenSendMethod"];
     var sendMethodSetting = PWM_VAR["helpdesk_setting_tokenSendMethod"];
-    var choiceFlag = sendMethodSetting == 'CHOICE_SMS_EMAIL';
+    var tokenChoiceFlag = sendMethodSetting == 'CHOICE_SMS_EMAIL';
 
 
     var sendTokenAction = function(choice) {
     var sendTokenAction = function(choice) {
         var sendContent = {};
         var sendContent = {};
         sendContent['userKey'] = userKey;
         sendContent['userKey'] = userKey;
-        if (choiceFlag && choice) {
+        if (tokenChoiceFlag && choice) {
             sendContent['method'] = choice;
             sendContent['method'] = choice;
         }
         }
         PWM_MAIN.showWaitDialog({loadFunction:function(){
         PWM_MAIN.showWaitDialog({loadFunction:function(){
-            var url = 'helpdesk?processAction=sendVerificationToken';
+            var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "sendVerificationToken");
             var loadFunction = function(data) {
             var loadFunction = function(data) {
                 if (!data['error']) {
                 if (!data['error']) {
                     var text = '<table><tr><td>Token Destination</td><td>' + data['data']['destination'] + '</td></tr></table>';
                     var text = '<table><tr><td>Token Destination</td><td>' + data['data']['destination'] + '</td></tr></table>';
-                    PWM_HELPDESK.validateCode(userKey, function(){}, 'verifyVerificationToken',text,data['data']);
+                    PWM_HELPDESK.validateCode(userKey, 'verifyVerificationToken',text,data['data']);
                 } else {
                 } else {
                     PWM_MAIN.showErrorDialog(data);
                     PWM_MAIN.showErrorDialog(data);
                 }
                 }
@@ -434,34 +508,41 @@ PWM_HELPDESK.sendVerificationToken = function(userKey) {
         }});
         }});
     };
     };
 
 
-    if (choiceFlag) {
-        var confirmText = '<div style="text-align:center"><br/><br/><button class="btn" type="button" name="emailChoiceButton" id="emailChoiceButton">'
-            + '<span class="btn-icon pwm-icon pwm-icon-file-text"></span>' + PWM_MAIN.showString('Button_Email') + '</button>'
-            + '<br/><br/><button class="btn" type="button" name="smsChoiceButton" id="smsChoiceButton">'
-            + '<span class="btn-icon pwm-icon pwm-icon-phone"></span>' + PWM_MAIN.showString('Button_SMS') + '</button></div>';
-        var dialoagLoadFunction = function() {
-            PWM_MAIN.addEventHandler('emailChoiceButton','click',function(){sendTokenAction('email')});
-            PWM_MAIN.addEventHandler('smsChoiceButton','click',function(){sendTokenAction('sms')});
-        };
-        PWM_MAIN.showConfirmDialog({
-            title:'Verification send method',
-            text:confirmText,
-            showOk: !choiceFlag,
-            okAction:function(){
-                sendTokenAction();
-            },
-            loadFunction:dialoagLoadFunction
-        });
-    } else {
-        PWM_MAIN.showConfirmDialog({
-            okAction:function(){
-                sendTokenAction();
-            }
-        });
+    var dialogText = '<div style="text-align:center"><br/>';
+    if (PWM_MAIN.JSLibrary.arrayContains(methods,'TOKEN')) {
+        if (tokenChoiceFlag || sendMethodSetting == 'EMAILONLY') {
+            dialogText += '<br/><button class="btn" type="button" name="emailChoiceButton" id="emailChoiceButton">'
+                + '<span class="btn-icon pwm-icon pwm-icon-envelope-o"></span>' + PWM_MAIN.showString('Button_Email') + '</button>';
+        }
+        if (tokenChoiceFlag || sendMethodSetting == 'EMAILONLY') {
+            dialogText += '<br/><br/><button class="btn" type="button" name="smsChoiceButton" id="smsChoiceButton">'
+                + '<span class="btn-icon pwm-icon pwm-icon-phone"></span>' + PWM_MAIN.showString('Button_SMS') + '</button>';
+        }
     }
     }
+    if (PWM_MAIN.JSLibrary.arrayContains(methods,'OTP')) {
+        dialogText += '<br/><br/><button class="btn" type="button" name="otpChoiceButton" id="otpChoiceButton">'
+        + '<span class="btn-icon pwm-icon pwm-icon-qrcode"></span>' + PWM_MAIN.showString('Button_OTP') + '</button>';
+    }
+    dialogText += '</div>';
+
+    var dialoagLoadFunction = function() {
+        PWM_MAIN.addEventHandler('emailChoiceButton','click',function(){sendTokenAction('email')});
+        PWM_MAIN.addEventHandler('smsChoiceButton','click',function(){sendTokenAction('sms')});
+        PWM_MAIN.addEventHandler('otpChoiceButton','click',function(){PWM_HELPDESK.validateOtpCode(userKey)});
+    };
+    PWM_MAIN.showConfirmDialog({
+        title:'Verification send method',
+        text:dialogText,
+        showOk: !tokenChoiceFlag,
+        okAction:function(){
+            sendTokenAction();
+        },
+        loadFunction:dialoagLoadFunction
+    });
 };
 };
 
 
 PWM_HELPDESK.initHelpdeskSearchPage = function() {
 PWM_HELPDESK.initHelpdeskSearchPage = function() {
+    PWM_MAIN.addEventHandler('button-show-current-verifications','click',PWM_HELPDESK.showRecentVerifications);
     PWM_HELPDESK.makeSearchGrid(function(){
     PWM_HELPDESK.makeSearchGrid(function(){
         PWM_MAIN.addEventHandler('username', "keyup, input", function(){
         PWM_MAIN.addEventHandler('username', "keyup, input", function(){
             PWM_HELPDESK.processHelpdeskSearch();
             PWM_HELPDESK.processHelpdeskSearch();
@@ -516,7 +597,9 @@ PWM_HELPDESK.initHelpdeskDetailPage = function() {
         PWM_HELPDESK.validateOtpCode(PWM_VAR['helpdesk_obfuscatedDN']);
         PWM_HELPDESK.validateOtpCode(PWM_VAR['helpdesk_obfuscatedDN']);
     });
     });
     PWM_MAIN.addEventHandler('sendTokenButton','click',function(){
     PWM_MAIN.addEventHandler('sendTokenButton','click',function(){
-        PWM_HELPDESK.sendVerificationToken(PWM_VAR['helpdesk_obfuscatedDN']);
+
+        var verificationMethods = PWM_VAR['verificationMethods']['optional'];
+        PWM_HELPDESK.sendVerificationToken(PWM_VAR['helpdesk_obfuscatedDN'],verificationMethods);
     });
     });
     PWM_MAIN.addEventHandler('helpdesk_deleteUserButton','click',function(){
     PWM_MAIN.addEventHandler('helpdesk_deleteUserButton','click',function(){
         PWM_HELPDESK.deleteUser();
         PWM_HELPDESK.deleteUser();
@@ -561,7 +644,8 @@ PWM_HELPDESK.unlockIntruder = function() {
         okAction:function() {
         okAction:function() {
             PWM_MAIN.showWaitDialog({
             PWM_MAIN.showWaitDialog({
                 loadFunction:function(){
                 loadFunction:function(){
-                    var ajaxUrl = "helpdesk?processAction=unlockIntruder&userKey=" + PWM_VAR['helpdesk_obfuscatedDN'];
+                    var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "unlockIntruder");
+                    url = PWM_MAIN.addParamToUrl(url, "userKey", PWM_VAR['helpdesk_obfuscatedDN']);
                     var load = function(data) {
                     var load = function(data) {
                         if (data['error'] == true) {
                         if (data['error'] == true) {
                             PWM_MAIN.showErrorDialog(error);
                             PWM_MAIN.showErrorDialog(error);
@@ -575,7 +659,7 @@ PWM_HELPDESK.unlockIntruder = function() {
                             });
                             });
                         }
                         }
                     };
                     };
-                    PWM_MAIN.ajaxRequest(ajaxUrl, load);
+                    PWM_MAIN.ajaxRequest(url, load);
                 }
                 }
             });
             });
         }
         }
@@ -586,7 +670,7 @@ PWM_HELPDESK.doOtpClear = function() {
     var inputValues = {};
     var inputValues = {};
     inputValues['userKey'] = PWM_VAR['helpdesk_obfuscatedDN'];
     inputValues['userKey'] = PWM_VAR['helpdesk_obfuscatedDN'];
     PWM_MAIN.showWaitDialog({loadFunction:function() {
     PWM_MAIN.showWaitDialog({loadFunction:function() {
-        var url = "helpdesk?processAction=clearOtpSecret";
+        var url = PWM_MAIN.addParamToUrl(window.location.href,"processAction", "clearOtpSecret");
         var loadFunction = function(results) {
         var loadFunction = function(results) {
             if (results['error'] != true) {
             if (results['error'] != true) {
                 PWM_MAIN.showDialog({
                 PWM_MAIN.showDialog({