Selaa lähdekoodia

implement support for multiple email/sms token destinations in forgotten password.

Jason Rivard 7 vuotta sitten
vanhempi
commit
6c28e3ac82
27 muutettua tiedostoa jossa 420 lisäystä ja 279 poistoa
  1. 3 0
      server/src/main/java/password/pwm/PwmConstants.java
  2. 66 27
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  3. 8 0
      server/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java
  4. 8 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  5. 8 0
      server/src/main/java/password/pwm/http/PwmHttpRequestWrapper.java
  6. 1 1
      server/src/main/java/password/pwm/http/SessionManager.java
  7. 4 12
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  8. 2 2
      server/src/main/java/password/pwm/http/servlet/ForgottenUsernameServlet.java
  9. 51 39
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  10. 89 116
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  11. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskDetailInfoBean.java
  12. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  13. 1 1
      server/src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServletUtil.java
  14. 1 1
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  15. 1 1
      server/src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  16. 8 0
      server/src/main/java/password/pwm/ldap/UserInfo.java
  17. 6 0
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  18. 32 0
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  19. 1 1
      server/src/main/java/password/pwm/svc/intruder/IntruderManager.java
  20. 2 1
      server/src/main/java/password/pwm/util/java/JavaHelper.java
  21. 32 16
      server/src/main/java/password/pwm/util/macro/MacroMachine.java
  22. 3 3
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  23. 20 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  24. 9 1
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  25. 18 2
      server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  26. 5 3
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-entertoken.jsp
  27. 39 50
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp

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

@@ -112,7 +112,9 @@ public abstract class PwmConstants
     public static final String SESSION_ATTR_PWM_SESSION = "PwmSession";
     public static final String SESSION_ATTR_BEANS = "SessionBeans";
     public static final String SESSION_ATTR_PWM_APP_NONCE = "PwmApplication-Nonce";
+
     public static final String REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
+    public static final String REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE = "ForgottenPw-AvailableTokenDestCache";
 
     public static final PwmHashAlgorithm SETTING_CHECKSUM_HASH_METHOD = PwmHashAlgorithm.SHA256;
 
@@ -141,6 +143,7 @@ public abstract class PwmConstants
 
 
     public static final String PARAM_ACTION_REQUEST = "processAction";
+    public static final String PARAM_RESET_TYPE = "resetType";
     public static final String PARAM_ACTION_STATE = "actionState";
     public static final String PARAM_RESPONSE_PREFIX = "PwmResponse_R_";
     public static final String PARAM_QUESTION_PREFIX = "PwmResponse_Q_";

+ 66 - 27
server/src/main/java/password/pwm/bean/TokenDestinationItem.java

@@ -22,21 +22,27 @@
 
 package password.pwm.bean;
 
-import lombok.AllArgsConstructor;
-import lombok.Getter;
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.PwmApplication;
 import password.pwm.config.Configuration;
+import password.pwm.config.option.MessageSendMethod;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.UserInfo;
 import password.pwm.util.ValueObfuscator;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.secure.SecureService;
 
+import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
-@Getter
-@AllArgsConstructor
-public class TokenDestinationItem
+@Value
+@Builder
+public class TokenDestinationItem implements Serializable
 {
     private String id;
     private String display;
@@ -45,44 +51,77 @@ public class TokenDestinationItem
 
     public enum Type
     {
-        sms,
-        email,
+        sms( MessageSendMethod.SMSONLY ),
+        email( MessageSendMethod.EMAILONLY ),;
+
+        private MessageSendMethod messageSendMethod;
+
+        Type( final MessageSendMethod messageSendMethod )
+        {
+            this.messageSendMethod = messageSendMethod;
+        }
+
+        public MessageSendMethod getMessageSendMethod( )
+        {
+            return messageSendMethod;
+        }
     }
 
-    public static List<TokenDestinationItem> allFromConfig( final Configuration configuration, final UserInfo userInfo )
+    public static List<TokenDestinationItem> allFromConfig(
+            final PwmApplication pwmApplication,
+            final UserInfo userInfo
+    )
             throws PwmUnrecoverableException
     {
+        final Configuration configuration = pwmApplication.getConfig();
+        final SecureService secureService = pwmApplication.getSecureService();
+
         final ValueObfuscator valueObfuscator = new ValueObfuscator( configuration );
-        int counter = 0;
 
-        final List<TokenDestinationItem> results = new ArrayList<>();
+        final Map<String, TokenDestinationItem> results = new LinkedHashMap<>(  );
 
+        for ( final String emailValue : new String[]
+                {
+                        userInfo.getUserEmailAddress(),
+                        userInfo.getUserEmailAddress2(),
+                        userInfo.getUserEmailAddress3(),
+                }
+                )
         {
-            final String smsValue = userInfo.getUserSmsNumber();
-            if ( !StringUtil.isEmpty( smsValue ) )
+            if ( !StringUtil.isEmpty( emailValue ) )
             {
-                results.add( new TokenDestinationItem(
-                        String.valueOf( ++counter ),
-                        valueObfuscator.maskPhone( smsValue ),
-                        smsValue,
-                        Type.sms
-                ) );
+                final String idHash = secureService.hash( emailValue + Type.email.name() );
+                final TokenDestinationItem item = TokenDestinationItem.builder()
+                        .id( idHash )
+                        .display( valueObfuscator.maskEmail( emailValue ) )
+                        .value( emailValue )
+                        .type( Type.email )
+                        .build();
+                results.put( idHash, item );
             }
         }
 
+        for ( final String smsValue : new String[]
+                {
+                        userInfo.getUserSmsNumber(),
+                        userInfo.getUserSmsNumber2(),
+                        userInfo.getUserSmsNumber3(),
+                }
+                )
         {
-            final String emailValue = userInfo.getUserEmailAddress();
-            if ( !StringUtil.isEmpty( emailValue ) )
+            if ( !StringUtil.isEmpty( smsValue ) )
             {
-                results.add( new TokenDestinationItem(
-                        String.valueOf( ++counter ),
-                        valueObfuscator.maskEmail( emailValue ),
-                        emailValue,
-                        Type.email
-                ) );
+                final String idHash = secureService.hash( smsValue + Type.sms.name() );
+                final TokenDestinationItem item = TokenDestinationItem.builder()
+                        .id( idHash )
+                        .display( valueObfuscator.maskPhone( smsValue ) )
+                        .value( smsValue )
+                        .type( Type.sms )
+                        .build();
+                results.put(  idHash, item );
             }
         }
 
-        return Collections.unmodifiableList( results );
+        return Collections.unmodifiableList( new ArrayList<>( results.values() ) );
     }
 }

+ 8 - 0
server/src/main/java/password/pwm/bean/pub/PublicUserInfoBean.java

@@ -47,7 +47,11 @@ public class PublicUserInfoBean implements Serializable
     private String userID;
     private String userGUID;
     private String userEmailAddress;
+    private String userEmailAddress2;
+    private String userEmailAddress3;
     private String userSmsNumber;
+    private String userSmsNumber2;
+    private String userSmsNumber3;
     private Instant passwordExpirationTime;
     private Instant passwordLastModifiedTime;
     private Instant lastLoginTime;
