Explorar o código

replace 'enforce min lifetime' forgotten setting with 'min lifetime options' setting
add support for intruder-only mode min lifetime option.

Jason Rivard %!s(int64=7) %!d(string=hai) anos
pai
achega
cfa70be140
Modificáronse 24 ficheiros con 380 adicións e 157 borrados
  1. 1 1
      server/src/main/java/password/pwm/PwmConstants.java
  2. 12 2
      server/src/main/java/password/pwm/config/PwmSetting.java
  3. 30 0
      server/src/main/java/password/pwm/config/option/RecoveryMinLifetimeOption.java
  4. 17 30
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  5. 12 14
      server/src/main/java/password/pwm/health/LDAPStatusChecker.java
  6. 1 0
      server/src/main/java/password/pwm/http/PwmRequestAttribute.java
  7. 19 0
      server/src/main/java/password/pwm/http/bean/PwmSessionBean.java
  8. 17 0
      server/src/main/java/password/pwm/http/servlet/AbstractPwmServlet.java
  9. 1 0
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java
  10. 1 0
      server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java
  11. 2 14
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServlet.java
  12. 17 17
      server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServletUtil.java
  13. 58 39
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  14. 83 11
      server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java
  15. 2 0
      server/src/main/java/password/pwm/ldap/UserInfo.java
  16. 1 0
      server/src/main/java/password/pwm/ldap/UserInfoBean.java
  17. 12 0
      server/src/main/java/password/pwm/ldap/UserInfoReader.java
  18. 7 1
      server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java
  19. 50 18
      server/src/main/java/password/pwm/util/operations/PasswordUtility.java
  20. 17 6
      server/src/main/resources/password/pwm/config/PwmSetting.xml
  21. 1 0
      server/src/main/resources/password/pwm/i18n/Display.properties
  22. 5 3
      server/src/main/resources/password/pwm/i18n/PwmSetting.properties
  23. 8 0
      server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp
  24. 6 1
      server/src/main/webapp/WEB-INF/jsp/forgottenpassword-actionchoice.jsp

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

@@ -112,7 +112,7 @@ public abstract class PwmConstants
     public static final String SESSION_ATTR_PWM_SESSION = "PwmSession";
     public static final String SESSION_ATTR_BEANS = "SessionBeans";
     public static final String SESSION_ATTR_PWM_APP_NONCE = "PwmApplication-Nonce";
-    public static final String SESSION_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
+    public static final String REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE = "ForgottenPw-UserInfoCache";
 
     public static final PwmHashAlgorithm SETTING_CHECKSUM_HASH_METHOD = PwmHashAlgorithm.SHA256;
 

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

@@ -724,8 +724,6 @@ public enum PwmSetting
             "response.hashMethod", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_SETTINGS ),
     FORGOTTEN_USER_POST_ACTIONS(
             "recovery.postActions", PwmSettingSyntax.ACTION, PwmSettingCategory.RECOVERY_SETTINGS ),
