Browse Source

activate user token updates and fixes

Jason Rivard 7 years ago
parent
commit
ede1128865
22 changed files with 1330 additions and 1162 deletions
  1. 4 4
      server/src/main/java/password/pwm/bean/TokenDestinationItem.java
  2. 2 0
      server/src/main/java/password/pwm/config/PwmSetting.java
  3. 1 0
      server/src/main/java/password/pwm/http/JspUrl.java
  4. 4 1
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  5. 16 70
      server/src/main/java/password/pwm/http/bean/ActivateUserBean.java
  6. 0 879
      server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java
  7. 2 1
      server/src/main/java/password/pwm/http/servlet/PwmServletDefinition.java
  8. 484 0
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserServlet.java
  9. 390 0
      server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java
  10. 1 1
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  11. 16 136
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  12. 5 5
      server/src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java
  13. 16 3
      server/src/main/java/password/pwm/svc/token/TokenDestinationDisplayMasker.java
  14. 3 4
      server/src/main/java/password/pwm/svc/token/TokenService.java
  15. 214 0
      server/src/main/java/password/pwm/svc/token/TokenUtil.java
  16. 13 32
      server/src/main/java/password/pwm/ws/client/rest/RestTokenDataClient.java
  17. 6 0
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  18. 12 13
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  19. 1 2
      server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp
  20. 21 9
      server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp
  21. 117 0
      server/src/main/webapp/WEB-INF/jsp/activateuser-tokenchoice.jsp
  22. 2 2
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-tokenchoice.jsp

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

@@ -29,7 +29,7 @@ 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.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.secure.SecureService;
 
@@ -76,7 +76,7 @@ public class TokenDestinationItem implements Serializable
         final Configuration configuration = pwmApplication.getConfig();
         final SecureService secureService = pwmApplication.getSecureService();
 
-        final ValueObfuscator valueObfuscator = new ValueObfuscator( configuration );
+        final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( configuration );
 
         final Map<String, TokenDestinationItem> results = new LinkedHashMap<>(  );
 
@@ -93,7 +93,7 @@ public class TokenDestinationItem implements Serializable
                 final String idHash = secureService.hash( emailValue + Type.email.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
-                        .display( valueObfuscator.maskEmail( emailValue ) )
+                        .display( tokenDestinationDisplayMasker.maskEmail( emailValue ) )
                         .value( emailValue )
                         .type( Type.email )
                         .build();
@@ -114,7 +114,7 @@ public class TokenDestinationItem implements Serializable
                 final String idHash = secureService.hash( smsValue + Type.sms.name() );
                 final TokenDestinationItem item = TokenDestinationItem.builder()
                         .id( idHash )
-                        .display( valueObfuscator.maskPhone( smsValue ) )
+                        .display( tokenDestinationDisplayMasker.maskPhone( smsValue ) )
                         .value( smsValue )
                         .type( Type.sms )
                         .build();

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

@@ -615,6 +615,8 @@ public enum PwmSetting
             "token.lifetime", PwmSettingSyntax.DURATION, PwmSettingCategory.TOKEN ),
     TOKEN_LDAP_ATTRIBUTE(
             "token.ldap.attribute", PwmSettingSyntax.STRING, PwmSettingCategory.TOKEN ),