@@ -77,7 +81,11 @@ public class PublicUserInfoBean implements Serializable
         publicUserInfoBean.userID = userInfoBean.getUsername();
         publicUserInfoBean.userGUID = publicUserInfoBean.getUserGUID();
         publicUserInfoBean.userEmailAddress = userInfoBean.getUserEmailAddress();
+        publicUserInfoBean.userEmailAddress2 = userInfoBean.getUserEmailAddress2();
+        publicUserInfoBean.userEmailAddress3 = userInfoBean.getUserEmailAddress3();
         publicUserInfoBean.userSmsNumber = userInfoBean.getUserSmsNumber();
+        publicUserInfoBean.userSmsNumber2 = userInfoBean.getUserSmsNumber2();
+        publicUserInfoBean.userSmsNumber3 = userInfoBean.getUserSmsNumber3();
         publicUserInfoBean.passwordExpirationTime = userInfoBean.getPasswordExpirationTime();
         publicUserInfoBean.passwordLastModifiedTime = userInfoBean.getPasswordLastModifiedTime();
         publicUserInfoBean.passwordStatus = userInfoBean.getPasswordStatus();

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

@@ -246,8 +246,16 @@ public enum PwmSetting
             "ldap.group.label.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     EMAIL_USER_MAIL_ATTRIBUTE(
             "email.userMailAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    EMAIL_USER_MAIL_ATTRIBUTE_2(
+            "email.userMailAttribute2", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    EMAIL_USER_MAIL_ATTRIBUTE_3(
+            "email.userMailAttribute3", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     SMS_USER_PHONE_ATTRIBUTE(
             "sms.userSmsAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    SMS_USER_PHONE_ATTRIBUTE_2(
+            "sms.userSmsAttribute2", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
+    SMS_USER_PHONE_ATTRIBUTE_3(
+            "sms.userSmsAttribute3", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     CHALLENGE_USER_ATTRIBUTE(
             "challenge.userAttribute", PwmSettingSyntax.STRING, PwmSettingCategory.LDAP_ATTRIBUTES ),
     EVENTS_LDAP_ATTRIBUTE(

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

@@ -31,6 +31,7 @@ import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.PasswordData;
 import password.pwm.util.Validator;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
@@ -278,6 +279,13 @@ public abstract class PwmHttpRequestWrapper
         return strValue != null && Boolean.parseBoolean( strValue );
     }
 
+    public <E extends Enum<E>> E readParameterAsEnum( final String name, final Class<E> enumClass, final E defaultValue )
+            throws PwmUnrecoverableException
+    {
+        final String value = readParameterAsString( name, Flag.BypassValidation );
+        return JavaHelper.readEnumFromString( enumClass, defaultValue, value );
+    }
+
     public int readParameterAsInt( final String name, final int defaultValue )
             throws PwmUnrecoverableException
     {

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

@@ -233,7 +233,7 @@ public class SessionManager
         final UserInfo userInfoBean = pwmSession.isAuthenticated()
                 ? pwmSession.getUserInfo()
                 : null;
-        return new MacroMachine( pwmApplication, pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
+        return MacroMachine.forUser( pwmApplication, pwmSession.getLabel(), userInfoBean, pwmSession.getLoginInfoBean() );
     }
 
     public Profile getProfile( final PwmApplication pwmApplication, final ProfileType profileType ) throws PwmUnrecoverableException

+ 4 - 12
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -28,9 +28,9 @@ import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.Value;
 import password.pwm.VerificationMethodSystem;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.option.IdentityVerificationMethod;
-import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.config.value.data.FormConfiguration;
 
@@ -88,11 +88,8 @@ public class ForgottenPasswordBean extends PwmSessionBean
         @SerializedName( "m" )
         private final Set<IdentityVerificationMethod> satisfiedMethods = new LinkedHashSet<>();
 
-        @SerializedName( "c" )
-        private MessageSendMethod tokenSendChoice;
-
-        @SerializedName( "a" )
-        private String tokenSentAddress;
+        @SerializedName( "d" )
+        private TokenDestinationItem tokenDestination;
 
         @SerializedName( "i" )
         private IdentityVerificationMethod inProgressVerificationMethod;
@@ -102,8 +99,7 @@ public class ForgottenPasswordBean extends PwmSessionBean
         public void clearTokenSentStatus( )
         {
             this.setTokenSent( false );
-            this.setTokenSentAddress( null );
-            this.setTokenSendChoice( null );
+            this.setTokenDestination( null );
         }
     }
 
@@ -123,16 +119,12 @@ public class ForgottenPasswordBean extends PwmSessionBean
         @SerializedName( "m" )
         private final int minimumOptionalAuthMethods;
 
-        @SerializedName( "t" )
-        private final MessageSendMethod tokenSendMethod;
-
         public RecoveryFlags( )
         {
             this.requiredAuthMethods = Collections.emptySet();
             this.optionalAuthMethods = Collections.emptySet();
             this.allowWhenLdapIntruderLocked = false;
             this.minimumOptionalAuthMethods = 0;
-            this.tokenSendMethod = MessageSendMethod.NONE;
         }
     }
 

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

@@ -326,7 +326,7 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
             return new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
         }
 
-        final MacroMachine macroMachine = new MacroMachine( pwmApplication, sessionLabel, userInfo, null );
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
 
         pwmApplication.sendSmsUsingQueue( toNumber, smsMessage, sessionLabel, macroMachine );
         return null;
@@ -346,7 +346,7 @@ public class ForgottenUsernameServlet extends AbstractPwmServlet
             return new ErrorInformation( PwmError.ERROR_UNKNOWN, errorMsg );
         }
 
-        final MacroMachine macroMachine = new MacroMachine( pwmApplication, sessionLabel, userInfo, null );
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmApplication, sessionLabel, userInfo, null );
 
         pwmApplication.getEmailQueue().submitEmail( emailItemBean, userInfo, macroMachine );
 

+ 51 - 39
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -39,7 +39,6 @@ import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
-import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
 import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.profile.ForgottenPasswordProfile;
@@ -83,6 +82,7 @@ import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.PasswordUtility;
@@ -153,6 +153,14 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
     }
 
+    public enum ResetType
+    {
+        exitForgottenPassword,
+        gotoSearch,
+        clearTokenDestination,
+    }
+
+
     @Override
     public Class<? extends ProcessAction> getProcessActionsClass( )
     {
@@ -165,12 +173,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         resetPassword,
     }
 