-    CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME(
-            "challenge.enforceMinimumPasswordLifetime", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
     RECOVERY_BOGUS_USER_ENABLE(
             "recovery.bogus.user.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_SETTINGS ),
 
@@ -752,6 +750,9 @@ public enum PwmSetting
             "recovery.allowWhenLocked", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
     TOKEN_RESEND_ENABLE(
             "recovery.token.resend.enable", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
+    RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS(
+            "recovery.minimumPasswordLifetimeOptions", PwmSettingSyntax.SELECT, PwmSettingCategory.RECOVERY_OPTIONS ),
+
 
     // recovery oauth
     RECOVERY_OAUTH_ID_LOGIN_URL(
@@ -1157,6 +1158,11 @@ public enum PwmSetting
 
 
     // deprecated.
+
+    // deprecated 2018-02-27
+    RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME(
+            "challenge.enforceMinimumPasswordLifetime", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.RECOVERY_OPTIONS ),
+
     UPDATE_PROFILE_CHECK_QUERY_MATCH(
             "updateAttributes.check.queryMatch", PwmSettingSyntax.USER_PERMISSION, PwmSettingCategory.UPDATE_PROFILE ),
     PASSWORD_POLICY_AD_COMPLEXITY(
@@ -1168,6 +1174,10 @@ public enum PwmSetting
     HELPDESK_ENABLE_OTP_VERIFY(
             "helpdesk.otp.verify", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.HELPDESK_BASE ),;
 
+
+
+
+
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmSetting.class );
 
     private final String key;

+ 30 - 0
server/src/main/java/password/pwm/config/option/RecoveryMinLifetimeOption.java

@@ -0,0 +1,30 @@
+/*
+ * 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.config.option;
+
+public enum RecoveryMinLifetimeOption implements ConfigurationOption
+{
+    ALLOW,
+    UNLOCKONLY,
+    NONE
+}

+ 17 - 30
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -1391,39 +1391,26 @@ public class StoredConfigurationImpl implements StoredConfiguration
                 }
             }
 
-            /*
+            for ( final String profileID : storedConfiguration.profilesForSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME ) )
             {
-                if (!storedConfiguration.isDefaultValue(PwmSetting.CHALLENGE_REQUIRE_RESPONSES)) {
-                    final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
-                    final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
-                    final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
-                    newMethods.putAll(existingSettings.getMethodSettings());
-                    VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.disabled);
-                    newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
-                    final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
-                            newMethods,
-                            existingSettings.getMinOptionalRequired()
-                    );
-                    storedConfiguration.writeSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default", new VerificationMethodValue(newSettings), actor);
-                }
-            }
-
-            {
-                if (!storedConfiguration.isDefaultValue(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP)) {
-                    final StoredValue configValue = storedConfiguration.readSetting(PwmSetting.RECOVERY_VERIFICATION_METHODS, "default");
-                    final VerificationMethodValue.VerificationMethodSettings existingSettings = (VerificationMethodValue.VerificationMethodSettings)configValue.toNativeObject();
-                    final Map<RecoveryVerificationMethod,VerificationMethodValue.VerificationMethodSetting> newMethods = new HashMap<>();
-                    newMethods.putAll(existingSettings.getMethodSettings());
-                    VerificationMethodValue.VerificationMethodSetting setting = new VerificationMethodValue.VerificationMethodSetting(VerificationMethodValue.EnabledState.required);
-                    newMethods.put(RecoveryVerificationMethod.CHALLENGE_RESPONSES,setting);
-                    final VerificationMethodValue.VerificationMethodSettings newSettings = new VerificationMethodValue.VerificationMethodSettings(
-                            newMethods,
-                            existingSettings.getMinOptionalRequired()
-                    );
-                    storedConfiguration.writeSetting(PwmSetting.FORGOTTEN_PASSWORD_REQUIRE_OTP, "default", new VerificationMethodValue(newSettings), actor);
+                if ( !storedConfiguration.isDefaultValue( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ) )
+                {
+                    final boolean enforceEnabled = ( boolean ) storedConfiguration.readSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID ).toNativeObject();
+                    final StoredValue value = enforceEnabled
+                            ? new StringValue( "NONE" )
+                            : new StringValue( "ALLOW" );
+                    final ValueMetaData existingData = storedConfiguration.readSettingMetadata( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID );
+                    LOGGER.warn( "converting deprecated non-default setting "
+                            + PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug(profileID,PwmConstants.DEFAULT_LOCALE) + "/" + profileID
+                            + " to replacement setting " + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( profileID, PwmConstants.DEFAULT_LOCALE )
+                            + ", value=" + value.toNativeObject().toString() );
+                    final UserIdentity newActor = existingData != null && existingData.getUserIdentity() != null
+                            ? existingData.getUserIdentity()
+                            : actor;
+                    storedConfiguration.writeSetting( PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS, profileID, value, newActor );
+                    storedConfiguration.resetSetting( PwmSetting.RECOVERY_ENFORCE_MINIMUM_PASSWORD_LIFETIME, profileID, actor );
                 }
             }
-            */
         }
     }
 

+ 12 - 14
server/src/main/java/password/pwm/health/LDAPStatusChecker.java

@@ -50,7 +50,6 @@ import password.pwm.config.profile.PwmPasswordRule;
 import password.pwm.config.value.data.UserPermission;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.ldap.UserInfo;
@@ -312,20 +311,19 @@ public class LDAPStatusChecker implements HealthChecker
                             passwordStatus = userInfo.getPasswordStatus();
                         }
 
-                        try
-                        {
-                            PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                                    theUser,
-                                    SessionLabel.HEALTH_SESSION_LABEL,
-                                    passwordPolicy,
-                                    pwdLastModified,
-                                    passwordStatus
-                            );
-                        }
-                        catch ( PwmException e )
                         {
-                            LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, "skipping test user password set: " + e.getMessage() );
-                            doPasswordChange = false;
+                            final boolean withinMinLifetime = PasswordUtility.isPasswordWithinMinimumLifetimeImpl(
+                                            theUser,
+                                            SessionLabel.HEALTH_SESSION_LABEL,
+                                            passwordPolicy,
+                                            pwdLastModified,
+                                            passwordStatus
+                                    );
+                            if ( withinMinLifetime )
+                            {
+                                LOGGER.trace( SessionLabel.HEALTH_SESSION_LABEL, "skipping test user password set due to password being within minimum lifetime" );
+                                doPasswordChange = false;
+                            }
                         }
                     }
                     if ( doPasswordChange )

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

@@ -80,6 +80,7 @@ public enum PwmRequestAttribute
     ForgottenPasswordOtpRecord,
     ForgottenPasswordResendTokenEnabled,
     ForgottenPasswordTokenDestItems,
+    ForgottenPasswordInhibitPasswordReset,
 
     GuestCurrentExpirationDate,
     GuestMaximumExpirationDate,

+ 19 - 0
server/src/main/java/password/pwm/http/bean/PwmSessionBean.java

@@ -29,6 +29,9 @@ import password.pwm.error.ErrorInformation;
 
 import java.io.Serializable;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 @Getter
@@ -41,6 +44,17 @@ public abstract class PwmSessionBean implements Serializable
         AUTHENTICATED,
     }
 
+    private static List<Class<? extends PwmSessionBean>> publicBeans;
+
+    static
+    {
+        final List<Class<? extends PwmSessionBean>> list = new ArrayList<>(  );
+        list.add( ActivateUserBean.class );
+        list.add( ForgottenPasswordBean.class );
+        list.add( NewUserBean.class );
+        publicBeans = Collections.unmodifiableList( list );
+    }
+
     private String guid;
     private Instant timestamp;
     private ErrorInformation lastError;
@@ -48,4 +62,9 @@ public abstract class PwmSessionBean implements Serializable
     public abstract Type getType( );
 
     public abstract Set<SessionBeanMode> supportedModes( );
+
+    public static List<Class<? extends PwmSessionBean>> getPublicBeans()
+    {
+        return publicBeans;
+    }
 }

+ 17 - 0
server/src/main/java/password/pwm/http/servlet/AbstractPwmServlet.java

@@ -154,6 +154,23 @@ public abstract class AbstractPwmServlet extends HttpServlet implements PwmServl
             }
 
             outputUnrecoverableException( pwmRequest, pue );
+
+            clearModuleBeans( pwmRequest );
+        }
+    }
+
+    private void clearModuleBeans( final PwmRequest pwmRequest )
+    {
+        for ( final Class theClass : PwmSessionBean.getPublicBeans() )
+        {
+            try
+            {
+                pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, theClass );
+            }
+            catch ( PwmUnrecoverableException e )
+            {
+                LOGGER.debug( pwmRequest, "error while clearing module bean during after module error output: " + e.getMessage() );
+            }
         }
     }
 

+ 1 - 0
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataBean.java