+    TOKEN_ENABLE_VALUE_MASKING(
+            "token.valueMasking.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.TOKEN ),
 
     // OTP
     OTP_PROFILE_LIST(

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

@@ -39,6 +39,7 @@ public enum JspUrl
     ADMIN_DEBUG( "admin-user-debug.jsp" ),
     ACTIVATE_USER( "activateuser.jsp" ),
     ACTIVATE_USER_AGREEMENT( "activateuser-agreement.jsp" ),
+    ACTIVATE_USER_TOKEN_CHOICE( "activateuser-tokenchoice.jsp" ),
     ACTIVATE_USER_ENTER_CODE( "activateuser-entercode.jsp" ),
     LOGIN( "login.jsp" ),
     LOGIN_PW_ONLY( "login-passwordonly.jsp" ),

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

@@ -79,7 +79,6 @@ public enum PwmRequestAttribute
     ForgottenPasswordInstructions,
     ForgottenPasswordOtpRecord,
     ForgottenPasswordResendTokenEnabled,
-    ForgottenPasswordTokenDestItems,
     ForgottenPasswordInhibitPasswordReset,
 
     GuestCurrentExpirationDate,
@@ -96,4 +95,8 @@ public enum PwmRequestAttribute
 
     UserDebugData,
     AppDashboardData,
+
+    TokenDestItems,
+
+    ShowGoBackButton,
 }

+ 16 - 70
server/src/main/java/password/pwm/http/bean/ActivateUserBean.java

@@ -22,92 +22,38 @@
 
 package password.pwm.http.bean;
 
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.option.SessionBeanMode;
 
 import java.util.Collections;
 import java.util.Set;
 
+@Data
+@EqualsAndHashCode( callSuper = false )
 public class ActivateUserBean extends PwmSessionBean
 {
-    private boolean tokenIssued;
+    @SerializedName( "tp" )
     private boolean tokenPassed;
+
+    @SerializedName( "ap" )
     private boolean agreementPassed;
+
+    @SerializedName( "v" )
     private boolean formValidated;
-    private String tokenDisplayText;
-    private String agreementText;
 
+    @SerializedName( "u" )
     private UserIdentity userIdentity;
 
-    public boolean isTokenIssued( )
-    {
-        return tokenIssued;
-    }
+    @SerializedName( "ts" )
+    private boolean tokenSent;
 
-    public void setTokenIssued( final boolean tokenIssued )
-    {
-        this.tokenIssued = tokenIssued;
-    }
-
-    public boolean isAgreementPassed( )
-    {
-        return agreementPassed;
-    }
-
-    public void setAgreementPassed( final boolean agreementPassed )
-    {
-        this.agreementPassed = agreementPassed;
-    }
-
-    public boolean isFormValidated( )
-    {
-        return formValidated;
-    }
-
-    public void setFormValidated( final boolean formValidated )
-    {
-        this.formValidated = formValidated;
-    }
-
-    public boolean isTokenPassed( )
-    {
-        return tokenPassed;
-    }
+    @SerializedName( "td" )
+    private TokenDestinationItem tokenDestination;
 
-    public void setTokenPassed( final boolean tokenPassed )
-    {
-        this.tokenPassed = tokenPassed;
-    }
-
-    public UserIdentity getUserIdentity( )
-    {
-        return userIdentity;
-    }
-
-    public void setUserIdentity( final UserIdentity userIdentity )
-    {
-        this.userIdentity = userIdentity;
-    }
-
-    public String getTokenDisplayText( )
-    {
-        return tokenDisplayText;
-    }
-
-    public void setTokenDisplayText( final String tokenSendAddress )
-    {
-        this.tokenDisplayText = tokenSendAddress;
-    }
-
-    public String getAgreementText( )
-    {
-        return agreementText;
-    }
-
-    public void setAgreementText( final String agreementText )
-    {
-        this.agreementText = agreementText;
-    }
 
     public Type getType( )
     {

+ 0 - 879
server/src/main/java/password/pwm/http/servlet/ActivateUserServlet.java

@@ -1,879 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package password.pwm.http.servlet;
-
-import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiOperationException;
-import com.novell.ldapchai.exception.ChaiUnavailableException;
-import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
-import com.novell.ldapchai.provider.ChaiProvider;
-import password.pwm.PwmApplication;
-import password.pwm.PwmConstants;
-import password.pwm.bean.EmailItemBean;
-import password.pwm.bean.LocalSessionStateBean;
-import password.pwm.bean.LoginInfoBean;
-import password.pwm.bean.TokenDestinationItem;
-import password.pwm.bean.UserIdentity;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
-import password.pwm.config.option.MessageSendMethod;
-import password.pwm.config.profile.LdapProfile;
-import password.pwm.config.value.data.ActionConfiguration;
-import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.config.value.data.UserPermission;
-import password.pwm.error.ErrorInformation;
-import password.pwm.error.PwmDataValidationException;
-import password.pwm.error.PwmError;
-import password.pwm.error.PwmOperationalException;
-import password.pwm.error.PwmUnrecoverableException;
-import password.pwm.http.HttpMethod;
-import password.pwm.http.JspUrl;
-import password.pwm.http.PwmRequest;
-import password.pwm.http.PwmSession;
-import password.pwm.http.bean.ActivateUserBean;
-import password.pwm.i18n.Message;
-import password.pwm.ldap.LdapPermissionTester;
-import password.pwm.ldap.UserInfo;
-import password.pwm.ldap.UserInfoFactory;
-import password.pwm.ldap.auth.AuthenticationType;
-import password.pwm.ldap.auth.PwmAuthenticationSource;
-import password.pwm.ldap.auth.SessionAuthenticator;
-import password.pwm.ldap.search.SearchConfiguration;
-import password.pwm.ldap.search.UserSearchEngine;
-import password.pwm.svc.event.AuditEvent;
-import password.pwm.svc.event.AuditRecord;
-import password.pwm.svc.event.AuditRecordFactory;
-import password.pwm.svc.stats.Statistic;
-import password.pwm.svc.token.TokenPayload;
-import password.pwm.svc.token.TokenService;
-import password.pwm.svc.token.TokenType;
-import password.pwm.util.CaptchaUtility;
-import password.pwm.util.PostChangePasswordAction;
-import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.macro.MacroMachine;
-import password.pwm.util.operations.ActionExecutor;
-import password.pwm.util.operations.PasswordUtility;
-import password.pwm.ws.client.rest.RestTokenDataClient;
-
-import javax.servlet.ServletException;
-import javax.servlet.annotation.WebServlet;
-import java.io.IOException;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * User interaction servlet for creating new users (self registration).
- *
- * @author Jason D. Rivard
- */
-@WebServlet(
-        name = "ActivateUserServlet",
-        urlPatterns = {
-                PwmConstants.URL_PREFIX_PUBLIC + "/activate",
-                PwmConstants.URL_PREFIX_PUBLIC + "/activate/*",
-                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser",
-                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser/*",
-        }
-)
-public class ActivateUserServlet extends AbstractPwmServlet
-{
-
-    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserServlet.class );
-
-    public enum ActivateUserAction implements AbstractPwmServlet.ProcessAction
-    {
-        activate( HttpMethod.POST ),
-        enterCode( HttpMethod.POST, HttpMethod.GET ),
-        reset( HttpMethod.POST ),
-        agree( HttpMethod.POST ),;
-
-        private final Collection<HttpMethod> method;
-
-        ActivateUserAction( final HttpMethod... method )
-        {
-            this.method = Collections.unmodifiableList( Arrays.asList( method ) );
-        }
-
-        public Collection<HttpMethod> permittedMethods( )
-        {
-            return method;
-        }
-    }
-
-    protected ActivateUserAction readProcessAction( final PwmRequest request )
-            throws PwmUnrecoverableException
-    {
-        try
-        {
-            return ActivateUserAction.valueOf( request.readParameterAsString( PwmConstants.PARAM_ACTION_REQUEST ) );
-        }
-        catch ( IllegalArgumentException e )
-        {
-            return null;
-        }
-    }
-
-
-    protected void processAction( final PwmRequest pwmRequest )
-            throws ServletException, ChaiUnavailableException, IOException, PwmUnrecoverableException
-    {
-        //Fetch the session state bean.
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-
-        final Configuration config = pwmApplication.getConfig();
-
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-
-        if ( !config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_ENABLE ) )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "activate user is not enabled" );
-            pwmRequest.respondWithError( errorInformation );
-            return;
-        }
-
-        if ( pwmSession.isAuthenticated() )
-        {
-            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_USERAUTHENTICATED );
-            pwmRequest.respondWithError( errorInformation );
-            return;
-        }
-
-        final ActivateUserAction action = readProcessAction( pwmRequest );
-
-        // convert a url command like /pwm/public/NewUserServlet/12321321 to redirect with a process action.
-        if ( action == null )
-        {
-            if ( pwmRequest.convertURLtokenCommand( PwmServletDefinition.ActivateUser, ActivateUserAction.enterCode ) )
-            {
-                return;
-            }
-        }
-        else
-        {
-            switch ( action )
-            {
-                case activate:
-                    handleActivationRequest( pwmRequest );
-                    break;
-
-                case enterCode:
-                    handleEnterTokenCode( pwmRequest );
-                    break;
-
-                case reset:
-                    pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
-                    forwardToActivateUserForm( pwmRequest );
-                    return;
-
-                case agree:
-                    handleAgreeRequest( pwmRequest, activateUserBean );
-                    advanceToNextStage( pwmRequest );
-                    break;
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( action );
-
-            }
-        }
-
-        if ( !pwmRequest.getPwmResponse().isCommitted() )
-        {
-            this.advanceToNextStage( pwmRequest );
-        }
-    }
-
-    public void handleActivationRequest( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
-
-        if ( CaptchaUtility.captchaEnabledForRequest( pwmRequest ) )
-        {
-            if ( !CaptchaUtility.verifyReCaptcha( pwmRequest ) )
-            {
-                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_BAD_CAPTCHA_RESPONSE );
-                LOGGER.debug( pwmRequest, errorInfo );
-                setLastError( pwmRequest, errorInfo );
-                return;
-            }
-        }
-
-
-        pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
-        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
-
-        Map<FormConfiguration, String> formValues = new HashMap<>();
-        try
-        {
-            //read the values from the request
-            formValues = FormUtility.readFormValuesFromRequest( pwmRequest, configuredActivationForm,
-                    ssBean.getLocale() );
-
-            // check for intruders
-            pwmApplication.getIntruderManager().convenience().checkAttributes( formValues );
-
-            // read the context attr
-            final String contextParam = pwmRequest.readParameterAsString( PwmConstants.PARAM_CONTEXT );
-
-            // read the profile attr
-            final String ldapProfile = pwmRequest.readParameterAsString( PwmConstants.PARAM_LDAP_PROFILE );
-
-            // see if the values meet the configured form requirements.
-            FormUtility.validateFormValues( config, formValues, ssBean.getLocale() );
-
-            final String searchFilter = figureLdapSearchFilter( pwmRequest );
-
-            // read an ldap user object based on the params
-            final UserIdentity userIdentity;
-            {
-                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
-                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
-                        .contexts( Collections.singletonList( contextParam ) )
-                        .filter( searchFilter )
-                        .formValues( formValues )
-                        .ldapProfile( ldapProfile )
-                        .build();
-
-                userIdentity = userSearchEngine.performSingleUserSearch( searchConfiguration, pwmRequest.getSessionLabel() );
-            }
-
-            validateParamsAgainstLDAP( pwmRequest, formValues, userIdentity );
-
-            final List<UserPermission> userPermissions = config.readSettingAsUserPermission( PwmSetting.ACTIVATE_USER_QUERY_MATCH );
-            if ( !LdapPermissionTester.testUserPermissions( pwmApplication, pwmSession.getLabel(), userIdentity, userPermissions ) )
-            {
-                final String errorMsg = "user " + userIdentity + " attempted activation, but does not match query string";
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATE_NO_PERMISSION, errorMsg );
-                pwmApplication.getIntruderManager().convenience().markUserIdentity( userIdentity, pwmSession );
-                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-                throw new PwmUnrecoverableException( errorInformation );
-            }
-
-            final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-            activateUserBean.setUserIdentity( userIdentity );
-            activateUserBean.setFormValidated( true );
-            pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
-            pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
-        }
-        catch ( PwmOperationalException e )
-        {
-            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
-            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-            setLastError( pwmRequest, e.getErrorInformation() );
-            LOGGER.debug( pwmSession.getLabel(), e.getErrorInformation().toDebugStr() );
-        }
-
-        // redirect user to change password screen.
-        advanceToNextStage( pwmRequest );
-    }
-
-    private void handleAgreeRequest(
-            final PwmRequest pwmRequest,
-            final ActivateUserBean activateUserBean
-    )
-            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
-    {
-        LOGGER.debug( pwmRequest, "user accepted agreement" );
-
-        if ( !activateUserBean.isAgreementPassed() )
-        {
-            activateUserBean.setAgreementPassed( true );
-            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
-                    AuditEvent.AGREEMENT_PASSED,
-                    pwmRequest.getUserInfoIfLoggedIn(),
-                    pwmRequest.getSessionLabel(),
-                    "ActivateUser"
-            );
-            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
-        }
-    }
-
-
-    private void advanceToNextStage( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, IOException, ServletException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-
-        if ( !activateUserBean.isFormValidated() || activateUserBean.getUserIdentity() == null )
-        {
-            forwardToActivateUserForm( pwmRequest );
-            return;
-        }
-
-        final boolean tokenRequired = MessageSendMethod.NONE != MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-        if ( tokenRequired )
-        {
-            if ( !activateUserBean.isTokenIssued() )
-            {
-                try
-                {
-                    final Locale locale = pwmSession.getSessionStateBean().getLocale();
-                    initializeToken( pwmRequest, locale, activateUserBean.getUserIdentity() );
-                }
-                catch ( PwmOperationalException e )
-                {
-                    setLastError( pwmRequest, e.getErrorInformation() );
-                    forwardToActivateUserForm( pwmRequest );
-                    return;
-                }
-            }
-
-            if ( !activateUserBean.isTokenPassed() )
-            {
-                pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_ENTER_CODE );
-                return;
-            }
-        }
-
-        final String agreementText = config.readSettingAsLocalizedString(
-                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
-                pwmSession.getSessionStateBean().getLocale()
-        );
-        if ( agreementText != null && agreementText.length() > 0 && !activateUserBean.isAgreementPassed() )
-        {
-            if ( activateUserBean.getAgreementText() == null )
-            {
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, activateUserBean.getUserIdentity() );
-                final String expandedText = macroMachine.expandMacros( agreementText );
-                activateUserBean.setAgreementText( expandedText );
-            }
-            pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_AGREEMENT );
-            return;
-        }
-
-        try
-        {
-            activateUser( pwmRequest, activateUserBean.getUserIdentity() );
-            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_ActivateUser );
-        }
-        catch ( PwmOperationalException e )
-        {
-            LOGGER.debug( pwmRequest, e.getErrorInformation() );
-            pwmApplication.getIntruderManager().convenience().markUserIdentity( activateUserBean.getUserIdentity(), pwmSession );
-            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
-            pwmRequest.respondWithError( e.getErrorInformation() );
-        }
-    }
-
-    public void activateUser(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
-        if ( config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_UNLOCK ) )
-        {
-            try
-            {
-                theUser.unlockPassword();
-            }
-            catch ( ChaiOperationException e )
-            {
-                final String errorMsg = "error unlocking user " + userIdentity + ": " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                throw new PwmOperationalException( errorInformation );
-            }
-        }
-
-        try
-        {
-            {
-                // execute configured actions
-                LOGGER.debug( pwmSession.getLabel(), "executing configured pre-actions to user " + theUser.getEntryDN() );
-                final List<ActionConfiguration> configValues = config.readSettingAsAction( PwmSetting.ACTIVATE_USER_PRE_WRITE_ATTRIBUTES );
-                if ( configValues != null && !configValues.isEmpty() )
-                {
-                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-
-                    final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                            .setExpandPwmMacros( true )
-                            .setMacroMachine( macroMachine )
-                            .createActionExecutor();
-
-                    actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
-                }
-            }
-
-            //authenticate the pwm session
-            final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, PwmAuthenticationSource.USER_ACTIVATION );
-            sessionAuthenticator.authUserWithUnknownPassword( userIdentity, AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-
-            //ensure a change password is triggered
-            pwmSession.getLoginInfoBean().setType( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-            pwmSession.getLoginInfoBean().getAuthFlags().add( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
-            pwmSession.getLoginInfoBean().getLoginFlags().add( LoginInfoBean.LoginFlag.forcePwChange );
-
-
-            // mark the event log
-            pwmApplication.getAuditManager().submit( AuditEvent.ACTIVATE_USER, pwmSession.getUserInfo(), pwmSession );
-
-            // update the stats bean
-            pwmApplication.getStatisticsManager().incrementValue( Statistic.ACTIVATED_USERS );
-
-            // send email or sms
-            sendPostActivationNotice( pwmRequest );
-
-            // setup post-change attributes
-            final PostChangePasswordAction postAction = new PostChangePasswordAction()
-            {
-
-                public String getLabel( )
-                {
-                    return "ActivateUser write attributes";
-                }
-
-                public boolean doAction( final PwmSession pwmSession, final String newPassword )
-                        throws PwmUnrecoverableException
-                {
-                    try
-                    {
-                        {
-                            // execute configured actions
-                            LOGGER.debug( pwmSession.getLabel(), "executing post-activate configured actions to user " + userIdentity.toDisplayString() );
-
-                            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( pwmApplication );
-                            final List<ActionConfiguration> configValues = pwmApplication.getConfig().readSettingAsAction( PwmSetting.ACTIVATE_USER_POST_WRITE_ATTRIBUTES );
-
-                            final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
-                                    .setExpandPwmMacros( true )
-                                    .setMacroMachine( macroMachine )
-                                    .createActionExecutor();
-                            actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
-                        }
-                    }
-                    catch ( PwmOperationalException e )
-                    {
-                        final ErrorInformation info = new ErrorInformation(
-                                PwmError.ERROR_ACTIVATION_FAILURE,
-                                e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
-                        );
-                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
-                        newException.initCause( e );
-                        throw newException;
-                    }
-                    catch ( ChaiUnavailableException e )
-                    {
-                        final String errorMsg = "unable to reach ldap server while writing post-activate attributes: " + e.getMessage();
-                        final ErrorInformation info = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
-                        newException.initCause( e );
-                        throw newException;
-                    }
-                    return true;
-                }
-            };
-
-            pwmSession.getUserSessionDataCacheBean().addPostChangePasswordActions( "activateUserWriteAttributes", postAction );
-        }
-        catch ( ImpossiblePasswordPolicyException e )
-        {
-            final ErrorInformation info = new ErrorInformation( PwmError.ERROR_UNKNOWN, "unexpected ImpossiblePasswordPolicyException error while activating user" );
-            LOGGER.warn( pwmSession, info, e );
-            throw new PwmOperationalException( info );
-        }
-    }
-
-    protected static void validateParamsAgainstLDAP(
-            final PwmRequest pwmRequest,
-            final Map<FormConfiguration, String> formValues,
-            final UserIdentity userIdentity
-    )
-            throws ChaiUnavailableException, PwmDataValidationException, PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final String searchFilter = figureLdapSearchFilter( pwmRequest );
-        final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider( userIdentity.getLdapProfileID() );
-        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
-
-        for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
-        {
-            final FormConfiguration formItem = entry.getKey();
-            final String attrName = formItem.getName();
-            final String tokenizedAttrName = "%" + attrName + "%";
-            if ( searchFilter.contains( tokenizedAttrName ) )
-            {
-                LOGGER.trace( pwmSession, "skipping validation of ldap value for '" + attrName + "' because it is in search filter" );
-            }
-            else
-            {
-                final String value = entry.getValue();
-                try
-                {
-                    if ( !chaiUser.compareStringAttribute( attrName, value ) )
-                    {
-                        final String errorMsg = "incorrect value for '" + attrName + "'";
-                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_ACTIVATION_VALIDATIONFAIL, errorMsg, new String[]
-                                {
-                                        attrName,
-                                }
-                        );
-                        LOGGER.debug( pwmSession.getLabel(), errorInfo.toDebugStr() );
-                        throw new PwmDataValidationException( errorInfo );
-                    }
-                    LOGGER.trace( pwmSession.getLabel(), "successful validation of ldap value for '" + attrName + "'" );
-                }
-                catch ( ChaiOperationException e )
-                {
-                    LOGGER.error( pwmSession.getLabel(), "error during param validation of '" + attrName + "', error: " + e.getMessage() );
-                    throw new PwmDataValidationException( new ErrorInformation(
-                            PwmError.ERROR_ACTIVATION_VALIDATIONFAIL,
-                            "ldap error testing value for '" + attrName + "'", new String[]
-                            {
-                                    attrName,
-                            }
-                    ) );
-                }
-            }
-        }
-    }
-
-    private void sendPostActivationNotice(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-
-        final boolean success;
-        switch ( pref )
-        {
-            case SMSONLY:
-                // Only try SMS
-                success = sendPostActivationSms( pwmRequest );
-                break;
-            case EMAILONLY:
-            default:
-                // Only try email
-                success = sendPostActivationEmail( pwmRequest );
-                break;
-        }
-        if ( !success )
-        {
-            LOGGER.warn( pwmSession, "skipping send activation message for '" + userInfo.getUserIdentity() + "' no email or SMS number configured" );
-        }
-    }
-
-    private boolean sendPostActivationEmail(
-            final PwmRequest pwmRequest
-    )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final Configuration config = pwmApplication.getConfig();
-        final Locale locale = pwmSession.getSessionStateBean().getLocale();
-        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION, locale );
-
-        if ( configuredEmailSetting == null )
-        {
-            LOGGER.debug( pwmSession, "skipping send activation email for '" + userInfo.getUserIdentity() + "' no email configured" );
-            return false;
-        }
-
-        pwmApplication.getEmailQueue().submitEmail(
-                configuredEmailSetting,
-                pwmSession.getUserInfo(),
-                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
-        );
-        return true;
-    }
-
-    private Boolean sendPostActivationSms( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final Configuration config = pwmApplication.getConfig();
-        final UserInfo userInfo = pwmSession.getUserInfo();
-        final Locale locale = pwmSession.getSessionStateBean().getLocale();
-        final LdapProfile ldapProfile = userInfo.getUserIdentity().getLdapProfile( config );
-
-        final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_TEXT, locale );
-
-        final String toSmsNumber;
-        try
-        {
-            toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
-        }
-        catch ( Exception e )
-        {
-            LOGGER.debug( pwmSession.getLabel(), "error reading SMS attribute from user '" + pwmSession.getUserInfo().getUserIdentity() + "': " + e.getMessage() );
-            return false;
-        }
-
-        if ( toSmsNumber == null || toSmsNumber.length() < 1 )
-        {
-            LOGGER.debug( pwmSession.getLabel(), "skipping send activation SMS for '" + pwmSession.getUserInfo().getUserIdentity() + "' no SMS number configured" );
-            return false;
-        }
-
-        pwmApplication.sendSmsUsingQueue(
-                toSmsNumber,
-                message,
-                pwmRequest.getSessionLabel(),
-                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
-        );
-        return true;
-    }
-
-    private static void initializeToken(
-            final PwmRequest pwmRequest,
-            final Locale locale,
-            final UserIdentity userIdentity
-
-    )
-            throws PwmUnrecoverableException, PwmOperationalException, ChaiUnavailableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-        final Configuration config = pwmApplication.getConfig();
-
-        final RestTokenDataClient.TokenDestinationData inputTokenDestData;
-        {
-            final String toAddress;
-            {
-                final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION_VERIFICATION, locale );
-                final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-                toAddress = macroMachine.expandMacros( emailItemBean.getTo() );
-            }
-
-            final String toSmsNumber;
-            try
-            {
-                final UserInfo userInfo = UserInfoFactory.newUserInfoUsingProxy( pwmApplication, pwmSession.getLabel(), userIdentity, pwmRequest.getLocale() );
-                final LdapProfile ldapProfile = userIdentity.getLdapProfile( config );
-                toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
-            }
-            catch ( Exception e )
-            {
-                final String errorMsg = "unable to read user SMS attribute due to ldap error, unable to send token: " + e.getMessage();
-                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
-                LOGGER.error( pwmSession.getLabel(), errorInformation );
-                throw new PwmOperationalException( errorInformation );
-            }
-            inputTokenDestData = new RestTokenDataClient.TokenDestinationData( toAddress, toSmsNumber, null );
-        }
-
-        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
-        final RestTokenDataClient.TokenDestinationData outputDestTokenData = restTokenDataClient.figureDestTokenDisplayString(
-                pwmSession.getLabel(),
-                inputTokenDestData,
-                activateUserBean.getUserIdentity(),
-                pwmSession.getSessionStateBean().getLocale() );
-
-        final Set<String> destinationValues = new HashSet<>();
-        if ( outputDestTokenData.getEmail() != null )
-        {
-            destinationValues.add( outputDestTokenData.getEmail() );
-        }
-        if ( outputDestTokenData.getSms() != null )
-        {
-            destinationValues.add( outputDestTokenData.getSms() );
-        }
-
-
-        final Map<String, String> tokenMapData = new HashMap<>();
-
-        try
-        {
-            final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
-                    pwmApplication,
-                    pwmSession.getLabel(),
-                    activateUserBean.getUserIdentity() );
-            if ( userLastPasswordChange != null )
-            {
-                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, JavaHelper.toIsoDate( userLastPasswordChange ) );
-            }
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            LOGGER.error( pwmSession.getLabel(), "unexpected error reading user's last password change time" );
-        }
-
-        final String tokenKey;
-        final TokenPayload tokenPayload;
-        try
-        {
-            tokenPayload = pwmApplication.getTokenService().createTokenPayload(
-                    TokenType.ACTIVATION,
-                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
-                    tokenMapData,
-                    userIdentity,
-                    destinationValues
-            );
-            tokenKey = pwmApplication.getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-
-        final String displayValue = sendToken( pwmRequest, userIdentity, locale, outputDestTokenData.getEmail(), outputDestTokenData.getSms(), tokenKey );
-        activateUserBean.setTokenDisplayText( displayValue );
-        activateUserBean.setTokenIssued( true );
-    }
-
-    private void handleEnterTokenCode(
-            final PwmRequest pwmRequest
-    )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final PwmSession pwmSession = pwmRequest.getPwmSession();
-        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
-        final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
-
-        ErrorInformation errorInformation = null;
-        try
-        {
-            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
-                    pwmSession,
-                    activateUserBean.getUserIdentity(),
-                    TokenType.ACTIVATION,
-                    userEnteredCode
-            );
-            if ( tokenPayload != null )
-            {
-                activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
-                activateUserBean.setTokenPassed( true );
-                activateUserBean.setFormValidated( true );
-            }
-        }
-        catch ( PwmOperationalException e )
-        {
-            final String errorMsg = "token incorrect: " + e.getMessage();
-            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
-        }
-
-        if ( !activateUserBean.isTokenPassed() )
-        {
-            if ( errorInformation == null )
-            {
-                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
-            }
-            LOGGER.debug( pwmSession.getLabel(), errorInformation.toDebugStr() );
-            setLastError( pwmRequest, errorInformation );
-        }
-
-        this.advanceToNextStage( pwmRequest );
-    }
-
-    private static String sendToken(
-            final PwmRequest pwmRequest,
-            final UserIdentity userIdentity,
-            final Locale userLocale,
-            final String toAddress,
-            final String toSmsNumber,
-            final String tokenKey
-    )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Configuration config = pwmApplication.getConfig();
-        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
-        final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION_VERIFICATION, userLocale );
-        final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_VERIFICATION_TEXT, userLocale );
-
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
-
-        final List<TokenDestinationItem.Type> sentTypes = TokenService.TokenSender.sendToken(
-                TokenService.TokenSendInfo.builder()
-                        .pwmApplication( pwmApplication )
-                        .userInfo( null )
-                        .macroMachine( macroMachine )
-                        .configuredEmailSetting( emailItemBean )
-                        .tokenSendMethod( pref )
-                        .emailAddress( toAddress )
-                        .smsNumber( toSmsNumber )
-                        .smsMessage( smsMessage )
-                        .tokenKey( tokenKey )
-                        .sessionLabel( pwmRequest.getSessionLabel() )
-                        .build()
-        );
-
-        return TokenService.TokenSender.figureDisplayString(
-                pwmApplication.getConfig(),
-                sentTypes,
-                toAddress,
-                toSmsNumber
-        );
-    }
-
-    private static String figureLdapSearchFilter( final PwmRequest pwmRequest )
-            throws PwmUnrecoverableException
-    {
-        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
-        final Configuration config = pwmApplication.getConfig();
-        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
-
-        final String configuredSearchFilter = config.readSettingAsString( PwmSetting.ACTIVATE_USER_SEARCH_FILTER );
-        final String searchFilter;
-        if ( configuredSearchFilter == null || configuredSearchFilter.isEmpty() )
-        {
-            searchFilter = FormUtility.ldapSearchFilterForForm( pwmApplication, configuredActivationForm );
-            LOGGER.trace( pwmRequest, "auto generated search filter based on activation form: " + searchFilter );
-        }
-        else
-        {
-            searchFilter = configuredSearchFilter;
-        }
-        return searchFilter;
-    }
-
-    private static void forwardToActivateUserForm( final PwmRequest pwmRequest )
-            throws ServletException, PwmUnrecoverableException, IOException
-    {
-        pwmRequest.addFormInfoToRequestAttr( PwmSetting.ACTIVATE_USER_FORM, false, false );
-        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER );
-    }
-}

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

@@ -40,6 +40,7 @@ import password.pwm.http.bean.SetupResponsesBean;
 import password.pwm.http.bean.ShortcutsBean;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.http.servlet.accountinfo.AccountInformationServlet;
+import password.pwm.http.servlet.activation.ActivateUserServlet;
 import password.pwm.http.servlet.admin.AdminServlet;
 import password.pwm.http.servlet.changepw.PrivateChangePasswordServlet;
 import password.pwm.http.servlet.changepw.PublicChangePasswordServlet;
@@ -94,7 +95,7 @@ public enum PwmServletDefinition
     ConfigManager_PwNotify( ConfigManagerPwNotifyServlet.class, ConfigManagerBean.class ),
 
     NewUser( NewUserServlet.class, NewUserBean.class ),
-    ActivateUser( password.pwm.http.servlet.ActivateUserServlet.class, ActivateUserBean.class ),
+    ActivateUser( ActivateUserServlet.class, ActivateUserBean.class ),
     ForgottenPassword( password.pwm.http.servlet.forgottenpw.ForgottenPasswordServlet.class, ForgottenPasswordBean.class ),
     ForgottenUsername( password.pwm.http.servlet.ForgottenUsernameServlet.class, null ),;
 

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

@@ -0,0 +1,484 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.activation;
+
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.LocalSessionStateBean;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.config.value.data.UserPermission;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.HttpMethod;
+import password.pwm.http.JspUrl;
+import password.pwm.http.ProcessStatus;
+import password.pwm.http.PwmHttpRequestWrapper;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.PwmSession;
+import password.pwm.http.bean.ActivateUserBean;
+import password.pwm.http.servlet.ControlledPwmServlet;
+import password.pwm.http.servlet.PwmServletDefinition;
+import password.pwm.i18n.Message;
+import password.pwm.ldap.LdapPermissionTester;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.UserInfoFactory;
+import password.pwm.ldap.search.SearchConfiguration;
+import password.pwm.ldap.search.UserSearchEngine;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.event.AuditRecord;
+import password.pwm.svc.event.AuditRecordFactory;
+import password.pwm.svc.token.TokenPayload;
+import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
+import password.pwm.util.CaptchaUtility;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * User interaction servlet for creating new users (self registration).
+ *
+ * @author Jason D. Rivard
+ */
+@WebServlet(
+        name = "ActivateUserServlet",
+        urlPatterns = {
+                PwmConstants.URL_PREFIX_PUBLIC + "/activate",
+                PwmConstants.URL_PREFIX_PUBLIC + "/activate/*",
+                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser",
+                PwmConstants.URL_PREFIX_PUBLIC + "/ActivateUser/*",
+        }
+)
+public class ActivateUserServlet extends ControlledPwmServlet
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserServlet.class );
+
+    public enum ActivateUserAction implements ProcessAction
+    {
+        activate( HttpMethod.POST ),
+        tokenChoice( HttpMethod.POST ),
+        enterCode( HttpMethod.POST, HttpMethod.GET ),
+        reset( HttpMethod.POST ),
+        agree( HttpMethod.POST ),;
+
+        private final Collection<HttpMethod> method;
+
+        ActivateUserAction( final HttpMethod... method )
+        {
+            this.method = Collections.unmodifiableList( Arrays.asList( method ) );
+        }
+
+        public Collection<HttpMethod> permittedMethods( )
+        {
+            return method;
+        }
+    }
+
+    public enum ResetType
+    {
+        exitActivation,
+        clearTokenDestination,
+    }
+
+    @Override
+    public Class<? extends ProcessAction> getProcessActionsClass( )
+    {
+        return ActivateUserAction.class;
+    }
+
+    @Override
+    public ProcessStatus preProcessCheck( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ServletException
+    {
+        final Configuration config = pwmRequest.getConfig();
+
+        if ( !config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_ENABLE ) )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "activate user is not enabled" );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        if ( pwmRequest.isAuthenticated() )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_USERAUTHENTICATED );
+            throw new PwmUnrecoverableException( errorInformation );
+        }
+
+        final ProcessAction action = this.readProcessAction( pwmRequest );
+
+        // convert a url command like /public/newuser/12321321 to redirect with a process action.
+        if ( action == null )
+        {
+            if ( pwmRequest.convertURLtokenCommand( PwmServletDefinition.ActivateUser, ActivateUserAction.enterCode ) )
+            {
+                return ProcessStatus.Halt;
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    static UserInfo userInfo ( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+        return UserInfoFactory.newUserInfoUsingProxy(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                activateUserBean.getUserIdentity(),
+                pwmRequest.getLocale() );
+
+    }
+
+    static ActivateUserBean activateUserBean( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
+    {
+        return pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+    }
+
+    @ActionHandler( action = "reset" )
+    public ProcessStatus handleResetRequest( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final ResetType resetType = pwmRequest.readParameterAsEnum( PwmConstants.PARAM_RESET_TYPE, ResetType.class, ResetType.exitActivation );
+
+        switch ( resetType )
+        {
+            case exitActivation:
+                pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
+                ActivateUserUtils.forwardToActivateUserForm( pwmRequest );
+                return ProcessStatus.Halt;
+
+            case clearTokenDestination:
+                activateUserBean( pwmRequest ).setTokenDestination( null );
+                activateUserBean( pwmRequest ).setTokenSent( false );
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( resetType );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "activate" )
+    public ProcessStatus handleActivateRequest( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final LocalSessionStateBean ssBean = pwmSession.getSessionStateBean();
+
+        if ( CaptchaUtility.captchaEnabledForRequest( pwmRequest ) )
+        {
+            if ( !CaptchaUtility.verifyReCaptcha( pwmRequest ) )
+            {
+                final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_BAD_CAPTCHA_RESPONSE );
+                throw new PwmUnrecoverableException( errorInfo );
+            }
+        }
+
+        pwmApplication.getSessionStateService().clearBean( pwmRequest, ActivateUserBean.class );
+        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
+
+        Map<FormConfiguration, String> formValues = new HashMap<>();
+        try
+        {
+            //read the values from the request
+            formValues = FormUtility.readFormValuesFromRequest( pwmRequest, configuredActivationForm,
+                    ssBean.getLocale() );
+
+            // check for intruders
+            pwmApplication.getIntruderManager().convenience().checkAttributes( formValues );
+
+            // read the context attr
+            final String contextParam = pwmRequest.readParameterAsString( PwmConstants.PARAM_CONTEXT );
+
+            // read the profile attr
+            final String ldapProfile = pwmRequest.readParameterAsString( PwmConstants.PARAM_LDAP_PROFILE );
+
+            // see if the values meet the configured form requirements.
+            FormUtility.validateFormValues( config, formValues, ssBean.getLocale() );
+
+            final String searchFilter = ActivateUserUtils.figureLdapSearchFilter( pwmRequest );
+
+            // read an ldap user object based on the params
+            final UserIdentity userIdentity;
+            {
+                final UserSearchEngine userSearchEngine = pwmApplication.getUserSearchEngine();
+                final SearchConfiguration searchConfiguration = SearchConfiguration.builder()
+                        .contexts( Collections.singletonList( contextParam ) )
+                        .filter( searchFilter )
+                        .formValues( formValues )
+                        .ldapProfile( ldapProfile )
+                        .build();
+
+                userIdentity = userSearchEngine.performSingleUserSearch( searchConfiguration, pwmRequest.getSessionLabel() );
+            }
+
+            ActivateUserUtils.validateParamsAgainstLDAP( pwmRequest, formValues, userIdentity );
+
+            final List<UserPermission> userPermissions = config.readSettingAsUserPermission( PwmSetting.ACTIVATE_USER_QUERY_MATCH );
+            if ( !LdapPermissionTester.testUserPermissions( pwmApplication, pwmSession.getLabel(), userIdentity, userPermissions ) )
+            {
+                final String errorMsg = "user " + userIdentity + " attempted activation, but does not match query string";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATE_NO_PERMISSION, errorMsg );
+                pwmApplication.getIntruderManager().convenience().markUserIdentity( userIdentity, pwmSession );
+                pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+
+            final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+            activateUserBean.setUserIdentity( userIdentity );
+            activateUserBean.setFormValidated( true );
+            pwmApplication.getIntruderManager().convenience().clearAttributes( formValues );
+            pwmApplication.getIntruderManager().convenience().clearAddressAndSession( pwmSession );
+        }
+        catch ( PwmOperationalException e )
+        {
+            pwmApplication.getIntruderManager().convenience().markAttributes( formValues, pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+            setLastError( pwmRequest, e.getErrorInformation() );
+            LOGGER.debug( pwmSession.getLabel(), e.getErrorInformation().toDebugStr() );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "tokenChoice" )
+    private ProcessStatus processTokenChoice( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+        final UserInfo userInfo = userInfo( pwmRequest );
+        final MessageSendMethod tokenSendMethod = pwmRequest.getConfig().readSettingAsEnum( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class );
+
+        final List<TokenDestinationItem> tokenDestinationItems = TokenUtil.figureAvailableTokenDestinations(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userInfo,
+                tokenSendMethod
+        );
+
+        final String requestedID = pwmRequest.readParameterAsString( "choice", PwmHttpRequestWrapper.Flag.BypassValidation );
+
+        if ( !StringUtil.isEmpty( requestedID ) )
+        {
+            for ( final TokenDestinationItem item : tokenDestinationItems )
+            {
+                if ( requestedID.equals( item.getId() ) )
+                {
+                    activateUserBean.setTokenDestination( item );
+                }
+            }
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+
+    @ActionHandler( action = "enterCode" )
+    public ProcessStatus handleEnterCode(
+            final PwmRequest pwmRequest
+    )
+            throws ChaiUnavailableException, PwmUnrecoverableException, IOException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final ActivateUserBean activateUserBean = pwmApplication.getSessionStateService().getBean( pwmRequest, ActivateUserBean.class );
+        final String userEnteredCode = pwmRequest.readParameterAsString( PwmConstants.PARAM_TOKEN );
+
+        ErrorInformation errorInformation = null;
+        try
+        {
+            final TokenPayload tokenPayload = pwmApplication.getTokenService().processUserEnteredCode(
+                    pwmSession,
+                    activateUserBean.getUserIdentity(),
+                    TokenType.ACTIVATION,
+                    userEnteredCode
+            );
+            if ( tokenPayload != null )
+            {
+                activateUserBean.setUserIdentity( tokenPayload.getUserIdentity() );
+                activateUserBean.setTokenPassed( true );
+                activateUserBean.setFormValidated( true );
+            }
+        }
+        catch ( PwmOperationalException e )
+        {
+            final String errorMsg = "token incorrect: " + e.getMessage();
+            errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT, errorMsg );
+        }
+
+        if ( !activateUserBean.isTokenPassed() )
+        {
+            if ( errorInformation == null )
+            {
+                errorInformation = new ErrorInformation( PwmError.ERROR_TOKEN_INCORRECT );
+            }
+            LOGGER.debug( pwmSession.getLabel(), errorInformation.toDebugStr() );
+            setLastError( pwmRequest, errorInformation );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @ActionHandler( action = "agree" )
+    public ProcessStatus handleAgreeRequest(
+            final PwmRequest pwmRequest
+    )
+            throws ServletException, IOException, PwmUnrecoverableException, ChaiUnavailableException
+    {
+        LOGGER.debug( pwmRequest, "user accepted agreement" );
+
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+
+        if ( !activateUserBean.isAgreementPassed() )
+        {
+            activateUserBean.setAgreementPassed( true );
+            final AuditRecord auditRecord = new AuditRecordFactory( pwmRequest ).createUserAuditRecord(
+                    AuditEvent.AGREEMENT_PASSED,
+                    pwmRequest.getUserInfoIfLoggedIn(),
+                    pwmRequest.getSessionLabel(),
+                    "ActivateUser"
+            );
+            pwmRequest.getPwmApplication().getAuditManager().submit( auditRecord );
+        }
+
+        return ProcessStatus.Continue;
+    }
+
+    @Override
+    protected void nextStep( final PwmRequest pwmRequest ) throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final ActivateUserBean activateUserBean = activateUserBean( pwmRequest );
+
+        if ( !activateUserBean.isFormValidated() || activateUserBean.getUserIdentity() == null )
+        {
+            ActivateUserUtils.forwardToActivateUserForm( pwmRequest );
+            return;
+        }
+
+        final UserInfo userInfo = userInfo( pwmRequest );
+
+        final MessageSendMethod tokenSendMethod = config.readSettingAsEnum( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD, MessageSendMethod.class );
+        if ( MessageSendMethod.NONE != tokenSendMethod )
+        {
+            final List<TokenDestinationItem> tokenDestinationItems = TokenUtil.figureAvailableTokenDestinations(
+                    pwmApplication,
+                    pwmRequest.getSessionLabel(),
+                    pwmRequest.getLocale(),
+                    userInfo,
+                    tokenSendMethod
+            );
+
+            if ( activateUserBean.getTokenDestination() == null )
+            {
+                if ( tokenDestinationItems.size() == 1 )
+                {
+                    activateUserBean.setTokenDestination( tokenDestinationItems.iterator().next() );
+                }
+                else if ( tokenDestinationItems.size() > 1 )
+                {
+                    forwardToTokenChoiceJsp( pwmRequest, tokenDestinationItems );
+                    return;
+                }
+            }
+
+            if ( !activateUserBean.isTokenSent() && activateUserBean.getTokenDestination() != null )
+            {
+
+                TokenUtil.initializeAndSendToken(
+                        pwmRequest,
+                        userInfo,
+                        activateUserBean.getTokenDestination(),
+                        PwmSetting.EMAIL_ACTIVATION_VERIFICATION,
+                        TokenType.ACTIVATION,
+                        PwmSetting.SMS_ACTIVATION_VERIFICATION_TEXT
+                );
+            }
+
+            if ( !activateUserBean.isTokenPassed() )
+            {
+                pwmRequest.setAttribute( PwmRequestAttribute.ShowGoBackButton, tokenDestinationItems.size() > 1 );
+                pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_ENTER_CODE );
+                return;
+            }
+        }
+
+        final String agreementText = config.readSettingAsLocalizedString(
+                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
+                pwmSession.getSessionStateBean().getLocale()
+        );
+        if ( !StringUtil.isEmpty( agreementText ) && !activateUserBean.isAgreementPassed() )
+        {
+            ActivateUserUtils.forwardToAgreementPage( pwmRequest );
+            return;
+        }
+
+        try
+        {
+            ActivateUserUtils.activateUser( pwmRequest, activateUserBean.getUserIdentity() );
+            pwmRequest.getPwmResponse().forwardToSuccessPage( Message.Success_ActivateUser );
+        }
+        catch ( PwmOperationalException e )
+        {
+            LOGGER.debug( pwmRequest, e.getErrorInformation() );
+            pwmApplication.getIntruderManager().convenience().markUserIdentity( activateUserBean.getUserIdentity(), pwmSession );
+            pwmApplication.getIntruderManager().convenience().markAddressAndSession( pwmSession );
+            pwmRequest.respondWithError( e.getErrorInformation() );
+        }
+    }
+
+
+    private void forwardToTokenChoiceJsp( final PwmRequest pwmRequest, final List<TokenDestinationItem> tokenDestinationItems )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, new ArrayList<>( tokenDestinationItems ) );
+        pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE );
+    }
+
+}

+ 390 - 0
server/src/main/java/password/pwm/http/servlet/activation/ActivateUserUtils.java

@@ -0,0 +1,390 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.http.servlet.activation;
+
+import com.novell.ldapchai.ChaiUser;
+import com.novell.ldapchai.exception.ChaiOperationException;
+import com.novell.ldapchai.exception.ChaiUnavailableException;
+import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
+import com.novell.ldapchai.provider.ChaiProvider;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import password.pwm.PwmApplication;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.LoginInfoBean;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.config.profile.LdapProfile;
+import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmDataValidationException;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.JspUrl;
+import password.pwm.http.PwmRequest;
+import password.pwm.http.PwmRequestAttribute;
+import password.pwm.http.PwmSession;
+import password.pwm.ldap.UserInfo;
+import password.pwm.ldap.auth.AuthenticationType;
+import password.pwm.ldap.auth.PwmAuthenticationSource;
+import password.pwm.ldap.auth.SessionAuthenticator;
+import password.pwm.svc.event.AuditEvent;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.util.PostChangePasswordAction;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.operations.ActionExecutor;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+class ActivateUserUtils
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( ActivateUserUtils.class );
+
+    private ActivateUserUtils()
+    {
+    }
+
+
+    @SuppressFBWarnings( "SE_BAD_FIELD" )
+    static void activateUser(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity
+    )
+            throws ChaiUnavailableException, PwmUnrecoverableException, PwmOperationalException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final ChaiUser theUser = pwmApplication.getProxiedChaiUser( userIdentity );
+        if ( config.readSettingAsBoolean( PwmSetting.ACTIVATE_USER_UNLOCK ) )
+        {
+            try
+            {
+                theUser.unlockPassword();
+            }
+            catch ( ChaiOperationException e )
+            {
+                final String errorMsg = "error unlocking user " + userIdentity + ": " + e.getMessage();
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
+                throw new PwmOperationalException( errorInformation );
+            }
+        }
+
+        try
+        {
+            {
+                // execute configured actions
+                LOGGER.debug( pwmSession.getLabel(), "executing configured pre-actions to user " + theUser.getEntryDN() );
+                final List<ActionConfiguration> configValues = config.readSettingAsAction( PwmSetting.ACTIVATE_USER_PRE_WRITE_ATTRIBUTES );
+                if ( configValues != null && !configValues.isEmpty() )
+                {
+                    final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity );
+
+                    final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                            .setExpandPwmMacros( true )
+                            .setMacroMachine( macroMachine )
+                            .createActionExecutor();
+
+                    actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
+                }
+            }
+
+            //authenticate the pwm session
+            final SessionAuthenticator sessionAuthenticator = new SessionAuthenticator( pwmApplication, pwmSession, PwmAuthenticationSource.USER_ACTIVATION );
+            sessionAuthenticator.authUserWithUnknownPassword( userIdentity, AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+
+            //ensure a change password is triggered
+            pwmSession.getLoginInfoBean().setType( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+            pwmSession.getLoginInfoBean().getAuthFlags().add( AuthenticationType.AUTH_FROM_PUBLIC_MODULE );
+            pwmSession.getLoginInfoBean().getLoginFlags().add( LoginInfoBean.LoginFlag.forcePwChange );
+
+
+            // mark the event log
+            pwmApplication.getAuditManager().submit( AuditEvent.ACTIVATE_USER, pwmSession.getUserInfo(), pwmSession );
+
+            // update the stats bean
+            pwmApplication.getStatisticsManager().incrementValue( Statistic.ACTIVATED_USERS );
+
+            // send email or sms
+            sendPostActivationNotice( pwmRequest );
+
+            // setup post-change attributes
+            final PostChangePasswordAction postAction = new PostChangePasswordAction()
+            {
+
+                public String getLabel( )
+                {
+                    return "ActivateUser write attributes";
+                }
+
+                public boolean doAction( final PwmSession pwmSession, final String newPassword )
+                        throws PwmUnrecoverableException
+                {
+                    try
+                    {
+                        {
+                            // execute configured actions
+                            LOGGER.debug( pwmSession.getLabel(), "executing post-activate configured actions to user " + userIdentity.toDisplayString() );
+
+                            final MacroMachine macroMachine = pwmSession.getSessionManager().getMacroMachine( pwmApplication );
+                            final List<ActionConfiguration> configValues = pwmApplication.getConfig().readSettingAsAction( PwmSetting.ACTIVATE_USER_POST_WRITE_ATTRIBUTES );
+
+                            final ActionExecutor actionExecutor = new ActionExecutor.ActionExecutorSettings( pwmApplication, userIdentity )
+                                    .setExpandPwmMacros( true )
+                                    .setMacroMachine( macroMachine )
+                                    .createActionExecutor();
+                            actionExecutor.executeActions( configValues, pwmRequest.getSessionLabel() );
+                        }
+                    }
+                    catch ( PwmOperationalException e )
+                    {
+                        final ErrorInformation info = new ErrorInformation(
+                                PwmError.ERROR_ACTIVATION_FAILURE,
+                                e.getErrorInformation().getDetailedErrorMsg(), e.getErrorInformation().getFieldValues()
+                        );
+                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
+                        newException.initCause( e );
+                        throw newException;
+                    }
+                    catch ( ChaiUnavailableException e )
+                    {
+                        final String errorMsg = "unable to reach ldap server while writing post-activate attributes: " + e.getMessage();
+                        final ErrorInformation info = new ErrorInformation( PwmError.ERROR_ACTIVATION_FAILURE, errorMsg );
+                        final PwmUnrecoverableException newException = new PwmUnrecoverableException( info );
+                        newException.initCause( e );
+                        throw newException;
+                    }
+                    return true;
+                }
+            };
+
+            pwmSession.getUserSessionDataCacheBean().addPostChangePasswordActions( "activateUserWriteAttributes", postAction );
+        }
+        catch ( ImpossiblePasswordPolicyException e )
+        {
+            final ErrorInformation info = new ErrorInformation( PwmError.ERROR_UNKNOWN, "unexpected ImpossiblePasswordPolicyException error while activating user" );
+            LOGGER.warn( pwmSession, info, e );
+            throw new PwmOperationalException( info );
+        }
+    }
+
+    static void validateParamsAgainstLDAP(
+            final PwmRequest pwmRequest,
+            final Map<FormConfiguration, String> formValues,
+            final UserIdentity userIdentity
+    )
+            throws ChaiUnavailableException, PwmDataValidationException, PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final String searchFilter = figureLdapSearchFilter( pwmRequest );
+        final ChaiProvider chaiProvider = pwmApplication.getProxyChaiProvider( userIdentity.getLdapProfileID() );
+        final ChaiUser chaiUser = chaiProvider.getEntryFactory().newChaiUser( userIdentity.getUserDN() );
+
+        for ( final Map.Entry<FormConfiguration, String> entry : formValues.entrySet() )
+        {
+            final FormConfiguration formItem = entry.getKey();
+            final String attrName = formItem.getName();
+            final String tokenizedAttrName = "%" + attrName + "%";
+            if ( searchFilter.contains( tokenizedAttrName ) )
+            {
+                LOGGER.trace( pwmSession, "skipping validation of ldap value for '" + attrName + "' because it is in search filter" );
+            }
+            else
+            {
+                final String value = entry.getValue();
+                try
+                {
+                    if ( !chaiUser.compareStringAttribute( attrName, value ) )
+                    {
+                        final String errorMsg = "incorrect value for '" + attrName + "'";
+                        final ErrorInformation errorInfo = new ErrorInformation( PwmError.ERROR_ACTIVATION_VALIDATIONFAIL, errorMsg, new String[]
+                                {
+                                        attrName,
+                                }
+                        );
+                        LOGGER.debug( pwmSession.getLabel(), errorInfo.toDebugStr() );
+                        throw new PwmDataValidationException( errorInfo );
+                    }
+                    LOGGER.trace( pwmSession.getLabel(), "successful validation of ldap value for '" + attrName + "'" );
+                }
+                catch ( ChaiOperationException e )
+                {
+                    LOGGER.error( pwmSession.getLabel(), "error during param validation of '" + attrName + "', error: " + e.getMessage() );
+                    throw new PwmDataValidationException( new ErrorInformation(
+                            PwmError.ERROR_ACTIVATION_VALIDATIONFAIL,
+                            "ldap error testing value for '" + attrName + "'", new String[]
+                            {
+                                    attrName,
+                            }
+                    ) );
+                }
+            }
+        }
+    }
+
+    static void sendPostActivationNotice(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final MessageSendMethod pref = MessageSendMethod.valueOf( config.readSettingAsString( PwmSetting.ACTIVATE_TOKEN_SEND_METHOD ) );
+
+        final boolean success;
+        switch ( pref )
+        {
+            case SMSONLY:
+                // Only try SMS
+                success = sendPostActivationSms( pwmRequest );
+                break;
+            case EMAILONLY:
+            default:
+                // Only try email
+                success = sendPostActivationEmail( pwmRequest );
+                break;
+        }
+        if ( !success )
+        {
+            LOGGER.warn( pwmSession, "skipping send activation message for '" + userInfo.getUserIdentity() + "' no email or SMS number configured" );
+        }
+    }
+
+    static boolean sendPostActivationEmail(
+            final PwmRequest pwmRequest
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final Configuration config = pwmApplication.getConfig();
+        final Locale locale = pwmSession.getSessionStateBean().getLocale();
+        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail( PwmSetting.EMAIL_ACTIVATION, locale );
+
+        if ( configuredEmailSetting == null )
+        {
+            LOGGER.debug( pwmSession, "skipping send activation email for '" + userInfo.getUserIdentity() + "' no email configured" );
+            return false;
+        }
+
+        pwmApplication.getEmailQueue().submitEmail(
+                configuredEmailSetting,
+                pwmSession.getUserInfo(),
+                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
+        );
+        return true;
+    }
+
+    static boolean sendPostActivationSms( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, ChaiUnavailableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final PwmSession pwmSession = pwmRequest.getPwmSession();
+        final Configuration config = pwmApplication.getConfig();
+        final UserInfo userInfo = pwmSession.getUserInfo();
+        final Locale locale = pwmSession.getSessionStateBean().getLocale();
+        final LdapProfile ldapProfile = userInfo.getUserIdentity().getLdapProfile( config );
+
+        final String message = config.readSettingAsLocalizedString( PwmSetting.SMS_ACTIVATION_TEXT, locale );
+
+        final String toSmsNumber;
+        try
+        {
+            toSmsNumber = userInfo.readStringAttribute( ldapProfile.readSettingAsString( PwmSetting.SMS_USER_PHONE_ATTRIBUTE ) );
+        }
+        catch ( Exception e )
+        {
+            LOGGER.debug( pwmSession.getLabel(), "error reading SMS attribute from user '" + pwmSession.getUserInfo().getUserIdentity() + "': " + e.getMessage() );
+            return false;
+        }
+
+        if ( toSmsNumber == null || toSmsNumber.length() < 1 )
+        {
+            LOGGER.debug( pwmSession.getLabel(), "skipping send activation SMS for '" + pwmSession.getUserInfo().getUserIdentity() + "' no SMS number configured" );
+            return false;
+        }
+
+        pwmApplication.sendSmsUsingQueue(
+                toSmsNumber,
+                message,
+                pwmRequest.getSessionLabel(),
+                pwmSession.getSessionManager().getMacroMachine( pwmApplication )
+        );
+        return true;
+    }
+
+    static String figureLdapSearchFilter( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final Configuration config = pwmApplication.getConfig();
+        final List<FormConfiguration> configuredActivationForm = config.readSettingAsForm( PwmSetting.ACTIVATE_USER_FORM );
+
+        final String configuredSearchFilter = config.readSettingAsString( PwmSetting.ACTIVATE_USER_SEARCH_FILTER );
+        final String searchFilter;
+        if ( configuredSearchFilter == null || configuredSearchFilter.isEmpty() )
+        {
+            searchFilter = FormUtility.ldapSearchFilterForForm( pwmApplication, configuredActivationForm );
+            LOGGER.trace( pwmRequest, "auto generated search filter based on activation form: " + searchFilter );
+        }
+        else
+        {
+            searchFilter = configuredSearchFilter;
+        }
+        return searchFilter;
+    }
+
+    static void forwardToAgreementPage( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        final String agreementText = pwmRequest.getConfig().readSettingAsLocalizedString(
+                PwmSetting.ACTIVATE_AGREEMENT_MESSAGE,
+                pwmRequest.getLocale()
+        );
+
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, ActivateUserServlet.userInfo( pwmRequest ).getUserIdentity() );
+        final String expandedText = macroMachine.expandMacros( agreementText );
+        pwmRequest.setAttribute( PwmRequestAttribute.AgreementText, expandedText );
+        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER_AGREEMENT );
+    }
+
+    static void forwardToActivateUserForm( final PwmRequest pwmRequest )
+            throws ServletException, PwmUnrecoverableException, IOException
+    {
+        pwmRequest.addFormInfoToRequestAttr( PwmSetting.ACTIVATE_USER_FORM, false, false );
+        pwmRequest.forwardToJsp( JspUrl.ACTIVATE_USER );
+    }
+}

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