-    public enum TokenChoice
-    {
-        email,
-        sms,
-    }
-
     @Override
     public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
     {
@@ -287,14 +289,27 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     private ProcessStatus processReset( final PwmRequest pwmRequest )
             throws IOException, PwmUnrecoverableException
     {
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-
-        clearForgottenPasswordBean( pwmRequest );
+        final ResetType resetType = pwmRequest.readParameterAsEnum( PwmConstants.PARAM_RESET_TYPE, ResetType.class, ResetType.exitForgottenPassword );
 
-        if ( forgottenPasswordBean.getUserIdentity() == null )
+        switch ( resetType )
         {
-            pwmRequest.sendRedirectToContinue();
-            return ProcessStatus.Halt;
+            case exitForgottenPassword:
+                clearForgottenPasswordBean( pwmRequest );
+                pwmRequest.sendRedirectToContinue();
+                return ProcessStatus.Halt;
+
+            case gotoSearch:
+                clearForgottenPasswordBean( pwmRequest );
+                break;
+
+            case clearTokenDestination:
+                forgottenPasswordBean( pwmRequest ).getProgress().setTokenDestination( null );
+                forgottenPasswordBean( pwmRequest ).getProgress().setTokenSent( false );
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( resetType );
+
         }
 
         return ProcessStatus.Continue;
@@ -302,27 +317,20 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
     @ActionHandler( action = "tokenChoice" )
     private ProcessStatus processTokenChoice( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, ServletException, IOException, ChaiUnavailableException
+            throws PwmUnrecoverableException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        if ( forgottenPasswordBean.getProgress().getTokenSendChoice() == MessageSendMethod.CHOICE_SMS_EMAIL )
+        final List<TokenDestinationItem> items = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+
+        final String requestedID = pwmRequest.readParameterAsString( "choice", PwmHttpRequestWrapper.Flag.BypassValidation );
+
+        if ( !StringUtil.isEmpty( requestedID ) )
         {
-            final String choice = pwmRequest.readParameterAsString( "choice" );
-            final TokenChoice tokenChoice = JavaHelper.readEnumFromString( TokenChoice.class, null, choice );
-            if ( tokenChoice != null )
+            for ( final TokenDestinationItem item : items )
             {
-                switch ( tokenChoice )
+                if ( requestedID.equals( item.getId() ) )
                 {
-                    case email:
-                        forgottenPasswordBean.getProgress().setTokenSendChoice( MessageSendMethod.EMAILONLY );
-                        break;
-
-                    case sms:
-                        forgottenPasswordBean.getProgress().setTokenSendChoice( MessageSendMethod.SMSONLY );
-                        break;
-
-                    default:
-                        JavaHelper.unhandledSwitchStatement( tokenChoice );
+                    forgottenPasswordBean.getProgress().setTokenDestination( item );
                 }
             }
         }
@@ -813,8 +821,8 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
         {
             final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-            final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getProgress().getTokenSendChoice();
-            ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, tokenSendMethod );
+            final TokenDestinationItem tokenDestinationItem = forgottenPasswordBean.getProgress().getTokenDestination();
+            ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, tokenDestinationItem );
         }
 
         final RestResultBean restResultBean = RestResultBean.forSuccessMessage( pwmRequest, Message.Success_TokenResend );
@@ -1410,12 +1418,17 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             case TOKEN:
             {
                 final ForgottenPasswordBean.Progress progress = forgottenPasswordBean.getProgress();
-                if ( progress.getTokenSendChoice() == null )
+                final List<TokenDestinationItem> tokenDestinations = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+                if ( progress.getTokenDestination() == null )
                 {
-                    progress.setTokenSendChoice( ForgottenPasswordUtil.figureTokenSendPreference( pwmRequest, forgottenPasswordBean ) );
+                    if ( tokenDestinations.size() == 1 )
+                    {
+                        final TokenDestinationItem singleItem = tokenDestinations.iterator().next();
+                        progress.setTokenDestination( singleItem );
+                    }
                 }
 
-                if ( progress.getTokenSendChoice() == MessageSendMethod.CHOICE_SMS_EMAIL )
+                if ( progress.getTokenDestination() == null )
                 {
                     forwardToTokenChoiceJsp( pwmRequest );
                     return;
@@ -1424,8 +1437,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                 if ( !progress.isTokenSent() )
                 {
                     final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-                    final String destAddress = ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, progress.getTokenSendChoice() );
-                    progress.setTokenSentAddress( destAddress );
+                    ForgottenPasswordUtil.initializeAndSendToken( pwmRequest, userInfo, progress.getTokenDestination() );
                     progress.setTokenSent( true );
                 }
 
@@ -1492,9 +1504,9 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     private void forwardToTokenChoiceJsp( final PwmRequest pwmRequest )
             throws ServletException, PwmUnrecoverableException, IOException
     {
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean( pwmRequest ) );
-        final ArrayList<TokenDestinationItem> destItems = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmRequest.getConfig(), userInfo ) );
-        pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordTokenDestItems, destItems );
+        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
+        final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
+        pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordTokenDestItems, new ArrayList<>( destItems ) );
         pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE );
     }
 }

+ 89 - 116
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -69,6 +69,7 @@ import password.pwm.svc.token.TokenType;
 import password.pwm.util.PasswordData;
 import password.pwm.util.RandomPasswordGenerator;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
@@ -88,6 +89,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 public class ForgottenPasswordUtil
 {
@@ -228,7 +230,7 @@ public class ForgottenPasswordUtil
         }
 
         final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
-        final MacroMachine macroMachine = new MacroMachine(
+        final MacroMachine macroMachine = MacroMachine.forUser(
                 pwmApplication,
                 pwmRequest.getSessionLabel(),
                 userInfo,
@@ -276,48 +278,61 @@ public class ForgottenPasswordUtil
         return false;
     }
 
-    static MessageSendMethod figureTokenSendPreference(
+    static List<TokenDestinationItem> figureAvailableTokenDestinations(
             final PwmRequest pwmRequest,
             final ForgottenPasswordBean forgottenPasswordBean
     )
             throws PwmUnrecoverableException
     {
-        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod();
-        if ( tokenSendMethod == null || tokenSendMethod.equals( MessageSendMethod.NONE ) )
         {
-            return MessageSendMethod.NONE;
+            @SuppressWarnings( "unchecked" )
+            final List<TokenDestinationItem> cachedItems = (List<TokenDestinationItem>) pwmRequest.getHttpServletRequest().getAttribute(
+                    PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE
+            );
+            if ( cachedItems != null )
+            {
+                return cachedItems;
+            }
         }
 
-        if ( !tokenSendMethod.equals( MessageSendMethod.CHOICE_SMS_EMAIL ) )
+        final String profileID = forgottenPasswordBean.getForgottenPasswordProfileID();
+        final ForgottenPasswordProfile forgottenPasswordProfile = pwmRequest.getConfig().getForgottenPasswordProfiles().get( profileID );
+        final MessageSendMethod tokenSendMethod = forgottenPasswordProfile.readSettingAsEnum( PwmSetting.RECOVERY_TOKEN_SEND_METHOD, MessageSendMethod.class );
+        if ( tokenSendMethod == null || tokenSendMethod.equals( MessageSendMethod.NONE ) )
         {
-            return tokenSendMethod;
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, "no token send methods configured in profile" );
         }
 
-        final String emailAddress = userInfo.getUserEmailAddress();
-        final String smsAddress = userInfo.getUserSmsNumber();
-
-        final boolean hasEmail = emailAddress != null && emailAddress.length() > 1;
-        final boolean hasSms = smsAddress != null && smsAddress.length() > 1;
+        final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+        List<TokenDestinationItem> tokenDestinations = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmRequest.getPwmApplication(), userInfo ) );
 
-        if ( hasEmail && hasSms )
+        if ( tokenSendMethod != MessageSendMethod.CHOICE_SMS_EMAIL )
         {
-            return MessageSendMethod.CHOICE_SMS_EMAIL;
+            tokenDestinations = tokenDestinations
+                    .stream()
+                    .filter( tokenDestinationItem -> tokenSendMethod == tokenDestinationItem.getType().getMessageSendMethod() )
+                    .collect( Collectors.toList() );
         }