@@ -41,6 +41,7 @@ public class UserDebugDataBean implements Serializable
 
     private final PublicUserInfoBean publicUserInfoBean;
     private final boolean passwordReadable;
+    private final boolean passwordWithinMinimumLifetime;
     private final Map<Permission, String> permissions;
 
     private final PwmPasswordPolicy ldapPasswordPolicy;

+ 1 - 0
server/src/main/java/password/pwm/http/servlet/admin/UserDebugDataReader.java

@@ -92,6 +92,7 @@ public class UserDebugDataReader
                 .ldapPasswordPolicy( ldapPasswordPolicy )
                 .configuredPasswordPolicy( configPasswordPolicy )
                 .passwordReadable( readablePassword )
+                .passwordWithinMinimumLifetime( userInfo.isWithinPasswordMinimumLifetime() )
                 .build();
 
         return userDebugData;

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

@@ -23,7 +23,6 @@
 package password.pwm.http.servlet.changepw;
 
 import com.novell.ldapchai.ChaiUser;
-import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import password.pwm.AppProperty;
 import password.pwm.Permission;
@@ -441,7 +440,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
     public void nextStep(
             final PwmRequest pwmRequest
     )
-            throws IOException, PwmUnrecoverableException, ChaiUnavailableException, ServletException
+            throws IOException, PwmUnrecoverableException, ServletException
     {
         final ChangePasswordBean changePasswordBean = pwmRequest.getPwmApplication().getSessionStateService().getBean( pwmRequest, ChangePasswordBean.class );
 
@@ -546,18 +545,7 @@ public abstract class ChangePasswordServlet extends ControlledPwmServlet
             return ProcessStatus.Halt;
         }
 
-        try
-        {
-            ChangePasswordServletUtil.checkMinimumLifetime( pwmApplication, pwmSession, changePasswordBean, pwmSession.getUserInfo() );
-        }
-        catch ( PwmOperationalException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
-        catch ( ChaiException e )
-        {
-            throw PwmUnrecoverableException.fromChaiException( e );
-        }
+        ChangePasswordServletUtil.checkMinimumLifetime( pwmApplication, pwmSession, changePasswordBean, pwmSession.getUserInfo() );
 
         return ProcessStatus.Continue;
     }

+ 17 - 17
server/src/main/java/password/pwm/http/servlet/changepw/ChangePasswordServletUtil.java

@@ -36,12 +36,12 @@ 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.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmSession;
 import password.pwm.http.bean.ChangePasswordBean;
+import password.pwm.http.servlet.forgottenpw.ForgottenPasswordUtil;
 import password.pwm.ldap.PasswordChangeProgressChecker;
 import password.pwm.ldap.UserInfo;
 import password.pwm.ldap.auth.AuthenticationType;
@@ -171,34 +171,34 @@ public class ChangePasswordServletUtil
             final ChangePasswordBean changePasswordBean,
             final UserInfo userInfo
     )
-            throws PwmUnrecoverableException, ChaiUnavailableException, PwmOperationalException
+            throws PwmUnrecoverableException
     {
         if ( changePasswordBean.isNextAllowedTimePassed() )
         {
             return;
         }
 
-        try
-        {
-            PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                    pwmSession.getSessionManager().getActor( pwmApplication ),
-                    pwmSession.getLabel(),
-                    userInfo.getPasswordPolicy(),
-                    userInfo.getPasswordLastModifiedTime(),
-                    userInfo.getPasswordStatus()
-            );
-        }
-        catch ( PwmException e )
+        if ( userInfo.isWithinPasswordMinimumLifetime() )
         {
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
-            if ( !enforceFromForgotten && userInfo.isRequiresNewPassword() )
+            boolean allowChange = false;
+            if ( pwmSession.getLoginInfoBean().getAuthFlags().contains( AuthenticationType.AUTH_FROM_PUBLIC_MODULE ) )
+            {
+                allowChange = ForgottenPasswordUtil.permitPwChangeDuringMinLifetime(
+                        pwmApplication,
+                        pwmSession.getLabel(),
+                        userInfo.getUserIdentity()
+                );
+
+            }
+
+            if ( allowChange )
             {
                 LOGGER.debug( pwmSession, "current password is too young, but skipping enforcement of minimum lifetime check due to setting "
-                        + PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME.toMenuLocationDebug( null, pwmSession.getSessionStateBean().getLocale() ) );
+                        + PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS.toMenuLocationDebug( null, pwmSession.getSessionStateBean().getLocale() ) );
             }
             else
             {
-                throw new PwmUnrecoverableException( e.getErrorInformation() );
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmSession.getLabel() );
             }
         }
 

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

@@ -41,6 +41,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.value.data.FormConfiguration;
@@ -223,19 +224,12 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         pwmRequest.getPwmApplication().getSessionStateService().clearBean( pwmRequest, ForgottenPasswordBean.class );
     }
 