@@ -1506,7 +1506,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
         final List<TokenDestinationItem> destItems = ForgottenPasswordUtil.figureAvailableTokenDestinations( pwmRequest, forgottenPasswordBean );
-        pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordTokenDestItems, new ArrayList<>( destItems ) );
+        pwmRequest.setAttribute( PwmRequestAttribute.TokenDestItems, new ArrayList<>( destItems ) );
         pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_TOKEN_CHOICE );
     }
 }

+ 16 - 136
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -63,23 +63,17 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.event.AuditRecordFactory;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
-import password.pwm.svc.token.TokenPayload;
-import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
+import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.PasswordData;
 import password.pwm.util.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;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
-import password.pwm.ws.client.rest.RestTokenDataClient;
 
 import javax.servlet.ServletException;
 import java.io.IOException;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -88,8 +82,6 @@ import java.util.List;
 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
 {
@@ -298,38 +290,17 @@ public class ForgottenPasswordUtil
         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 ) )
-        {
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, "no token send methods configured in profile" );
-        }
-
         final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        List<TokenDestinationItem> tokenDestinations = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmRequest.getPwmApplication(), userInfo ) );
-
-        if ( tokenSendMethod != MessageSendMethod.CHOICE_SMS_EMAIL )
-        {
-            tokenDestinations = tokenDestinations
-                    .stream()
-                    .filter( tokenDestinationItem -> tokenSendMethod == tokenDestinationItem.getType().getMessageSendMethod() )
-                    .collect( Collectors.toList() );
-        }
-
-        final List<TokenDestinationItem> effectiveItems = new ArrayList<>(  );
-        for ( final TokenDestinationItem item : tokenDestinations )
-        {
-            final TokenDestinationItem effectiveItem = invokeExternalTokenDestRestClient( pwmRequest, userInfo.getUserIdentity(), item );
-            effectiveItems.add( effectiveItem );
-        }
 