-        else if ( hasEmail )
+
+        final List<TokenDestinationItem> effectiveItems = new ArrayList<>(  );
+        for ( final TokenDestinationItem item : tokenDestinations )
         {
-            LOGGER.debug( pwmRequest, "though token send method is "
-                    + MessageSendMethod.CHOICE_SMS_EMAIL + ", no sms address is available for user so defaulting to email method" );
-            return MessageSendMethod.EMAILONLY;
+            final TokenDestinationItem effectiveItem = invokeExternalTokenDestRestClient( pwmRequest, userInfo.getUserIdentity(), item );
+            effectiveItems.add( effectiveItem );
         }
-        else if ( hasSms )
+
+        LOGGER.trace( pwmRequest, "calculated available token send destinations: " + JsonUtil.serializeCollection( effectiveItems ) );
+
+        if ( tokenDestinations.isEmpty() )
         {
-            LOGGER.debug( pwmRequest, "though token send method is "
-                    + MessageSendMethod.CHOICE_SMS_EMAIL + ", no email address is available for user so defaulting to sms method" );
-            return MessageSendMethod.SMSONLY;
+            final String msg = "no available contact methods of type " + tokenSendMethod.name() + " available";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
         }
 
-        throw new PwmUnrecoverableException( new ErrorInformation( PwmError.ERROR_TOKEN_MISSING_CONTACT ) );
+        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( effectiveItems );
+        pwmRequest.getHttpServletRequest().setAttribute( PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE, finalList );
+
+        return finalList;
     }
 
     static void verifyRequirementsForAuthMethod(
@@ -331,8 +346,7 @@ public class ForgottenPasswordUtil
         {
             case TOKEN:
             {
-                final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-                figureIfTokenSendMethodIsAvailableForUser( forgottenPasswordBean, userInfo );
+                ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
             }
             break;
 
@@ -401,55 +415,12 @@ public class ForgottenPasswordUtil
         }
     }
 
-    private static void figureIfTokenSendMethodIsAvailableForUser(
-            final ForgottenPasswordBean forgottenPasswordBean,
-            final UserInfo userInfoBean
-    )
-            throws PwmUnrecoverableException
-    {
-        final MessageSendMethod tokenSendMethod = forgottenPasswordBean.getRecoveryFlags().getTokenSendMethod();
-        if ( tokenSendMethod == null || tokenSendMethod == MessageSendMethod.NONE )
-        {
-            final String errorMsg = "user is required to complete token validation, yet there is not a token send method configured";
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_INVALID_CONFIG, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
-
-        final boolean hasEmailAddr = !StringUtil.isEmpty( userInfoBean.getUserEmailAddress() );
-        final boolean hasSmsAddr = !StringUtil.isEmpty( userInfoBean.getUserSmsNumber() );
-
-        if ( tokenSendMethod == MessageSendMethod.EMAILONLY && !hasEmailAddr )
-        {
-            final String errorMsg = "token send method requires an email address, yet user does not have an email address value";
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
-
-        if ( tokenSendMethod == MessageSendMethod.SMSONLY && !hasSmsAddr )
-        {
-            final String errorMsg = "token send method requires an sms destination, yet user does not have an sms destination value";
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg );
-            throw new PwmUnrecoverableException( errorInformation );
-        }
-
-        if ( tokenSendMethod == MessageSendMethod.CHOICE_SMS_EMAIL
-                || tokenSendMethod == MessageSendMethod.EMAILFIRST
-                || tokenSendMethod == MessageSendMethod.SMSFIRST )
-        {
-            if ( !hasEmailAddr && !hasSmsAddr )
-            {
-                final String errorMsg = "token send method requires an sms or email desitnation, yet user does not have an sms or email destination value";
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_MISSING_CONTACT, errorMsg );
-                throw new PwmUnrecoverableException( errorInformation );
-            }
-        }
-    }
 
     static Map<Challenge, String> readResponsesFromHttpRequest(
             final PwmRequest req,
             final ChallengeSet challengeSet
     )
-            throws ChaiValidationException, ChaiUnavailableException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
         final Map<Challenge, String> responses = new LinkedHashMap<>();
 
@@ -465,10 +436,10 @@ public class ForgottenPasswordUtil
         return responses;
     }
 
-    static String initializeAndSendToken(
+    static void initializeAndSendToken(
             final PwmRequest pwmRequest,
             final UserInfo userInfo,
-            final MessageSendMethod tokenSendMethod
+            final TokenDestinationItem tokenDestinationItem
 
     )
             throws PwmUnrecoverableException
@@ -477,7 +448,6 @@ public class ForgottenPasswordUtil
         final UserIdentity userIdentity = userInfo.getUserIdentity();
         final Map<String, String> tokenMapData = new LinkedHashMap<>();
 
-
         try
         {
             final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
@@ -497,30 +467,16 @@ public class ForgottenPasswordUtil
         }
 
         final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_CHALLENGE_TOKEN, pwmRequest.getLocale() );
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-
-        final RestTokenDataClient.TokenDestinationData inputDestinationData = new RestTokenDataClient.TokenDestinationData(
-                macroMachine.expandMacros( emailItemBean.getTo() ),
-                userInfo.getUserSmsNumber(),
-                null
-        );
-
-        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmRequest.getPwmApplication() );
-        final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString(
-                pwmRequest.getSessionLabel(),
-                inputDestinationData,
-                userIdentity,
-                pwmRequest.getLocale() );
-
-        final Set<String> destinationValues = new LinkedHashSet<>();
-        if ( outputDestrestTokenDataClient.getEmail() != null )
+        final MacroMachine.StringReplacer stringReplacer = ( matchedMacro, newValue ) ->
         {
-            destinationValues.add( outputDestrestTokenDataClient.getEmail() );
-        }
-        if ( outputDestrestTokenDataClient.getSms() != null )
-        {
-            destinationValues.add( outputDestrestTokenDataClient.getSms() );
-        }
+            if ( "@User:Email@".equals( matchedMacro )  )
+            {
+                return tokenDestinationItem.getValue();
+            }
+
+            return newValue;
+        };
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity, stringReplacer );
 
         final String tokenKey;
         final TokenPayload tokenPayload;
@@ -531,7 +487,7 @@ public class ForgottenPasswordUtil
                     new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
                     tokenMapData,
                     userIdentity,
-                    destinationValues
+                    Collections.singleton( tokenDestinationItem.getValue() )
             );
             tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
         }
@@ -542,15 +498,15 @@ public class ForgottenPasswordUtil
 
         final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_CHALLENGE_TOKEN_TEXT, pwmRequest.getLocale() );
 
-        final List<TokenDestinationItem.Type> sentTypes = TokenService.TokenSender.sendToken(
+        TokenService.TokenSender.sendToken(
                 TokenService.TokenSendInfo.builder()
                         .pwmApplication( pwmRequest.getPwmApplication() )
                         .userInfo( userInfo )
                         .macroMachine( macroMachine )
                         .configuredEmailSetting( emailItemBean )
-                        .tokenSendMethod( tokenSendMethod )
-                        .emailAddress( outputDestrestTokenDataClient.getEmail() )
-                        .smsNumber( outputDestrestTokenDataClient.getSms() )
+                        .tokenSendMethod( tokenDestinationItem.getType().getMessageSendMethod() )
+                        .emailAddress( tokenDestinationItem.getValue() )
+                        .smsNumber( tokenDestinationItem.getValue() )
                         .smsMessage( smsMessage )
                         .tokenKey( tokenKey )
                         .sessionLabel( pwmRequest.getSessionLabel() )
@@ -558,14 +514,38 @@ public class ForgottenPasswordUtil
         );
 
         StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_TOKENS_SENT );