-    static ForgottenPasswordProfile forgottenPasswordProfile( final PwmRequest pwmRequest ) throws PwmUnrecoverableException
-    {
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        return pwmRequest.getConfig().getForgottenPasswordProfiles().get( forgottenPasswordBean.getForgottenPasswordProfileID() );
-    }
-
-
     @ActionHandler( action = "actionChoice" )
     private ProcessStatus processActionChoice( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException, ServletException, IOException, ChaiUnavailableException
     {
         final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
-        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
 
         final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
 
@@ -245,6 +239,19 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             forgottenPasswordBean.getProgress().clearTokenSentStatus();
         }
 
+
+        final boolean disallowAllButUnlock;
+        {
+            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+            final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                    PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                    RecoveryMinLifetimeOption.class
+            );
+            disallowAllButUnlock = minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY
+                    && userInfo.isPasswordLocked();
+        }
+
+
         if ( forgottenPasswordBean.getProgress().isAllPassed() )
         {
             final String choice = pwmRequest.readParameterAsString( "choice" );
@@ -259,6 +266,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
                         break;
 
                     case resetPassword:
+                        if ( disallowAllButUnlock )
+                        {
+                            final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
+                            PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+                        }
                         this.executeResetPassword( pwmRequest );
                         break;
 
@@ -772,8 +784,10 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
     private ProcessStatus processResendToken( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException, IOException
     {
+        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
+
         {
-            final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+            final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
             final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
             if ( !resendEnabled )
             {
@@ -783,7 +797,6 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             }
         }
 
-        final ForgottenPasswordBean forgottenPasswordBean = forgottenPasswordBean( pwmRequest );
 
         if ( !forgottenPasswordBean.getProgress().isTokenSent() )
         {
@@ -927,7 +940,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
             return;
         }
 
-        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
         {
             final Map<String, ForgottenPasswordProfile> profileIDList = pwmRequest.getConfig().getForgottenPasswordProfiles();
             final String profileDebugMsg = forgottenPasswordProfile != null && profileIDList != null && profileIDList.size() > 1
@@ -1051,31 +1064,40 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
         }
 
         final UserInfo userInfo = ForgottenPasswordUtil.readUserInfo( pwmRequest, forgottenPasswordBean );
-        try
+        if ( userInfo == null )
         {
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
-            if ( enforceFromForgotten )
-            {
-                final ChaiUser theUser = pwmApplication.getProxiedChaiUser( forgottenPasswordBean.getUserIdentity() );
-                PasswordUtility.checkIfPasswordWithinMinimumLifetime(
-                        theUser,
-                        pwmRequest.getSessionLabel(),
-                        userInfo.getPasswordPolicy(),
-                        userInfo.getPasswordLastModifiedTime(),
-                        userInfo.getPasswordStatus()
-                );
-            }
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_UNKNOWN, "unable to load userInfo while processing forgotten password controller" );
         }
-        catch ( PwmOperationalException e )
+
+        // check if user's pw is within min lifetime window
+        final RecoveryMinLifetimeOption minLifetimeOption = forgottenPasswordProfile.readSettingAsEnum(
+                PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                RecoveryMinLifetimeOption.class
+        );
+        if ( minLifetimeOption == RecoveryMinLifetimeOption.NONE
+                || (
+                !userInfo.isPasswordLocked()
+                        &&  minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY )
+                )
         {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
+            if ( userInfo.isWithinPasswordMinimumLifetime() )
+            {
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+            }
         }
 
+        final boolean disallowAllButUnlock = minLifetimeOption == RecoveryMinLifetimeOption.UNLOCKONLY
+                && userInfo.isPasswordLocked();
+
         LOGGER.trace( pwmRequest, "all recovery checks passed, proceeding to configured recovery action" );
 
         final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction( config, forgottenPasswordBean );
         if ( recoveryAction == RecoveryAction.SENDNEWPW || recoveryAction == RecoveryAction.SENDNEWPW_AND_EXPIRE )
         {
+            if ( disallowAllButUnlock )
+            {
+                PasswordUtility.throwPasswordTooSoonException( userInfo, pwmRequest.getSessionLabel() );
+            }
             ForgottenPasswordUtil.doActionSendNewPassword( pwmRequest );
             return;
         }
@@ -1086,18 +1108,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             if ( !passwordStatus.isExpired() && !passwordStatus.isPreExpired() )
             {
-                try
+                if ( userInfo.isPasswordLocked() )
                 {
-                    final ChaiUser theUser = pwmApplication.getProxiedChaiUser( forgottenPasswordBean.getUserIdentity() );
-                    if ( theUser.isPasswordLocked() )
-                    {
-                        pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ACTION_CHOICE );
-                        return;
-                    }
-                }
-                catch ( ChaiOperationException e )
-                {
-                    LOGGER.error( pwmRequest, "chai operation error checking user lock status: " + e.getMessage() );
+                    pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset, Boolean.TRUE );
+                    pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ACTION_CHOICE );
+                    return;
                 }
             }
         }
@@ -1416,7 +1431,11 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
                 if ( !progress.getSatisfiedMethods().contains( IdentityVerificationMethod.TOKEN ) )
                 {
-                    final boolean resendEnabled = forgottenPasswordProfile( pwmRequest ).readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
+                    final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile(
+                            pwmRequest.getPwmApplication(),
+                            forgottenPasswordBean
+                    );
+                    final boolean resendEnabled = forgottenPasswordProfile.readSettingAsBoolean( PwmSetting.TOKEN_RESEND_ENABLE );
                     pwmRequest.setAttribute( PwmRequestAttribute.ForgottenPasswordResendTokenEnabled, resendEnabled );
                     pwmRequest.forwardToJsp( JspUrl.RECOVER_PASSWORD_ENTER_TOKEN );
                     return;
@@ -1455,7 +1474,7 @@ public class ForgottenPasswordServlet extends ControlledPwmServlet
 
             case OAUTH:
                 forgottenPasswordBean.getProgress().setInProgressVerificationMethod( IdentityVerificationMethod.OAUTH );
-                final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest );
+                final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
                 final OAuthSettings oAuthSettings = OAuthSettings.forForgottenPassword( forgottenPasswordProfile );
                 final OAuthMachine oAuthMachine = new OAuthMachine( oAuthSettings );
                 pwmRequest.getPwmApplication().getSessionStateService().saveSessionBeans( pwmRequest );

+ 83 - 11
server/src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordUtil.java

@@ -42,6 +42,7 @@ import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
+import password.pwm.config.option.RecoveryMinLifetimeOption;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ProfileType;
 import password.pwm.config.profile.ProfileUtility;
@@ -88,7 +89,7 @@ import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-class ForgottenPasswordUtil
+public class ForgottenPasswordUtil
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( ForgottenPasswordUtil.class );
 
@@ -146,12 +147,12 @@ class ForgottenPasswordUtil
             return null;
         }
 