-        LOGGER.trace( pwmRequest, "calculated available token send destinations: " + JsonUtil.serializeCollection( effectiveItems ) );
-
-        if ( tokenDestinations.isEmpty() )
-        {
-            final String msg = "no available contact methods of type " + tokenSendMethod.name() + " available";
-            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
-        }
+        final List<TokenDestinationItem> items = TokenUtil.figureAvailableTokenDestinations(
+                pwmRequest.getPwmApplication(),
+                pwmRequest.getSessionLabel(),
+                pwmRequest.getLocale(),
+                userInfo,
+                tokenSendMethod
+        );
 
-        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( effectiveItems );
+        final List<TokenDestinationItem> finalList = Collections.unmodifiableList( items );
         pwmRequest.getHttpServletRequest().setAttribute( PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_AVAIL_TOKEN_DEST_CACHE, finalList );
 
         return finalList;
@@ -444,109 +415,18 @@ public class ForgottenPasswordUtil
     )
             throws PwmUnrecoverableException
     {
-        final Configuration config = pwmRequest.getConfig();
-        final UserIdentity userIdentity = userInfo.getUserIdentity();
-        final Map<String, String> tokenMapData = new LinkedHashMap<>();
-
-        try
-        {
-            final Instant userLastPasswordChange = PasswordUtility.determinePwdLastModified(
-                    pwmRequest.getPwmApplication(),
-                    pwmRequest.getSessionLabel(),
-                    userIdentity
-            );
-            if ( userLastPasswordChange != null )
-            {
-                final String userChangeString = JavaHelper.toIsoDate( userLastPasswordChange );
-                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, userChangeString );
-            }
-        }
-        catch ( ChaiUnavailableException e )
-        {
-            LOGGER.error( pwmRequest, "unexpected error reading user's last password change time" );
-        }
-
-        final EmailItemBean emailItemBean = config.readSettingAsEmail( PwmSetting.EMAIL_CHALLENGE_TOKEN, pwmRequest.getLocale() );
-        final MacroMachine.StringReplacer stringReplacer = ( matchedMacro, newValue ) ->
-        {
-            if ( "@User:Email@".equals( matchedMacro )  )
-            {
-                return tokenDestinationItem.getValue();
-            }
-
-            return newValue;
-        };
-        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity, stringReplacer );
-
-        final String tokenKey;
-        final TokenPayload tokenPayload;
-        try
-        {
-            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
-                    TokenType.FORGOTTEN_PW,
-                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
-                    tokenMapData,
-                    userIdentity,
-                    Collections.singleton( tokenDestinationItem.getValue() )
-            );
-            tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-
-        final String smsMessage = config.readSettingAsLocalizedString( PwmSetting.SMS_CHALLENGE_TOKEN_TEXT, pwmRequest.getLocale() );
-
-        TokenService.TokenSender.sendToken(
-                TokenService.TokenSendInfo.builder()
-                        .pwmApplication( pwmRequest.getPwmApplication() )
-                        .userInfo( userInfo )
-                        .macroMachine( macroMachine )
-                        .configuredEmailSetting( emailItemBean )
-                        .tokenSendMethod( tokenDestinationItem.getType().getMessageSendMethod() )
-                        .emailAddress( tokenDestinationItem.getValue() )
-                        .smsNumber( tokenDestinationItem.getValue() )
-                        .smsMessage( smsMessage )
-                        .tokenKey( tokenKey )
-                        .sessionLabel( pwmRequest.getSessionLabel() )
-                        .build()
+        TokenUtil.initializeAndSendToken(
+                pwmRequest,
+                userInfo,
+                tokenDestinationItem,
+                PwmSetting.EMAIL_CHALLENGE_TOKEN,
+                TokenType.FORGOTTEN_PW,
+                PwmSetting.SMS_CHALLENGE_TOKEN_TEXT
         );
 
         StatisticsManager.incrementStat( pwmRequest, Statistic.RECOVERY_TOKENS_SENT );
     }
 