+    }
 
-        final String displayDestAddress = TokenService.TokenSender.figureDisplayString(
-                pwmRequest.getConfig(),
-                sentTypes,
-                outputDestrestTokenDataClient.getEmail(),
-                outputDestrestTokenDataClient.getSms()
+    private static TokenDestinationItem invokeExternalTokenDestRestClient(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity,
+            final TokenDestinationItem tokenDestinationItem
+    )
+            throws PwmUnrecoverableException
+    {
+        final RestTokenDataClient.TokenDestinationData inputDestinationData = new RestTokenDataClient.TokenDestinationData(
+                tokenDestinationItem.getType() == TokenDestinationItem.Type.email ? tokenDestinationItem.getValue() : null,
+                tokenDestinationItem.getType() == TokenDestinationItem.Type.sms ? tokenDestinationItem.getValue() : null,
+                tokenDestinationItem.getDisplay()
         );
-        return displayDestAddress;
+
+        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmRequest.getPwmApplication() );
+        final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString(
+                pwmRequest.getSessionLabel(),
+                inputDestinationData,
+                userIdentity,
+                pwmRequest.getLocale() );
+
+        final String outputValue = tokenDestinationItem.getType() == TokenDestinationItem.Type.email
+                ? outputDestrestTokenDataClient.getEmail()
+                : outputDestrestTokenDataClient.getSms();
+
+        return TokenDestinationItem.builder()
+                .type( tokenDestinationItem.getType() )
+                .display( outputDestrestTokenDataClient.getDisplayValue() )
+                .value( outputValue )
+                .id( tokenDestinationItem.getId() )
+                .build();
     }
 
     static void doActionSendNewPassword( final PwmRequest pwmRequest )
@@ -738,8 +718,7 @@ public class ForgottenPasswordUtil
                 false,
                 Collections.singleton( IdentityVerificationMethod.ATTRIBUTES ),
                 Collections.emptySet(),
-                0,
-                MessageSendMethod.NONE
+                0
         );
 
         forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
@@ -982,11 +961,6 @@ public class ForgottenPasswordUtil
         final Configuration config = pwmApplication.getConfig();
         final ForgottenPasswordProfile forgottenPasswordProfile = config.getForgottenPasswordProfiles().get( forgottenPasswordProfileID );
 
-        final MessageSendMethod tokenSendMethod = config.getForgottenPasswordProfiles().get( forgottenPasswordProfileID ).readSettingAsEnum(
-                PwmSetting.RECOVERY_TOKEN_SEND_METHOD,
-                MessageSendMethod.class
-        );
-
         final Set<IdentityVerificationMethod> requiredRecoveryVerificationMethods = forgottenPasswordProfile.requiredRecoveryAuthenticationMethods();
         final Set<IdentityVerificationMethod> optionalRecoveryVerificationMethods = forgottenPasswordProfile.optionalRecoveryAuthenticationMethods();
         final int minimumOptionalRecoveryAuthMethods = forgottenPasswordProfile.getMinOptionalRequired();
@@ -996,8 +970,7 @@ public class ForgottenPasswordUtil
                 allowWhenLdapIntruderLocked,
                 requiredRecoveryVerificationMethods,
                 optionalRecoveryVerificationMethods,
-                minimumOptionalRecoveryAuthMethods,
-                tokenSendMethod
+                minimumOptionalRecoveryAuthMethods
         );
     }
 }

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

@@ -138,7 +138,7 @@ public class HelpdeskDetailInfoBean implements Serializable
                 userIdentity,
                 theUser.getChaiProvider()
         );
-        final MacroMachine macroMachine = new MacroMachine( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
 
         try
         {

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

@@ -788,7 +788,7 @@ public class HelpdeskServlet extends ControlledPwmServlet
                 userIdentity,
                 getChaiUser( pwmRequest, helpdeskProfile, userIdentity ).getChaiProvider()
         );
-        final MacroMachine macroMachine = new MacroMachine( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
         final String configuredTokenString = config.readAppProperty( AppProperty.HELPDESK_TOKEN_VALUE );
         final String tokenKey = macroMachine.expandMacros( configuredTokenString );
         final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_HELPDESK_TOKEN, pwmRequest.getLocale() );

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

@@ -268,7 +268,7 @@ class HelpdeskServletUtil
                 chaiUser.getChaiProvider()
         );
 
-        final MacroMachine macroMachine = new MacroMachine(
+        final MacroMachine macroMachine = MacroMachine.forUser(
                 pwmApplication,
                 pwmRequest.getSessionLabel(),
                 userInfo,

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

@@ -500,7 +500,7 @@ class NewUserUtils
                 .attributes( formValues )
                 .build();
 
-        return new MacroMachine( pwmApplication, sessionLabel, stubUserBean, stubLoginBean );
+        return MacroMachine.forUser( pwmApplication, sessionLabel, stubUserBean, stubLoginBean );
     }
 
     @SuppressWarnings( "checkstyle:MethodLength" )

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

@@ -619,7 +619,7 @@ class PeopleSearchDataReader
                 userIdentity,
                 chaiProvider
         );