-        final String cacheKey = PwmConstants.SESSION_ATTR_FORGOTTEN_PW_USERINFO_CACHE;
+        final String cacheKey = PwmConstants.REQUEST_ATTR_FORGOTTEN_PW_USERINFO_CACHE;
 
         final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
 
         {
-            final UserInfo userInfoFromSession = ( UserInfo ) pwmRequest.getHttpServletRequest().getSession().getAttribute( cacheKey );
+            final UserInfo userInfoFromSession = ( UserInfo ) pwmRequest.getHttpServletRequest().getAttribute( cacheKey );
             if ( userInfoFromSession != null )
             {
                 if ( userIdentity.equals( userInfoFromSession.getUserIdentity() ) )
@@ -173,7 +174,7 @@ class ForgottenPasswordUtil
                 userIdentity, pwmRequest.getLocale()
         );
 
-        pwmRequest.getHttpServletRequest().getSession().setAttribute( cacheKey, userInfo );
+        pwmRequest.getHttpServletRequest().setAttribute( cacheKey, userInfo );
 
         return userInfo;
     }
@@ -572,7 +573,7 @@ class ForgottenPasswordUtil
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final ForgottenPasswordBean forgottenPasswordBean = ForgottenPasswordServlet.forgottenPasswordBean( pwmRequest );
-        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordServlet.forgottenPasswordProfile( pwmRequest );
+        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile( pwmRequest.getPwmApplication(), forgottenPasswordBean );
         final RecoveryAction recoveryAction = ForgottenPasswordUtil.getRecoveryAction( pwmApplication.getConfig(), forgottenPasswordBean );
 
         LOGGER.trace( pwmRequest, "beginning process to send new password to user" );
@@ -744,6 +745,77 @@ class ForgottenPasswordUtil
         forgottenPasswordBean.setRecoveryFlags( recoveryFlags );
     }
 
+    public static boolean permitPwChangeDuringMinLifetime(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        ForgottenPasswordProfile forgottenPasswordProfile = null;
+        try
+        {
+            forgottenPasswordProfile = ForgottenPasswordUtil.forgottenPasswordProfile(
+                    pwmApplication,
+                    sessionLabel,
+                    userIdentity
+            );
+        }
+        catch ( PwmUnrecoverableException e )
+        {
+            LOGGER.debug( sessionLabel, "can't read user's forgotten password profile - assuming no profile assigned, error: " + e.getMessage() );
+        }
+
+        if ( forgottenPasswordProfile == null )
+        {
+            // default is true.
+            return true;
+        }
+
+        final RecoveryMinLifetimeOption option = forgottenPasswordProfile.readSettingAsEnum(
+                PwmSetting.RECOVERY_MINIMUM_PASSWORD_LIFETIME_OPTIONS,
+                RecoveryMinLifetimeOption.class
+        );
+        return option == RecoveryMinLifetimeOption.ALLOW;
+    }
+
+    private static ForgottenPasswordProfile forgottenPasswordProfile(
+            final PwmApplication pwmApplication,
+            final SessionLabel sessionLabel,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException
+    {
+        final String forgottenProfileID = ProfileUtility.discoverProfileIDforUser(
+                pwmApplication,
+                sessionLabel,
+                userIdentity,
+                ProfileType.ForgottenPassword
+        );
+
+        if ( StringUtil.isEmpty( forgottenProfileID ) )
+        {
+            final String msg = "user does not have a forgotten password profile assigned";
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_NO_PROFILE_ASSIGNED, msg );
+        }
+
+        return pwmApplication.getConfig().getForgottenPasswordProfiles().get( forgottenProfileID );
+    }
+
+    static ForgottenPasswordProfile forgottenPasswordProfile(
+            final PwmApplication pwmApplication,
+            final ForgottenPasswordBean forgottenPasswordBean
+    )
+    {
+        final String forgottenProfileID = forgottenPasswordBean.getForgottenPasswordProfileID();
+        if ( StringUtil.isEmpty( forgottenProfileID ) )
+        {
+            throw new IllegalStateException( "cannot load forgotten profile without ID registered in bean" );
+        }
+        return pwmApplication.getConfig().getForgottenPasswordProfiles().get( forgottenProfileID );
+    }
+
+
     static void initForgottenPasswordBean(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity,
@@ -760,13 +832,13 @@ class ForgottenPasswordUtil
 
         final UserInfo userInfo = readUserInfo( pwmRequest, forgottenPasswordBean );
 
-        final String forgottenProfileID = ProfileUtility.discoverProfileIDforUser( pwmApplication, sessionLabel, userIdentity, ProfileType.ForgottenPassword );
-        if ( forgottenProfileID == null || forgottenProfileID.isEmpty() )
-        {
-            throw new PwmUnrecoverableException( PwmError.ERROR_NO_PROFILE_ASSIGNED.toInfo() );
-        }
+        final ForgottenPasswordProfile forgottenPasswordProfile = forgottenPasswordProfile(
+                pwmApplication,
+                pwmRequest.getSessionLabel(),
+                userIdentity
+        );
+        final String forgottenProfileID = forgottenPasswordProfile.getIdentifier();
         forgottenPasswordBean.setForgottenPasswordProfileID( forgottenProfileID );
-        final ForgottenPasswordProfile forgottenPasswordProfile = ForgottenPasswordServlet.forgottenPasswordProfile( pwmRequest );
 
         final ForgottenPasswordBean.RecoveryFlags recoveryFlags = calculateRecoveryFlags(
                 pwmApplication,

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

@@ -72,6 +72,8 @@ public interface UserInfo
 
     boolean isPasswordLocked( ) throws PwmUnrecoverableException;
 
+    boolean isWithinPasswordMinimumLifetime( ) throws PwmUnrecoverableException;
+
     Instant getPasswordLastModifiedTime( ) throws PwmUnrecoverableException;
 
     String getUserEmailAddress( ) throws PwmUnrecoverableException;

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

@@ -89,6 +89,7 @@ public class UserInfoBean implements UserInfo
     private final boolean requiresOtpConfig;
     private final boolean requiresUpdateProfile;
     private final boolean requiresInteraction;
+    private final boolean withinPasswordMinimumLifetime;
 
     @Builder.Default
     private Map<String, String> attributes = Collections.emptyMap();

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

@@ -794,4 +794,16 @@ public class UserInfoReader implements UserInfo
                 || selfCachedReference.isRequiresOtpConfig()
                 || selfCachedReference.getPasswordStatus().isWarnPeriod();
     }
+
+    @Override
+    public boolean isWithinPasswordMinimumLifetime( ) throws PwmUnrecoverableException
+    {
+        return PasswordUtility.isPasswordWithinMinimumLifetimeImpl(
+                this.chaiUser,
+                this.sessionLabel,
+                this.getPasswordPolicy(),
+                this.getPasswordLastModifiedTime(),
+                this.getPasswordStatus()
+        );
+    }
 }