-    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()
-        );
-
-        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 )
             throws ChaiUnavailableException, IOException, ServletException, PwmUnrecoverableException

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

@@ -63,7 +63,7 @@ import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.util.PasswordData;
 import password.pwm.util.RandomPasswordGenerator;
-import password.pwm.util.ValueObfuscator;
+import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
@@ -598,8 +598,8 @@ class NewUserUtils
                 }
 
                 newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.SMS );
-                final ValueObfuscator valueObfuscator = new ValueObfuscator( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( valueObfuscator.maskPhone( toNum ) );
+                final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
+                newUserBean.getTokenVerificationProgress().setTokenDisplayText( tokenDestinationDisplayMasker.maskPhone( toNum ) );
                 newUserBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.SMS );
             }
             break;
@@ -643,8 +643,8 @@ class NewUserUtils
 
                 newUserBean.getTokenVerificationProgress().getIssuedTokens().add( TokenVerificationProgress.TokenChannel.EMAIL );
                 newUserBean.getTokenVerificationProgress().setPhase( TokenVerificationProgress.TokenChannel.EMAIL );
-                final ValueObfuscator valueObfuscator = new ValueObfuscator( pwmApplication.getConfig() );
-                newUserBean.getTokenVerificationProgress().setTokenDisplayText( valueObfuscator.maskEmail( toAddress ) );
+                final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
+                newUserBean.getTokenVerificationProgress().setTokenDisplayText( tokenDestinationDisplayMasker.maskEmail( toAddress ) );
 
                 final EmailItemBean emailItemBean = new EmailItemBean(
                         outputDestTokenData.getEmail(),

+ 16 - 3
server/src/main/java/password/pwm/util/ValueObfuscator.java → server/src/main/java/password/pwm/svc/token/TokenDestinationDisplayMasker.java

@@ -20,23 +20,31 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm.util;
+package password.pwm.svc.token;
 
 import password.pwm.AppProperty;
 import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
 import password.pwm.util.java.StringUtil;
 
-public class ValueObfuscator
+public class TokenDestinationDisplayMasker
 {
     private final Configuration configuration;
+    private boolean enabled;
 
-    public ValueObfuscator( final Configuration configuration )
+    public TokenDestinationDisplayMasker( final Configuration configuration )
     {
         this.configuration = configuration;
+        this.enabled = configuration.readSettingAsBoolean( PwmSetting.TOKEN_ENABLE_VALUE_MASKING );
     }
 
     public String maskEmail( final String email )
     {
+        if ( !enabled )
+        {
+            return email;
+        }
+
         if ( StringUtil.isEmpty( email ) )
         {
             return "";
@@ -49,6 +57,11 @@ public class ValueObfuscator
 
     public String maskPhone( final String phone )
     {
+        if ( !enabled )
+        {
+            return phone;
+        }
+
         if ( StringUtil.isEmpty( phone ) )
         {
             return "";

+ 3 - 4
server/src/main/java/password/pwm/svc/token/TokenService.java

@@ -57,7 +57,6 @@ import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.DataStore;
-import password.pwm.util.ValueObfuscator;
 import password.pwm.util.db.DatabaseDataStore;
 import password.pwm.util.db.DatabaseTable;
 import password.pwm.util.java.JavaHelper;
@@ -826,12 +825,12 @@ public class TokenService implements PwmService
                 final String sms
         )
         {
-            final ValueObfuscator valueObfuscator = new ValueObfuscator( configuration );
+            final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( configuration );
             final StringBuilder displayDestAddress = new StringBuilder();
             {
                 if ( sentTypes.contains( TokenDestinationItem.Type.email ) )
                 {
-                    displayDestAddress.append( valueObfuscator.maskEmail( email ) );
+                    displayDestAddress.append( tokenDestinationDisplayMasker.maskEmail( email ) );
                 }
 
                 if ( sentTypes.contains( TokenDestinationItem.Type.sms ) )
@@ -840,7 +839,7 @@ public class TokenService implements PwmService
                     {
                         displayDestAddress.append( " & " );
                     }
-                    displayDestAddress.append( valueObfuscator.maskPhone( sms ) );
+                    displayDestAddress.append( tokenDestinationDisplayMasker.maskPhone( sms ) );
                 }
             }
             return displayDestAddress.toString();

+ 214 - 0
server/src/main/java/password/pwm/svc/token/TokenUtil.java

@@ -0,0 +1,214 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.svc.token;
+
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.bean.EmailItemBean;
+import password.pwm.bean.SessionLabel;
+import password.pwm.bean.TokenDestinationItem;
+import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
+import password.pwm.config.PwmSetting;
+import password.pwm.config.option.MessageSendMethod;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmOperationalException;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.PwmRequest;
+import password.pwm.ldap.UserInfo;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
+import password.pwm.ws.client.rest.RestTokenDataClient;
+
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+public class TokenUtil
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( TokenUtil.class );
+
+    private TokenUtil()
+    {
+    }
+
+
+    public static List<TokenDestinationItem> figureAvailableTokenDestinations(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            final UserInfo userInfo,
+            final MessageSendMethod tokenSendMethod
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( tokenSendMethod == null || tokenSendMethod.equals( MessageSendMethod.NONE ) )
+        {
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, "no token send methods configured in profile" );
+        }
+
+        List<TokenDestinationItem> tokenDestinations = new ArrayList<>( TokenDestinationItem.allFromConfig( pwmApplication, userInfo ) );
+
+        if ( tokenSendMethod != MessageSendMethod.CHOICE_SMS_EMAIL )
+        {
+            tokenDestinations = tokenDestinations
+                    .stream()
+                    .filter( tokenDestinationItem -> tokenSendMethod == tokenDestinationItem.getType().getMessageSendMethod() )
+                    .collect( Collectors.toList() );
+        }
+
+        final List<TokenDestinationItem> effectiveItems = new ArrayList<>(  );
+        for ( final TokenDestinationItem item : tokenDestinations )
+        {
+            final TokenDestinationItem effectiveItem = invokeExternalTokenDestRestClient( pwmApplication, sessionLabel, locale, userInfo.getUserIdentity(), item );
+            effectiveItems.add( effectiveItem );
+        }
+
+        LOGGER.trace( sessionLabel, "calculated available token send destinations: " + JsonUtil.serializeCollection( effectiveItems ) );
+
+        if ( tokenDestinations.isEmpty() )
+        {
+            final String msg = "no available contact methods of type " + tokenSendMethod.name() + " available";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_TOKEN_MISSING_CONTACT, msg );
+        }
+
+        return Collections.unmodifiableList( effectiveItems );
+    }
+
+    private static TokenDestinationItem invokeExternalTokenDestRestClient(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final Locale locale,
+            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()
+        );
+
+        final RestTokenDataClient restTokenDataClient = new RestTokenDataClient( pwmApplication );
+        final RestTokenDataClient.TokenDestinationData outputDestrestTokenDataClient = restTokenDataClient.figureDestTokenDisplayString(
+                sessionLabel,
+                inputDestinationData,
+                userIdentity,
+                locale );
+
+        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();
+    }
+
+
+    public static void initializeAndSendToken(
+            final PwmRequest pwmRequest,
+            final UserInfo userInfo,
+            final TokenDestinationItem tokenDestinationItem,
+            final PwmSetting emailToSend,
+            final TokenType tokenType,
+            final PwmSetting smsToSend
+
+    )
+            throws PwmUnrecoverableException
+    {
+        final Configuration config = pwmRequest.getConfig();
+        final UserIdentity userIdentity = userInfo.getUserIdentity();
+        final Map<String, String> tokenMapData = new LinkedHashMap<>();
+
+        {
+            final Instant userLastPasswordChange = userInfo.getPasswordLastModifiedTime();
+            if ( userLastPasswordChange != null )
+            {
+                final String userChangeString = JavaHelper.toIsoDate( userLastPasswordChange );
+                tokenMapData.put( PwmConstants.TOKEN_KEY_PWD_CHG_DATE, userChangeString );
+            }
+        }
+
+        final EmailItemBean emailItemBean = config.readSettingAsEmail( emailToSend, pwmRequest.getLocale() );
+        final MacroMachine.StringReplacer stringReplacer = ( matchedMacro, newValue ) ->
+        {
+            if ( "@User:Email@".equals( matchedMacro )  )
+            {
+                return tokenDestinationItem.getValue();
+            }
+
+            return newValue;
+        };
+        final MacroMachine macroMachine = MacroMachine.forUser( pwmRequest, userIdentity, stringReplacer );
+
+        final String tokenKey;
+        final TokenPayload tokenPayload;
+        try
+        {
+            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
+                    tokenType,
+                    new TimeDuration( config.readSettingAsLong( PwmSetting.TOKEN_LIFETIME ), TimeUnit.SECONDS ),
+                    tokenMapData,
+                    userIdentity,
+                    Collections.singleton( tokenDestinationItem.getValue() )
+            );
+            tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken( tokenPayload, pwmRequest.getSessionLabel() );
+        }
+        catch ( PwmOperationalException e )
+        {
+            throw new PwmUnrecoverableException( e.getErrorInformation() );
+        }
+
+        final String smsMessage = config.readSettingAsLocalizedString( smsToSend, pwmRequest.getLocale() );
+
+        TokenService.TokenSender.sendToken(
+                TokenService.TokenSendInfo.builder()
+                        .pwmApplication( pwmRequest.getPwmApplication() )
+                        .userInfo( userInfo )
+                        .macroMachine( macroMachine )
+                        .configuredEmailSetting( emailItemBean )
+                        .tokenSendMethod( tokenDestinationItem.getType().getMessageSendMethod() )
+                        .emailAddress( tokenDestinationItem.getValue() )
+                        .smsNumber( tokenDestinationItem.getValue() )
+                        .smsMessage( smsMessage )
+                        .tokenKey( tokenKey )
+                        .sessionLabel( pwmRequest.getSessionLabel() )
+                        .build()
+        );
+    }
+
+}