-        return new MacroMachine( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
+        return MacroMachine.forUser( pwmRequest.getPwmApplication(), pwmRequest.getSessionLabel(), userInfo, null );
     }
 
     void checkIfUserIdentityViewable(

+ 8 - 0
server/src/main/java/password/pwm/ldap/UserInfo.java

@@ -78,8 +78,16 @@ public interface UserInfo
 
     String getUserEmailAddress( ) throws PwmUnrecoverableException;
 
+    String getUserEmailAddress2( ) throws PwmUnrecoverableException;
+
+    String getUserEmailAddress3( ) throws PwmUnrecoverableException;
+
     String getUserSmsNumber( ) throws PwmUnrecoverableException;
 
+    String getUserSmsNumber2( ) throws PwmUnrecoverableException;
+
+    String getUserSmsNumber3( ) throws PwmUnrecoverableException;
+
     String getUserGuid( ) throws PwmUnrecoverableException;
 
     ResponseInfoBean getResponseInfoBean( ) throws PwmUnrecoverableException;

+ 6 - 0
server/src/main/java/password/pwm/ldap/UserInfoBean.java

@@ -50,7 +50,13 @@ public class UserInfoBean implements UserInfo
     private final UserIdentity userIdentity;
     private final String username;
     private final String userEmailAddress;
+    private final String userEmailAddress2;
+    private final String userEmailAddress3;
+
     private final String userSmsNumber;
+    private final String userSmsNumber2;
+    private final String userSmsNumber3;
+
     private final String userGuid;
 
     /**

+ 32 - 0
server/src/main/java/password/pwm/ldap/UserInfoReader.java

@@ -544,6 +544,22 @@ public class UserInfoReader implements UserInfo
         return readStringAttribute( ldapEmailAttribute );
     }
 
+    @Override
+    public String getUserEmailAddress2( ) throws PwmUnrecoverableException
+    {
+        final LdapProfile ldapProfile = getUserIdentity().getLdapProfile( pwmApplication.getConfig() );
+        final String ldapEmailAttribute = ldapProfile.readSettingAsString( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE_2 );
+        return readStringAttribute( ldapEmailAttribute );
+    }
+
+    @Override
+    public String getUserEmailAddress3( ) throws PwmUnrecoverableException
+    {
+        final LdapProfile ldapProfile = getUserIdentity().getLdapProfile( pwmApplication.getConfig() );
+        final String ldapEmailAttribute = ldapProfile.readSettingAsString( PwmSetting.EMAIL_USER_MAIL_ATTRIBUTE_3 );
+        return readStringAttribute( ldapEmailAttribute );
+    }
+
     @Override
     public String getUserSmsNumber( ) throws PwmUnrecoverableException
     {
@@ -552,6 +568,22 @@ public class UserInfoReader implements UserInfo
         return readStringAttribute( ldapSmsAttribute );
     }
 
+    @Override
+    public String getUserSmsNumber2( ) throws PwmUnrecoverableException
+    {
+        final LdapProfile ldapProfile = getUserIdentity().getLdapProfile( pwmApplication.getConfig() );
+        final String ldapSmsAttribute = ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE_2 );
+        return readStringAttribute( ldapSmsAttribute );
+    }
+
+    @Override
+    public String getUserSmsNumber3( ) throws PwmUnrecoverableException
+    {
+        final LdapProfile ldapProfile = getUserIdentity().getLdapProfile( pwmApplication.getConfig() );
+        final String ldapSmsAttribute = ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE_3 );
+        return readStringAttribute( ldapSmsAttribute );
+    }
+
     @Override
     public String getUserGuid( ) throws PwmUnrecoverableException
     {

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

@@ -690,7 +690,7 @@ public class IntruderManager implements PwmService
                     userIdentity, locale
             );
 
-            final MacroMachine macroMachine = new MacroMachine(
+            final MacroMachine macroMachine = MacroMachine.forUser(
                     pwmApplication,
                     sessionLabel,
                     userInfo,

+ 2 - 1
server/src/main/java/password/pwm/util/java/JavaHelper.java

@@ -146,12 +146,13 @@ public class JavaHelper
     public static long pause( final long sleepTimeMS )
     {
         final long startTime = System.currentTimeMillis();
+        final long sliceTime = Math.max( 5, sleepTimeMS / 10 );
         do
         {
             try
             {
                 final long sleepTime = sleepTimeMS - ( System.currentTimeMillis() - startTime );
-                Thread.sleep( sleepTime > 0 ? sleepTime : 5 );
+                Thread.sleep( Math.min( sleepTime, sliceTime ) );
             }
             catch ( InterruptedException e )
             {

+ 32 - 16
server/src/main/java/password/pwm/util/macro/MacroMachine.java

@@ -54,6 +54,7 @@ public class MacroMachine
     private final SessionLabel sessionLabel;
     private final UserInfo userInfo;
     private final LoginInfoBean loginInfoBean;
+    private final StringReplacer stringReplacer;
 
     private static final Map<MacroImplementation.Scope, Map<Pattern, MacroImplementation>> BUILTIN_MACROS = makeImplementations();
 
@@ -61,13 +62,15 @@ public class MacroMachine
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
             final UserInfo userInfo,
-            final LoginInfoBean loginInfoBean
+            final LoginInfoBean loginInfoBean,
+            final StringReplacer stringReplacer
     )
     {
         this.pwmApplication = pwmApplication;
         this.sessionLabel = sessionLabel;
         this.userInfo = userInfo;
         this.loginInfoBean = loginInfoBean;
+        this.stringReplacer = stringReplacer;
     }
 
     private static Map<MacroImplementation.Scope, Map<Pattern, MacroImplementation>> makeImplementations( )
@@ -122,14 +125,6 @@ public class MacroMachine
     public String expandMacros(
             final String input
     )
-    {
-        return expandMacros( input, null );
-    }
-
-    public String expandMacros(
-            final String input,
-            final StringReplacer stringReplacer
-    )
     {
         if ( input == null )
         {
@@ -193,7 +188,7 @@ public class MacroMachine
                         final Matcher matcher = pattern.matcher( workingString );
                         if ( matcher.find() )
                         {
-                            workingString = doReplace( workingString, pwmMacro, matcher, stringReplacer, macroRequestInfo );
+                            workingString = doReplace( workingString, pwmMacro, matcher, macroRequestInfo );
                             if ( workingString.equals( previousString ) )
                             {
                                 LOGGER.warn( sessionLabel, "macro replace was called but input string was not modified.  "
@@ -238,7 +233,6 @@ public class MacroMachine
             final String input,
             final MacroImplementation macroImplementation,
             final Matcher matcher,
-            final StringReplacer stringReplacer,
             final MacroImplementation.MacroRequestInfo macroRequestInfo
     )
     {
@@ -299,7 +293,7 @@ public class MacroMachine
 
     public static MacroMachine forStatic( )
     {
-        return new MacroMachine( null, null, null, null );
+        return new MacroMachine( null, null, null, null, null );
     }
 
     public interface StringReplacer
@@ -316,15 +310,24 @@ public class MacroMachine
         return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getSessionLabel(), userIdentity );
     }
 
+    public static MacroMachine forUser(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity,
+            final StringReplacer stringReplacer
+    )
+            throws PwmUnrecoverableException
+    {
+        return forUser( pwmRequest.getPwmApplication(), pwmRequest.getLocale(), pwmRequest.getSessionLabel(), userIdentity, stringReplacer );
+    }
+
     public static MacroMachine forUser(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
             final UserInfo userInfo,
             final LoginInfoBean loginInfoBean
-
     )
     {
-        return new MacroMachine( pwmApplication, sessionLabel, userInfo, loginInfoBean );
+        return new MacroMachine( pwmApplication, sessionLabel, userInfo, loginInfoBean, null );
     }
 
     public static MacroMachine forUser(
@@ -336,7 +339,20 @@ public class MacroMachine
             throws PwmUnrecoverableException
     {
         final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
-        return new MacroMachine( pwmApplication, sessionLabel, userInfoBean, null );
+        return new MacroMachine( pwmApplication, sessionLabel, userInfoBean, null, null );
+    }
+
+    public static MacroMachine forUser(
+            final PwmApplication pwmApplication,
+            final Locale userLocale,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity,
+            final StringReplacer stringReplacer
+    )
+            throws PwmUnrecoverableException
+    {
+        final UserInfo userInfoBean = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, sessionLabel, userIdentity, userLocale );
+        return new MacroMachine( pwmApplication, sessionLabel, userInfoBean, null, stringReplacer );
     }
 
     public static MacroMachine forNonUserSpecific(
@@ -345,6 +361,6 @@ public class MacroMachine
     )
             throws PwmUnrecoverableException
     {
-        return new MacroMachine( pwmApplication, sessionLabel, null, null );
+        return new MacroMachine( pwmApplication, sessionLabel, null, null, null );
     }
 }

+ 3 - 3
server/src/main/java/password/pwm/util/operations/PasswordUtility.java

@@ -348,7 +348,7 @@ public class PasswordUtility
                 final LoginInfoBean clonedLoginInfoBean = JsonUtil.cloneUsingJson( pwmSession.getLoginInfoBean(), LoginInfoBean.class );
                 clonedLoginInfoBean.setUserCurrentPassword( newPassword );
 
-                final MacroMachine macroMachine = new MacroMachine(
+                final MacroMachine macroMachine = MacroMachine.forUser(
                         pwmApplication,
                         pwmSession.getLabel(),
                         pwmSession.getUserInfo(),
@@ -515,7 +515,7 @@ public class PasswordUtility
                 final LoginInfoBean loginInfoBean = new LoginInfoBean();
                 loginInfoBean.setUserCurrentPassword( newPassword );
 
-                final MacroMachine macroMachine = new MacroMachine(
+                final MacroMachine macroMachine = MacroMachine.forUser(
                         pwmApplication,
                         sessionLabel,
                         userInfo,
@@ -1170,7 +1170,7 @@ public class PasswordUtility
 
         final MacroMachine macroMachine = userInfo == null
                 ? null
-                : new MacroMachine(
+                : MacroMachine.forUser(
                 pwmApplication,
                 pwmSession.getLabel(),
                 userInfo,

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

@@ -758,6 +758,16 @@
             <value><![CDATA[mail]]></value>
         </default>
     </setting>
+    <setting hidden="false" key="email.userMailAttribute2" level="2" required="false">
+        <ldapPermission actor="proxy" access="read"/>
+        <regex>^[a-zA-Z][a-zA-Z0-9-]*$</regex>
+        <default/>
+    </setting>
+    <setting hidden="false" key="email.userMailAttribute3" level="2" required="false">
+        <ldapPermission actor="proxy" access="read"/>
+        <regex>^[a-zA-Z][a-zA-Z0-9-]*$</regex>
+        <default/>
+    </setting>
     <setting hidden="false" key="email.queueMaxAge" level="2" required="true">
         <default>
             <value>3600</value>
@@ -888,6 +898,16 @@
             <value><![CDATA[personalMobile]]></value>
         </default>
     </setting>
+    <setting hidden="false" key="sms.userSmsAttribute2" level="1" required="false">
+        <ldapPermission actor="proxy" access="read"/>
+        <regex>^[a-zA-Z][a-zA-Z0-9-]*$</regex>
+        <default/>
+    </setting>
+    <setting hidden="false" key="sms.userSmsAttribute3" level="1" required="false">
+        <ldapPermission actor="proxy" access="read"/>
+        <regex>^[a-zA-Z][a-zA-Z0-9-]*$</regex>
+        <default/>
+    </setting>
     <setting hidden="false" key="sms.queueMaxAge" level="2" required="true">
         <default>
             <value>300</value>

+ 9 - 1
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -337,6 +337,8 @@ Setting_Description_email.updateguest=Define this template to send an email to u
 Setting_Description_email.updateProfile=Define this template to send an email to users after a profile update.
 Setting_Description_email.updateProfile.token=Define this template to send an email to users during the profile email validation.
 Setting_Description_email.userMailAttribute=Specify the LDAP attribute that contains the users' email address.
+Setting_Description_email.userMailAttribute2=Specify the secondary LDAP attribute that contains the users' email address.
+Setting_Description_email.userMailAttribute3=Specify the tertiary LDAP attribute that contains the users' email address.
 Setting_Description_enableSessionVerification=Enable this option to verify browser sessions using an HTTP redirect and verification code.  This verification proves that the browser can correctly establish a session with the server.  Verification proves the browser either supports cookies or URL sessions (if enabled) and the communication channel between browser and application server is 'sticky' when there are multiple server instances.  Additionally, it helps prevent some types of XSS attacks.<br/><br/>The pre-loaded browser cache shows a "please wait" screen to the user during the verification.  This has the added benefit that the browser "pre-caches" many of the HTTP resources (JavaScript, CSS, images, and so forth) before it loads any actual pages.
 Setting_Description_events.alert.dailySummary.enable=Enable this option to send an email alert once a day (at 0\:00 GMT) that contains a summary of the day's statistics and health.
 Setting_Description_events.audit.maxAge=Specify the maximum age (in seconds) of the local audit event log.  The default is 30 days.
@@ -671,7 +673,9 @@ Setting_Description_sms.responseOkRegex=Specify the regular expression that you
 Setting_Description_sms.senderID=Specify the alphanumerical sender identification. If blank, the provider uses a default or anonymous sender identification. In most cases, the SMS provider must validate the sender ID. Contact your provider for values that you can use as a valid sender identification.
 Setting_Description_sms.successResultCodes=Specify the HTTP Result codes that @PwmAppName@ consideres as successful send attempts.
 Setting_Description_sms.updateProfile.token.message=Specify the text of the SMS @PwmAppName@ sends during the profile update SMS phone number verification.
-Setting_Description_sms.userSmsAttribute=Specify the users' LDAP attribute containing the users' mobile phone numbers for SMS.
+Setting_Description_sms.userSmsAttribute=Specify the LDAP attribute containing the users' mobile phone numbers for SMS.
+Setting_Description_sms.userSmsAttribute2=Specify the secondary LDAP attribute containing the users' mobile phone numbers for SMS.
+Setting_Description_sms.userSmsAttribute3=Specify the tertiary LDAP attribute containing the users' mobile phone numbers for SMS.
 Setting_Description_sms.useUrlShortener=Enable this option to use a URL shortener service like tinyurl.com, bit.ly, and goo.gl. This enables searching the SMS text for HTTP and HTTPS URLs and replaces them with a shortened version. You can configure the service @PwmAppName@ uses under the Miscellaneous options. Enusre that you check View and Advanced options.
 Setting_Description_template.ldap=<p>This setting changes the default values throughout this configuration to reasonable values based on this value.  Only default (non-modified) settings are affected.  Any settings that have been modified from the default are unaffected.</p><p>You can change this setting at any time but use caution when doing so as the overall behavior of the application might change.  After changing this setting, review and test @PwmAppName@ to ensure the desired behavior occurs.</p>
 Setting_Description_template.storage=<p>This setting changes the default values throughout this configuration to reasonable values based on this value.  Only default (non-modified) settings are affected.  Any settings that have been modified from the default are unaffected.</p><p>You can change this setting at any time but use caution when doing so as the overall behavior of the application might change.  After changing this setting review and test @PwmAppName@ to ensure the desired behavior occurs.</p>
@@ -824,6 +828,8 @@ Setting_Label_email.updateguest=Guest Registration Update Email
 Setting_Label_email.updateProfile.token=Update Profile Email Verification
 Setting_Label_email.updateProfile=Update Profile Email
 Setting_Label_email.userMailAttribute=User Email Attribute
+Setting_Label_email.userMailAttribute2=Secondary User Email Attribute
+Setting_Label_email.userMailAttribute3=Tertiary User Email Attribute
 Setting_Label_enableSessionVerification=Sticky Session Verification
 Setting_Label_events.alert.dailySummary.enable=Daily Summary Alerts
 Setting_Label_events.audit.maxAge=LocalDB Audit Events Storage Max Age
@@ -1159,6 +1165,8 @@ Setting_Label_sms.senderID=SMS Sender ID
 Setting_Label_sms.successResultCodes=Successful HTTP Result Codes
 Setting_Label_sms.updateProfile.token.message=Update Profile SMS Verification Text
 Setting_Label_sms.userSmsAttribute=SMS Destination Address LDAP Attribute
+Setting_Label_sms.userSmsAttribute2=Secondary SMS Destination Address LDAP Attribute
+Setting_Label_sms.userSmsAttribute3=Tertiary SMS Destination Address LDAP Attribute
 Setting_Label_sms.useUrlShortener=Use URL Shortener
 Setting_Label_template.ldap=LDAP Vendor Default Settings
 Setting_Label_template.storage=Storage Default Settings

+ 18 - 2
server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -113,13 +113,29 @@
                 <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getPasswordLastModifiedTime())%></td>
             </tr>
             <tr>
-                <td class="key">Email Address</td>
+                <td class="key">Email Address 1</td>
                 <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getUserEmailAddress())%></td>
             </tr>
             <tr>
-                <td class="key">Phone Number</td>
+                <td class="key">Email Address 2</td>
+                <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getUserEmailAddress2())%></td>
+            </tr>
+            <tr>
+                <td class="key">Email Address 3</td>
+                <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getUserEmailAddress3())%></td>
+            </tr>
+            <tr>
+                <td class="key">Phone Number 1</td>
                 <td><%=JspUtility.freindlyWrite(pageContext, userDebugDataBean.getUserInfo().getUserSmsNumber())%></td>
             </tr>
+            <tr>
+                <td class="key">Phone Number 2</td>
+                <td><%=JspUtility.freindlyWrite(pageContext, userDebugDataBean.getUserInfo().getUserSmsNumber2())%></td>
+            </tr>
+            <tr>
+                <td class="key">Phone Number 3</td>
+                <td><%=JspUtility.freindlyWrite(pageContext, userDebugDataBean.getUserInfo().getUserSmsNumber3())%></td>
+            </tr>
             <tr>
                 <td class="key">Username</td>
                 <td><%=JspUtility.freindlyWrite(pageContext, userInfo.getUserID())%></td>

+ 5 - 3
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-entertoken.jsp

@@ -38,7 +38,7 @@
     <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
         <% final ForgottenPasswordBean fpb = JspUtility.getSessionBean(pageContext, ForgottenPasswordBean.class); %>
-        <% final String destination = fpb.getProgress().getTokenSentAddress(); %>
+        <% final String destination = fpb.getProgress().getTokenDestination().getDisplay(); %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=destination%>"/></p>
         <% if (resendEnabled) { %>
         <p><pwm:display key="Display_TokenResend"/></p>
@@ -108,11 +108,13 @@
     <div class="push"></div>
 </div>
 <form id="form-goBack" action="<pwm:current-url/>" method="post">
-    <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.verificationChoice%>"/>
+    <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ForgottenPasswordServlet.ResetType.clearTokenDestination%>"/>
     <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
 </form>
 <form id="form-cancel" action="<pwm:current-url/>" method="post">
-    <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
     <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
 </form>
 <%@ include file="fragment/footer.jsp" %>

+ 39 - 50
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp

@@ -21,6 +21,7 @@
 --%>
 
 <%@ page import="password.pwm.bean.TokenDestinationItem" %>
+<%@ page import="password.pwm.http.servlet.forgottenpw.ForgottenPasswordServlet" %>
 <%@ page import="java.util.List" %>
 
 <!DOCTYPE html>
@@ -39,54 +40,30 @@
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <p><pwm:display key="Display_RecoverTokenSendChoices"/></p>
         <table class="noborder">
+            <% for (final TokenDestinationItem item : tokenDestinationItems) { %>
             <tr>
                 <td style="text-align: center">
                     <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form">
                         <button class="btn" type="submit" name="submitBtn">
+                            <% if (item.getType() == TokenDestinationItem.Type.email) { %>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-file-text"></span></pwm:if>
                             <pwm:display key="Button_Email"/>
-                        </button>
-                        <input type="hidden" name="choice" value="email"/>
-                        <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.tokenChoice%>"/>
-                        <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
-                    </form>
-                </td>
-                <td>
-                    <pwm:display key="Display_RecoverTokenSendChoiceEmail"/>
-                </td>
-            </tr>
-            <pwm:if test="<%=PwmIfTest.showMaskedTokenSelection%>">
-                <tr>
-                    <td>
-                    </td>
-                    <td>
-                        <% for (final TokenDestinationItem item : tokenDestinationItems) { %>
-                        <% if (item.getType() == TokenDestinationItem.Type.email) { %>
-                        <%=item.getDisplay()%>
-                        <% } %>
-                        <% } %>
-                    </td>
-                </tr>
-                <tr>
-                    <td>
-                        &nbsp;
-                    </td>
-                </tr>
-            </pwm:if>
-            <tr>
-                <td style="text-align: center">
-                    <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form">
-                        <button class="btn" type="submit" name="submitBtn">
+                            <% } else { %>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-phone"></span></pwm:if>
                             <pwm:display key="Button_SMS"/>
+                            <% } %>
                         </button>
-                        <input type="hidden" name="choice" value="sms"/>
+                        <input type="hidden" name="choice" value="<%=item.getId()%>"/>
                         <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.tokenChoice%>"/>
                         <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                     </form>
                 </td>
                 <td>
+                    <% if (item.getType() == TokenDestinationItem.Type.email) { %>
+                    <pwm:display key="Display_RecoverTokenSendChoiceEmail"/>
+                    <% } else { %>
                     <pwm:display key="Display_RecoverTokenSendChoiceSMS"/>
+                    <% } %>
                 </td>
             </tr>
             <pwm:if test="<%=PwmIfTest.showMaskedTokenSelection%>">
@@ -94,11 +71,7 @@
                     <td>
                     </td>
                     <td>
-                        <% for (final TokenDestinationItem item : tokenDestinationItems) { %>
-                        <% if (item.getType() == TokenDestinationItem.Type.sms) { %>
                         <%=item.getDisplay()%>
-                        <% } %>
-                        <% } %>
                     </td>
                 </tr>
                 <tr>
@@ -107,21 +80,37 @@
                     </td>
                 </tr>
             </pwm:if>
-            <tr>
-                <td>
-                    <%@ include file="/WEB-INF/jsp/fragment/forgottenpassword-cancel.jsp" %>
-                </td>
-                <td>
-                    &nbsp;
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    &nbsp;
-                </td>
-            </tr>
+            <% } %>
         </table>
+        <div>
+        <div class="buttonbar">
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
+                <% if ("true".equals(JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordOptionalPageView))) { %>
+                <button type="submit" id="button-goBack" name="button-goBack" class="btn">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
+                    <pwm:display key="Button_GoBack"/>
+                </button>
+                <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
+                <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ForgottenPasswordServlet.ResetType.gotoSearch%>"/>
+                <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+            <% } %>
+            <pwm:if test="<%=PwmIfTest.showCancel%>">
+            <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search" class="pwm-form" autocomplete="off">
+                <button type="submit" name="button" class="btn" id="button-sendReset">
+                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span></pwm:if>
+                    <pwm:display key="Button_Cancel"/>
+                </button>
+                <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.reset%>"/>
+                <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ForgottenPasswordServlet.ResetType.exitForgottenPassword%>"/>
+                <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
+            </form>
+            </pwm:if>
+        </div>
+        </div>
+        </form>
     </div>
+
     <div class="push"></div>
 </div>
 <%@ include file="fragment/footer.jsp" %>