+ 7 - 1
server/src/main/java/password/pwm/ldap/auth/LDAPAuthenticationRequest.java

@@ -45,6 +45,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.servlet.forgottenpw.ForgottenPasswordUtil;
 import password.pwm.ldap.LdapOperationsHelper;
 import password.pwm.svc.event.AuditEvent;
 import password.pwm.svc.event.AuditRecord;
@@ -512,7 +513,12 @@ class LDAPAuthenticationRequest implements AuthenticationRequest
         {
             final Instant date = OracleDSEntries.convertZuluToDate( oracleDSPrePasswordAllowChangeTime );
 
-            final boolean enforceFromForgotten = pwmApplication.getConfig().readSettingAsBoolean( PwmSetting.CHALLENGE_ENFORCE_MINIMUM_PASSWORD_LIFETIME );
+            final boolean enforceFromForgotten = !ForgottenPasswordUtil.permitPwChangeDuringMinLifetime(
+                    pwmApplication,
+                    sessionLabel,
+                    userIdentity
+            );
+
             if ( enforceFromForgotten )
             {
                 if ( Instant.now().isBefore( date ) )

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

@@ -1234,14 +1234,55 @@ public class PasswordUtility
         return null;
     }
 
-    public static void checkIfPasswordWithinMinimumLifetime(
+    public static void throwPasswordTooSoonException(
+            final UserInfo userInfo,
+            final SessionLabel sessionLabel
+    )
+            throws PwmUnrecoverableException
+    {
+        if ( !userInfo.isWithinPasswordMinimumLifetime() )
+        {
+            return;
+        }
+
+        final Instant lastModified = userInfo.getPasswordLastModifiedTime();
+        final TimeDuration minimumLifetime;
+        {
+            final int minimumLifetimeSeconds = userInfo.getPasswordPolicy().getRuleHelper().readIntValue( PwmPasswordRule.MinimumLifetime );
+            if ( minimumLifetimeSeconds < 1 )
+            {
+                return;
+            }
+
+            if ( userInfo.getPasswordPolicy() == null )
+            {
+                LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password last set time is unknown" );
+                return;
+            }
+
+            minimumLifetime = new TimeDuration( minimumLifetimeSeconds, TimeUnit.SECONDS );
+
+        }
+        final Instant allowedChangeDate = Instant.ofEpochMilli( lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds() );
+        final TimeDuration passwordAge = TimeDuration.fromCurrent( lastModified );
+        final String msg = "last password change was at "
+                + JavaHelper.toIsoDate( lastModified )
+                + " and is too recent (" + passwordAge.asCompactString()
+                + " ago), password cannot be changed within minimum lifetime of "
+                + minimumLifetime.asCompactString()
+                + ", next eligible time to change is after " + JavaHelper.toIsoDate( allowedChangeDate );
+        throw PwmUnrecoverableException.newException( PwmError.PASSWORD_TOO_SOON, msg );
+
+    }
+
+    public static boolean isPasswordWithinMinimumLifetimeImpl(
             final ChaiUser chaiUser,
             final SessionLabel sessionLabel,
             final PwmPasswordPolicy passwordPolicy,
             final Instant lastModified,
             final PasswordStatus passwordStatus
     )
-            throws PwmOperationalException, PwmUnrecoverableException
+            throws PwmUnrecoverableException
     {
 
         // for oracle DS; this check is also handled in UserAuthenticator.
@@ -1261,7 +1302,7 @@ public class PasswordUtility
                         throw new PwmUnrecoverableException( errorInformation );
                     }
                 }
-                return;
+                return false;
             }
         }
         catch ( ChaiException e )
@@ -1274,13 +1315,13 @@ public class PasswordUtility
             final int minimumLifetimeSeconds = passwordPolicy.getRuleHelper().readIntValue( PwmPasswordRule.MinimumLifetime );
             if ( minimumLifetimeSeconds < 1 )
             {
-                return;
+                return false;
             }
 
             if ( lastModified == null )
             {
                 LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password last set time is unknown" );
-                return;
+                return false;
             }
 
             minimumLifetime = new TimeDuration( minimumLifetimeSeconds, TimeUnit.SECONDS );
@@ -1296,31 +1337,22 @@ public class PasswordUtility
         if ( lastModified.isAfter( Instant.now() ) )
         {
             LOGGER.debug( sessionLabel, "skipping minimum lifetime check, password lastModified time is in the future" );
-            return;
+            return false;
         }
 
         final boolean passwordTooSoon = passwordAge.isShorterThan( minimumLifetime );
         if ( !passwordTooSoon )
         {
             LOGGER.trace( sessionLabel, "minimum lifetime check passed, password age " );
-            return;
+            return false;
         }
 
         if ( passwordStatus.isExpired() || passwordStatus.isPreExpired() || passwordStatus.isWarnPeriod() )
         {
             LOGGER.debug( sessionLabel, "current password is too young, but skipping enforcement of minimum lifetime check because current password is expired" );
-            return;
+            return false;
         }
 