+ 13 - 32
server/src/main/java/password/pwm/ws/client/rest/RestTokenDataClient.java

@@ -23,6 +23,7 @@
 package password.pwm.ws.client.rest;
 
 import com.novell.ldapchai.exception.ChaiUnavailableException;
+import lombok.Value;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
@@ -35,7 +36,9 @@ import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.UserInfoFactory;
+import password.pwm.svc.token.TokenDestinationDisplayMasker;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 
@@ -49,41 +52,16 @@ public class RestTokenDataClient implements RestClient
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( RestTokenDataClient.class );
 
+    private final PwmApplication pwmApplication;
+
+    @Value
     public static class TokenDestinationData implements Serializable
     {
         private String email;
         private String sms;
         private String displayValue;
-
-        public TokenDestinationData(
-                final String email,
-                final String sms,
-                final String displayValue
-        )
-        {
-            this.email = email;
-            this.sms = sms;
-            this.displayValue = displayValue;
-        }
-
-        public String getEmail( )
-        {
-            return email;
-        }
-
-        public String getSms( )
-        {
-            return sms;
-        }
-
-        public String getDisplayValue( )
-        {
-            return displayValue;
-        }
     }
 
-    private final PwmApplication pwmApplication;
-
     public RestTokenDataClient( final PwmApplication pwmApplication )
     {
         this.pwmApplication = pwmApplication;
@@ -152,21 +130,24 @@ public class RestTokenDataClient implements RestClient
 
     private TokenDestinationData builtInService( final TokenDestinationData tokenDestinationData )
     {
+
+        final TokenDestinationDisplayMasker tokenDestinationDisplayMasker = new TokenDestinationDisplayMasker( pwmApplication.getConfig() );
+
         final StringBuilder tokenSendDisplay = new StringBuilder();
 
-        if ( tokenDestinationData.getEmail() != null )
+        if ( !StringUtil.isEmpty( tokenDestinationData.getEmail() ) )
         {
-            tokenSendDisplay.append( tokenDestinationData.getEmail() );
+            tokenSendDisplay.append( tokenDestinationDisplayMasker.maskEmail( tokenDestinationData.getEmail() ) );
         }
 
-        if ( tokenDestinationData.getSms() != null )
+        if ( !StringUtil.isEmpty( tokenDestinationData.getSms() ) )
         {
             if ( tokenSendDisplay.length() > 0 )
             {
                 tokenSendDisplay.append( " / " );
             }
 
-            tokenSendDisplay.append( tokenDestinationData.getSms() );
+            tokenSendDisplay.append( tokenDestinationDisplayMasker.maskPhone( tokenDestinationData.getSms() ) );
         }
 
         return new TokenDestinationData(

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

@@ -1748,6 +1748,11 @@
             <value><![CDATA[pwmToken]]></value>
         </default>
     </setting>
+    <setting hidden="false" key="token.valueMasking.enable" level="1" required="true">
+        <default>
+            <value>true</value>
+        </default>
+    </setting>
     <setting hidden="false" key="otp.enabled" level="1" required="true">
         <default>
             <value>false</value>
@@ -2962,6 +2967,7 @@
             <option value="NONE">None - Token verification will not be performed</option>
             <option value="EMAILONLY">Email - Send to email address</option>
             <option value="SMSONLY">SMS - Send via SMS</option>
+            <option value="CHOICE_SMS_EMAIL">User Choice - If both SMS and email address is available, user decides</option>
         </options>
     </setting>
     <setting hidden="false" key="updateAttributes.enable" level="1" required="true">

+ 12 - 13
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -87,6 +87,7 @@ Category_Description_OTP_SETTINGS=Options for time-based one time passwords.
 Category_Description_OTP_SETUP=Options for time-based one time passwords.
 Category_Description_PASSWORD_GLOBAL=Password related settings that apply to all users regardless of the password policy or profile appear here.  For profile-specific password settings, see Profiles -> Password Policy Profiles.
 Category_Description_PASSWORD_POLICY=Settings that define the LDAP directories that are used by the application.  If the user identities are in multiple LDAP directories, configure each directory as an LDAP Directory Profile.  Within each LDAP directory profile definition, you can control the individual servers and other settings for each LDAP directory.
+Category_Description_PW_EXP_NOTIFY=Password Expiration Notification
 Category_Description_PEOPLE_SEARCH=The people search module provides basic white pages or directory lookup functionality to your users.  Customizations allow easy searching and display quick detailed information about your users' colleagues.
 Category_Description_PROFILES=Profiles
 Category_Description_RECOVERY_DEF=Definition
@@ -187,6 +188,7 @@ Category_Label_PASSWORD_GLOBAL=Password Settings
 Category_Label_PASSWORD_POLICY=Password Policies
 Category_Label_PEOPLE_SEARCH=People Search
 Category_Label_PROFILES=Policies
+Category_Label_PW_EXP_NOTIFY=Password Expiration Notification
 Category_Label_RECOVERY_DEF=Definition
 Category_Label_RECOVERY=Forgotten Password
 Category_Label_RECOVERY_OAUTH=OAuth
@@ -600,6 +602,10 @@ Setting_Description_pwm.securityKey=<p>Specify a Security Key used for cryptogra
 Setting_Description_pwm.seedlist.location=Specify the location of the seed list in the form of a valid URL. When @PwmAppName@ randomly generates passwords, it can generate a "friendly", random password suggestions to users.  It does this by using a "seed" word or words, and then modifying that word randomly until it is sufficiently complex and meets the configured rules computed for the user.<br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
 Setting_Description_pwm.selfURL=<p>The URL to this application, as seen by users. @PwmAppName@ uses the value in email macros and other user-facing communications.</p><p>The URL must use a valid fully qualified hostname. Do not use a network address.</p><p>In simple environments, the URL will be the base of the URL in the browser you are currently using to view this page, however in more complex environments the URL will typically be an upstream proxy, gateway or network device.</p><p>The URL should include the path to the base application, typically <code>/@Case:lower:[[@PwmAppName@]]@</code>.</p>
 Setting_Description_pwm.wordlist.location=Specify a word list file URL for dictionary checking to prevent users from using commonly used words as passwords.   Using word lists is an important part of password security.  Word lists are used by intruders to guess common passwords.   The default word list included contains commonly used English passwords.  <br/><br/>The first time a startup occurs with a new word list setting, it takes some time to compile the word list into a database.  See the status screen and logs for progress information.  The word list file format is one or more text files containing a single word per line, enclosed in a ZIP file.  The String <i>\!\#comment\:</i> at the beginning of a line indicates a comment. <br/><br/>The value must be a valid URL, using the protocol "file" (local file system), "http", or "https".
+Setting_Description_pwNotify.enable=Enable Password Expiration Notification
+Setting_Description_pwNotify.queryString=Expiration Notification User Match
+Setting_Description_pwNotify.intervals=Expiration Notification Intervals
+Setting_Description_pwNotify.job.offSet=Job Offset
 Setting_Description_recovery.action=Add actions to take when the user completes the forgotten password process.
 Setting_Description_recovery.allowWhenLocked=Enable this option to allow users to use the forgotten password feature when the account is intruder locked in LDAP.  This feature is not available when a user is using NMAS stored responses.
 Setting_Description_recovery.bogus.user.enable=Enable this option to have forgotten password act as though invalid user searches are valid, and present such users with a bogus forgotten password policy.  This can help prevent username discovery.
@@ -684,6 +690,7 @@ Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.lifetime=Specify the default lifetime an token is valid (in seconds). The default is one hour.  This default may be overridden by module specific settings.
 Setting_Description_token.storageMethod=Select the storage method @PwmAppName@ uses to save issued tokens.<table style\="width\: 400px"><tr><td>Method</td><td>Description</td></tr><tr><td>LocalDB</td><td>Stores the tokens in the local embedded LocalDB database.  Tokens are not common across multiple application instances.</td></tr><tr><td>DB</td><td>Store the tokens in a configured, remote database.  Tokens work across multiple application instances.</td></tr><tr><td>Crypto</td><td>Use crypto to create and read tokens, they are not stored locally.  Tokens work across multiple application instances if they have the same Security Key.  Crypto tokens ignore the length rules and might be too long to use for SMS purposes.</td></tr><tr><td>LDAP</td><td>Use the LDAP directory to store tokens.  Tokens work across multiple application instances.  You cannot use LDAP tokens as New User Registration tokens.</td></tr></table>
+Setting_Description_token.valueMasking.enable=Enable this option to mask token destination display values (email addresses and telephone numbers).
 Setting_Description_updateAttributes.check.queryMatch=When you use the "checkProfile" or "checkAll" parameter with the command servlet, @PwmAppName@ uses this query match to determine if the user is required to populate the parameter values. <br/><br/>If this value is blank, then @PwmAppName@ checks the user's current values against the form requirements.
 Setting_Description_updateAttributes.email.verification=Enable this option to send an email to the user's email address before @PwmAppName@ updates the account. The user's email must change to cause this verification email to be sent. The user must verify receipt of the email before @PwmAppName@ updates the account.
 Setting_Description_updateAttributes.enable=Enable the option to Update Profile Attributes.  If true, this setting enables the Update Profile module.
@@ -1091,6 +1098,10 @@ Setting_Label_pwm.securityKey=Security Key
 Setting_Label_pwm.seedlist.location=Seed List File URL
 Setting_Label_pwm.selfURL=Site URL
 Setting_Label_pwm.wordlist.location=Word List File URL
+Setting_Label_pwNotify.enable=Enable Password Expiration Notification
+Setting_Label_pwNotify.queryString=Expiration Notification User Match
+Setting_Label_pwNotify.intervals=Expiration Notification Intervals
+Setting_Label_pwNotify.job.offSet=Job Offset
 Setting_Label_recovery.action=Forgotten Password Recovery Mode
 Setting_Label_recovery.allowWhenLocked=Allow Forgotten Password when Locked
 Setting_Label_recovery.bogus.user.enable=Enable Bogus User Policy
@@ -1175,6 +1186,7 @@ Setting_Label_token.ldap.attribute=Token LDAP attribute name
 Setting_Label_token.length=Token Length
 Setting_Label_token.lifetime=Token Maximum Lifetime
 Setting_Label_token.storageMethod=Token Storage Method
+Setting_Label_token.valueMasking.enable=Enable Token Destination Value Masking
 Setting_Label_updateAttributes.check.queryMatch=Update Profile Check Match
 Setting_Label_updateAttributes.email.verification=Enable Email Verification
 Setting_Label_updateAttributes.enable=Enable Update Profile
@@ -1198,16 +1210,3 @@ Setting_Label_webservices.queryMatch=Web Services LDAP Authentication Permission
 Setting_Label_webservices.thirdParty.queryMatch=Web Services LDAP Third Party Permissions
 Setting_Label_webservice.userAttributes=Web Service User Attributes
 Setting_Label_wordlistCaseSensitive=Word List Case Sensitivity
-
-Category_Description_PW_EXP_NOTIFY=Password Expiration Notification
-Category_Label_PW_EXP_NOTIFY=Password Expiration Notification
-
-Setting_Label_pwNotify.enable=Enable Password Expiration Notification
-Setting_Label_pwNotify.queryString=Expiration Notification User Match
-Setting_Label_pwNotify.intervals=Expiration Notification Intervals
-Setting_Label_pwNotify.job.offSet=Job Offset
-
-Setting_Description_pwNotify.enable=Enable Password Expiration Notification
-Setting_Description_pwNotify.queryString=Expiration Notification User Match
-Setting_Description_pwNotify.intervals=Expiration Notification Intervals
-Setting_Description_pwNotify.job.offSet=Job Offset

+ 1 - 2
server/src/main/webapp/WEB-INF/jsp/activateuser-agreement.jsp

@@ -26,7 +26,6 @@
 <!DOCTYPE html>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
-<% final ActivateUserBean activateUserBean = JspUtility.getSessionBean(pageContext, ActivateUserBean.class); %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
 <body class="nihilo">
@@ -38,7 +37,7 @@
     <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <%@ include file="fragment/message.jsp" %>
-        <% final String expandedText = activateUserBean.getAgreementText(); %>
+        <% final String expandedText = (String)JspUtility.getAttribute( pageContext, PwmRequestAttribute.AgreementText ); %>
         <br/><br/>
         <div id="agreementText" class="agreementText"><%= expandedText %></div>
         <div class="buttonbar">

+ 21 - 9
server/src/main/webapp/WEB-INF/jsp/activateuser-entercode.jsp

@@ -25,6 +25,7 @@
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ page import="password.pwm.http.bean.ActivateUserBean" %>
 <%@ page import="password.pwm.http.tag.conditional.PwmIfTest" %>
+<%@ page import="password.pwm.http.servlet.activation.ActivateUserServlet" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <%@ include file="fragment/header.jsp" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
@@ -36,7 +37,7 @@
     <div id="centerbody">
         <h1 id="page-content-title"><pwm:display key="Title_ActivateUser" displayIfMissing="true"/></h1>
         <% final ActivateUserBean activateUserBean = JspUtility.getSessionBean(pageContext, ActivateUserBean.class); %>
-        <% final String destination = activateUserBean.getTokenDisplayText(); %>
+        <% final String destination = activateUserBean.getTokenDestination().getDisplay(); %>
         <p><pwm:display key="Display_RecoverEnterCode" value1="<%=destination%>"/></p>
         <%@ include file="/WEB-INF/jsp/fragment/message.jsp" %>
         <h2><label for="<%=PwmConstants.PARAM_TOKEN%>"><pwm:display key="Field_Code"/></label></h2>
@@ -50,8 +51,16 @@
                 <input type="hidden" id="processAction" name="processAction" value="enterCode"/>
                 <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
             </form>
+
+            <% if ( (Boolean)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ShowGoBackButton) ) { %>
+            <button type="submit" id="button-goBack" name="button-goBack" class="btn" form="form-goBack">
+                <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-backward"></span></pwm:if>
+                <pwm:display key="Button_GoBack"/>
+            </button>
+            <% } %>
+
             <pwm:if test="<%=PwmIfTest.showCancel%>">
-                <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" class="something">
+                <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded">
                     <input type="hidden" name="processAction" value="reset"/>
                     <button type="submit" name="button" class="btn" id="buttonCancel">
                         <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-times"></span>&nbsp</pwm:if>
@@ -64,13 +73,16 @@
     </div>
     <div class="push"></div>
 </div>
-<pwm:script>
-<script type="text/javascript">
-    PWM_GLOBAL['startupFunctions'].push(function(){
-        PWM_MAIN.getObject('<%=PwmConstants.PARAM_TOKEN%>').focus();
-    });
-</script>
-</pwm:script>
+<form id="form-goBack" action="<pwm:current-url/>" method="post">
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.clearTokenDestination%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_FORM_ID%>" value="<pwm:FormID/>"/>
+</form>
+<form id="form-cancel" action="<pwm:current-url/>" method="post">
+    <input type="hidden" name="<%=PwmConstants.PARAM_ACTION_REQUEST%>" value="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.exitActivation%>"/>
+    <input type="hidden" name="<%=PwmConstants.PARAM_FORM_ID%>" value="<pwm:FormID/>"/>
+</form>
 <%@ include file="fragment/footer.jsp" %>
 </body>
 </html>

+ 117 - 0
server/src/main/webapp/WEB-INF/jsp/activateuser-tokenchoice.jsp

@@ -0,0 +1,117 @@
+<%--
+ ~ Password Management Servlets (PWM)
+ ~ http://www.pwm-project.org
+ ~
+ ~ Copyright (c) 2006-2009 Novell, Inc.
+ ~ Copyright (c) 2009-2018 The PWM Project
+ ~
+ ~ This program is free software; you can redistribute it and/or modify
+ ~ it under the terms of the GNU General Public License as published by
+ ~ the Free Software Foundation; either version 2 of the License, or
+ ~ (at your option) any later version.
+ ~
+ ~ This program is distributed in the hope that it will be useful,
+ ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ ~ GNU General Public License for more details.
+ ~
+ ~ You should have received a copy of the GNU General Public License
+ ~ along with this program; if not, write to the Free Software
+ ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+--%>
+
+<%@ page import="password.pwm.bean.TokenDestinationItem" %>
+<%@ page import="java.util.List" %>
+<%@ page import="password.pwm.http.servlet.activation.ActivateUserServlet" %>
+
+<!DOCTYPE html>
+<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems ); %>
+<%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
+<%@ taglib uri="pwm" prefix="pwm" %>
+<html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
+<%@ include file="fragment/header.jsp" %>
+<body class="nihilo">
+<div id="wrapper">
+    <jsp:include page="fragment/header-body.jsp">
+        <jsp:param name="pwm.PageName" value="Title_ForgottenPassword"/>
+    </jsp:include>
+    <div id="centerbody">
+        <h1 id="page-content-title"><pwm:display key="Title_ForgottenPassword" displayIfMissing="true"/></h1>
+        <%@ 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"/>
+                            <% } 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="<%=item.getId()%>"/>
+                        <input type="hidden" name="processAction" value="<%=ActivateUserServlet.ActivateUserAction.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%>">
+                <tr>
+                    <td>
+                    </td>
+                    <td>
+                        <%=item.getDisplay()%>
+                    </td>
+                </tr>
+                <tr>
+                    <td>
+                        &nbsp;
+                    </td>
+                </tr>
+            </pwm:if>
+            <% } %>
+        </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">
+                    <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="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+                    <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.clearTokenDestination%>"/>
+                    <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="<%=ActivateUserServlet.ActivateUserAction.reset%>"/>
+                        <input type="hidden" name="<%=PwmConstants.PARAM_RESET_TYPE%>" value="<%=ActivateUserServlet.ResetType.exitActivation%>"/>
+                        <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" %>
+</body>
+</html>
+

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

@@ -25,7 +25,7 @@
 <%@ page import="java.util.List" %>
 
 <!DOCTYPE html>
-<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordTokenDestItems); %>
+<% List<TokenDestinationItem> tokenDestinationItems = (List)JspUtility.getAttribute(pageContext, PwmRequestAttribute.TokenDestItems); %>
 <%@ page language="java" session="true" isThreadSafe="true" contentType="text/html" %>
 <%@ taglib uri="pwm" prefix="pwm" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
@@ -84,8 +84,8 @@
         </table>
         <div>
         <div class="buttonbar">
+            <% if ("true".equals(JspUtility.getAttribute(pageContext, PwmRequestAttribute.ForgottenPasswordOptionalPageView))) { %>
             <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"/>