-        final Instant allowedChangeDate = Instant.ofEpochMilli( lastModified.toEpochMilli() + minimumLifetime.getTotalMilliseconds() );
-        final String errorMsg = "last password change was at "
-                + JavaHelper.toIsoDate( lastModified )
-                + " and is too recent (" + passwordAge.asCompactString()
-                + " ago), password cannot be changed within minimum lifetime of "
-                + minimumLifetime.asCompactString()
-                + ", next eligible time to change is after " + JavaHelper.toIsoDate( allowedChangeDate );
-
-        final ErrorInformation errorInformation = new ErrorInformation( PwmError.PASSWORD_TOO_SOON, errorMsg );
-        throw new PwmOperationalException( errorInformation );
+        return true;
     }
 }

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

@@ -2311,11 +2311,6 @@
             <value><![CDATA[{"type":"ldapQuery","ldapProfileID":"all","ldapQuery":"(objectClass=*)"}]]></value>
         </default>
     </setting>
-    <setting hidden="false" key="challenge.enforceMinimumPasswordLifetime" level="2" required="true">
-        <default>
-            <value>false</value>
-        </default>
-    </setting>
     <setting hidden="true" key="challenge.profile.list" level="1">
         <regex>^(?!.*all.*)([a-zA-Z][a-zA-Z0-9-]{2,15})$</regex>
         <properties>
@@ -2342,7 +2337,7 @@
             <option value="TOKEN">SMS/Email Token Verification</option>
             <option value="OTP">OTP (Mobile Device) Verification</option>
             <option value="REMOTE_RESPONSES">External Responses</option>
-            <option value="OAUTH">OAuth2</option>
+            <option value="OAUTH">OAuth</option>
         </options>
     </setting>
     <setting hidden="false" key="recovery.oauth.idserver.loginUrl" level="2">
@@ -2535,6 +2530,16 @@
             <value>true</value>
         </default>
     </setting>
+    <setting hidden="false" key="recovery.minimumPasswordLifetimeOptions" level="1" required="true">
+        <default>
+            <value>ALLOW</value>
+        </default>
+        <options>
+            <option value="ALLOW">Allow - Allow normal action (ignore minimum lifetime)</option>
+            <option value="UNLOCKONLY">UnlockOnly - Allow only intruder password unlock</option>
+            <option value="NONE">None - Prohibit usage of the forgotten password module</option>
+        </options>
+    </setting>
     <setting hidden="false" key="forgottenUsername.enable" level="1" required="true">
         <default>
             <value>false</value>
@@ -3830,6 +3835,12 @@
             <value>false</value>
         </default>
     </setting>
+    <setting hidden="true" key="challenge.enforceMinimumPasswordLifetime" level="2" required="true">
+        <!-- deprecated 2018-02-27-->
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <!-- DEPRECATED SETTINGS -->
     <category hidden="false" key="TEMPLATES">
     </category>

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

@@ -140,6 +140,7 @@ Display_RecoverTokenSendChoices=To verify your identity, a security code will be
 Display_RecoverTokenSendChoiceEmail=Send code to your registered email address.
 Display_RecoverTokenSendChoiceSMS=Send code to your mobile phone using text messaging (SMS).
 Display_RecoverChoiceReset=Set a new password.  If you have forgotten your password and would like to set a new one, click here.  Your account will also be unlocked when you set a new password.
+Display_RecoverChoiceResetInhibit=Your password can not be changed at this time because it is within the minimum password lifetime limit.
 Display_RecoverChoiceUnlock=Unlock your account.  If you remember your password, you can unlock your account by selecting this option.  Your password will not be changed.
 Display_RecoverEnterCode=To verify your identity, a security code has been sent to you at %1%.  Please click the link in the email or copy and paste the security code here.
 Display_RecoverEnterCodeSMS=To verify your identity, a security code has been sent to your phone at %1%.  Please enter the security code in the message here.

+ 5 - 3
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -242,7 +242,7 @@ Setting_Description_cas.clearPass.key=<a href\="https://apereo.github.io/cas/4.2
 Setting_Description_cas.clearPass.alg=The algorithm used by the encryption key
 Setting_Description_challenge.allowDuplicateResponses=Enable this to allow duplicate responses in setup security responses
 Setting_Description_challenge.allowSetup.queryMatch=Specify the permissions used to determine if you permits the users to configure challenges.  This LDAP query must return the user or else @PwmAppName@ does not permit the user to configure challenges.
-Setting_Description_challenge.allowUnlock=Enable this option if @PwmAppName@ allows user accounts to be unlocked during forgotten password.  If true, and if the users' accounts are locked due to too many invalid login attempts, and the users' passwords are not expired, then @PwmAppName@ gives the users a chance to unlock their accounts instead of resetting their passwords.
+Setting_Description_challenge.allowUnlock=Enable this option to allow users to intruder unlock their account during forgotten password.  If true, and if the users' accounts are intruder locked due to too many invalid login attempts, and the users' passwords are not expired, then @PwmAppName@ gives the users a chance to unlock their accounts instead of resetting their passwords.
 Setting_Description_challenge.caseInsensitive=Enable to control the case sensitivity of responses.  If enabled, then @PwmAppName@ deems the responses correct even if the case is wrong.  Changing this value does not change existing stored responses -- @PwmAppName@ saves the case sensitive flag on each users' stored responses.
 Setting_Description_challenge.enable=Enable this option to have the save responses page available to users. (Default enabled)
 Setting_Description_challenge.enforceMinimumPasswordLifetime=Enable this option to enforce the minimum password lifetime setting when the users authenticate via Forgotten Password. If this setting is true, the users cannot change their passwords if the minimum password lifetime setting has not passed.  If false, @PwmAppName@ permits the users to change their passwords when they are authenticated via Forgotten Password even if the minimum lifetime setting has not passed.
@@ -600,6 +600,7 @@ Setting_Description_recovery.allowWhenLocked=Enable this option to allow users t
 Setting_Description_recovery.bogus.user.enable=Enable this option to have forgotten password act as though invalid user searches are valid, and present such users with a bogus forgotten password policy.  This can help prevent username discovery.
 Setting_Description_recovery.enable=Enable this option to have the forgotten password recovery available to users.
 Setting_Description_recovery.form=Specify the form fields for the activate user module. @PwmAppName@ requires the users to enter each attribute. Ideally, @PwmAppName@ requires the users to enter some personal data that is not publicly known.
+Setting_Description_recovery.minimumPasswordLifetimeOptions=Options to control behavior when a user attempts to use the forgotten password module while their password is within the minimum password policy lifetime window of their effective password policy.  These options are only relevant if the user has an effective minimum password lifetime as part of their password policy.
 Setting_Description_recovery.oauth.idserver.attributesUrl=Specify the web service URL provided by the identity server to return attribute data about the user.
 Setting_Description_recovery.oauth.idserver.clientName=Specify the OAuth client ID. The OAuth identity service provider gives you this value.
 Setting_Description_recovery.oauth.idserver.codeResolveUrl=Specify the OAuth Code Resolve Service URL. @PwmAppName@ uses this web service URL to resolve the artifact returned by the OAuth identity server.
@@ -727,7 +728,7 @@ Setting_Label_cas.clearPass.key=CAS ClearPass Encryption Key
 Setting_Label_cas.clearPass.alg=CAS ClearPass Algorithm
 Setting_Label_challenge.allowDuplicateResponses=Allow Duplicate Responses
 Setting_Label_challenge.allowSetup.queryMatch=Save Challenge Permission
-Setting_Label_challenge.allowUnlock=Allow Unlock
+Setting_Label_challenge.allowUnlock=Allow Intruder Unlock
 Setting_Label_challenge.caseInsensitive=Case Insensitive Responses
 Setting_Label_challenge.enable=Enable Setup Responses
 Setting_Label_challenge.enforceMinimumPasswordLifetime=Enforce Minimum Password Lifetime
@@ -899,7 +900,7 @@ Setting_Label_intruder.address.resetTime=Intruder Address Reset Time
 Setting_Label_intruder.attribute.checkTime=Intruder Attribute Check Time
 Setting_Label_intruder.attribute.maxAttempts=Intruder Attribute Maximum Attempts
 Setting_Label_intruder.attribute.resetTime=Intruder Attribute Reset Time
-Setting_Label_intruder.enable=Enable Intruder Detection
+Setting_Label_intruder.enable=Enable @PwmAppName@ Intruder Detection
 Setting_Label_intruder.session.maxAttempts=Maximum Intruder Attempts Per Session
 Setting_Label_intruder.storageMethod=Intruder Record Storage Location
 Setting_Label_intruder.tokenDest.checkTime=Intruder Token Destination Check Time
@@ -1085,6 +1086,7 @@ Setting_Label_recovery.allowWhenLocked=Allow Forgotten Password when Locked
 Setting_Label_recovery.bogus.user.enable=Enable Bogus User Policy
 Setting_Label_recovery.enable=Enable Forgotten Password
 Setting_Label_recovery.form=Forgotten Password User Search Form
+Setting_Label_recovery.minimumPasswordLifetimeOptions=Minimum Password Lifetime Options
 Setting_Label_recovery.oauth.idserver.attributesUrl=OAuth Profile Service URL
 Setting_Label_recovery.oauth.idserver.clientName=OAuth Client ID
 Setting_Label_recovery.oauth.idserver.codeResolveUrl=OAuth Code Resolve Service URL

+ 8 - 0
server/src/main/webapp/WEB-INF/jsp/admin-user-debug.jsp

@@ -196,6 +196,14 @@
                     <%= JspUtility.freindlyWrite(pageContext, userInfo.isRequiresUpdateProfile()) %>
                 </td>
             </tr>
+            <tr>
+                <td class="key">
+                    Password is Within Minimum Lifetime
+                </td>
+                <td>
+                    <%= JspUtility.freindlyWrite(pageContext, userDebugDataBean.isPasswordWithinMinimumLifetime()) %>
+                </td>
+            </tr>
         </table>
         <br/>
         <table>

+ 6 - 1
server/src/main/webapp/WEB-INF/jsp/forgottenpassword-actionchoice.jsp

@@ -27,6 +27,7 @@
 <%@ taglib uri="pwm" prefix="pwm" %>
 <html lang="<pwm:value name="<%=PwmValue.localeCode%>"/>" dir="<pwm:value name="<%=PwmValue.localeDir%>"/>">
 <%@ include file="fragment/header.jsp" %>
+<% boolean passwordResetInhibit = (boolean)JspUtility.getAttribute( pageContext, PwmRequestAttribute.ForgottenPasswordInhibitPasswordReset ); %>
 <body class="nihilo">
 <div id="wrapper">
     <jsp:include page="fragment/header-body.jsp">
@@ -61,7 +62,7 @@
             <tr>
                 <td>
                     <form action="<pwm:current-url/>" method="post" enctype="application/x-www-form-urlencoded" name="search">
-                        <button class="btn" type="submit" name="submitBtn">
+                        <button class="btn" type="submit" name="submitBtn" <%=passwordResetInhibit?"disabled=\"disabled\"":""%>>
                             <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="btn-icon pwm-icon pwm-icon-key"></span></pwm:if>
                             <pwm:display key="Button_ChangePassword"/>
                         </button>
@@ -71,7 +72,11 @@
                     </form>
                 </td>
                 <td>
+                    <% if (passwordResetInhibit) { %>
+                    <pwm:display key="Display_RecoverChoiceResetInhibit"/>
+                    <% } else { %>
                     <pwm:display key="Display_RecoverChoiceReset"/>
+                    <% } %>
                 </td>
             </tr>
             <tr>