Jason Rivard пре 8 година
родитељ
комит
541f5fedbd

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

@@ -282,6 +282,7 @@ public enum AppProperty {
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ("security.defaultEphemeralHashAlg"),
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ("security.defaultEphemeralHashAlg"),
     SEEDLIST_BUILTIN_PATH                           ("seedlist.builtin.path"),
     SEEDLIST_BUILTIN_PATH                           ("seedlist.builtin.path"),
     SMTP_SUBJECT_ENCODING_CHARSET                   ("smtp.subjectEncodingCharset"),
     SMTP_SUBJECT_ENCODING_CHARSET                   ("smtp.subjectEncodingCharset"),
+    TOKEN_CLEANER_INTERVAL_SECONDS                  ("token.cleaner.intervalSeconds"),
     TOKEN_MASK_EMAIL_REGEX                          ("token.mask.email.regex"),
     TOKEN_MASK_EMAIL_REGEX                          ("token.mask.email.regex"),
     TOKEN_MASK_EMAIL_REPLACE                        ("token.mask.email.replace"),
     TOKEN_MASK_EMAIL_REPLACE                        ("token.mask.email.replace"),
     TOKEN_MASK_SMS_REGEX                            ("token.mask.sms.regex"),
     TOKEN_MASK_SMS_REGEX                            ("token.mask.sms.regex"),

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

@@ -804,6 +804,11 @@ public enum PwmSetting {
             "newUser.redirectUrl", PwmSettingSyntax.STRING, PwmSettingCategory.NEWUSER_PROFILE),
             "newUser.redirectUrl", PwmSettingSyntax.STRING, PwmSettingCategory.NEWUSER_PROFILE),
     NEWUSER_PROMPT_FOR_PASSWORD(
     NEWUSER_PROMPT_FOR_PASSWORD(
             "newUser.promptForPassword", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.NEWUSER_PROFILE),
             "newUser.promptForPassword", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.NEWUSER_PROFILE),
+    NEWUSER_TOKEN_LIFETIME_EMAIL(
+            "newUser.token.lifetime", PwmSettingSyntax.DURATION, PwmSettingCategory.NEWUSER_PROFILE),
+    NEWUSER_TOKEN_LIFETIME_SMS(
+            "newUser.token.lifetime.sms", PwmSettingSyntax.DURATION, PwmSettingCategory.NEWUSER_PROFILE),
+
 
 
     // guest settings
     // guest settings
     GUEST_ENABLE(
     GUEST_ENABLE(
@@ -868,6 +873,11 @@ public enum PwmSetting {
             "updateAttributes.email.verification", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.UPDATE_PROFILE),
             "updateAttributes.email.verification", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.UPDATE_PROFILE),
     UPDATE_PROFILE_SMS_VERIFICATION(
     UPDATE_PROFILE_SMS_VERIFICATION(
             "updateAttributes.sms.verification", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.UPDATE_PROFILE),
             "updateAttributes.sms.verification", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.UPDATE_PROFILE),
+    UPDATE_PROFILE_TOKEN_LIFETIME_EMAIL(
+            "updateAttributes.token.lifetime", PwmSettingSyntax.DURATION, PwmSettingCategory.UPDATE_PROFILE),
+    UPDATE_PROFILE_TOKEN_LIFETIME_SMS(
+            "updateAttributes.token.lifetime.sms", PwmSettingSyntax.DURATION, PwmSettingCategory.UPDATE_PROFILE),
+
 
 
     UPDATE_PROFILE_CUSTOMLINKS(
     UPDATE_PROFILE_CUSTOMLINKS(
             "updateAttributes.customLinks", PwmSettingSyntax.CUSTOMLINKS, PwmSettingCategory.UPDATE_PROFILE),
             "updateAttributes.customLinks", PwmSettingSyntax.CUSTOMLINKS, PwmSettingCategory.UPDATE_PROFILE),

+ 18 - 0
src/main/java/password/pwm/config/profile/NewUserProfile.java

@@ -43,6 +43,7 @@ import java.time.Instant;
 import java.util.HashMap;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 
 public class NewUserProfile extends AbstractProfile {
 public class NewUserProfile extends AbstractProfile {
 
 
@@ -126,4 +127,21 @@ public class NewUserProfile extends AbstractProfile {
         return thePolicy;
         return thePolicy;
     }
     }
 
 
+    public TimeDuration getTokenDurationEmail() {
+        final long newUserDuration = readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_EMAIL);
+        if (newUserDuration < 1) {
+            final long defaultDuration = readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
+            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
+        }
+        return new TimeDuration(newUserDuration, TimeUnit.SECONDS);
+    }
+
+    public TimeDuration getTokenDurationSMS() {
+        final long newUserDuration = readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_SMS);
+        if (newUserDuration < 1) {
+            final long defaultDuration = readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
+            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
+        }
+        return new TimeDuration(newUserDuration, TimeUnit.SECONDS);
+    }
 }
 }

+ 20 - 0
src/main/java/password/pwm/config/profile/UpdateAttributesProfile.java

@@ -25,9 +25,11 @@ package password.pwm.config.profile;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.StoredValue;
 import password.pwm.config.StoredValue;
 import password.pwm.config.stored.StoredConfiguration;
 import password.pwm.config.stored.StoredConfiguration;
+import password.pwm.util.java.TimeDuration;
 
 
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 
 
 public class UpdateAttributesProfile extends AbstractProfile implements Profile {
 public class UpdateAttributesProfile extends AbstractProfile implements Profile {
 
 
@@ -53,4 +55,22 @@ public class UpdateAttributesProfile extends AbstractProfile implements Profile
     public ProfileType profileType() {
     public ProfileType profileType() {
         return PROFILE_TYPE;
         return PROFILE_TYPE;
     }
     }
+
+    public TimeDuration getTokenDurationEmail() {
+        final long duration = readSettingAsLong(PwmSetting.UPDATE_PROFILE_TOKEN_LIFETIME_EMAIL);
+        if (duration < 1) {
+            final long defaultDuration = readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
+            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
+        }
+        return new TimeDuration(duration, TimeUnit.SECONDS);
+    }
+
+    public TimeDuration getTokenDurationSMS() {
+        final long duration = readSettingAsLong(PwmSetting.UPDATE_PROFILE_TOKEN_LIFETIME_SMS);
+        if (duration < 1) {
+            final long defaultDuration = readSettingAsLong(PwmSetting.TOKEN_LIFETIME);
+            return new TimeDuration(defaultDuration, TimeUnit.SECONDS);
+        }
+        return new TimeDuration(duration, TimeUnit.SECONDS);
+    }
 }
 }

+ 9 - 1
src/main/java/password/pwm/http/servlet/ActivateUserServlet.java

@@ -72,6 +72,7 @@ import password.pwm.util.CaptchaUtility;
 import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.PostChangePasswordAction;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.form.FormUtility;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.ActionExecutor;
@@ -91,6 +92,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 
 /**
 /**
  * User interaction servlet for creating new users (self registration)
  * User interaction servlet for creating new users (self registration)
@@ -665,7 +667,13 @@ public class ActivateUserServlet extends AbstractPwmServlet {
         final String tokenKey;
         final String tokenKey;
         final TokenPayload tokenPayload;
         final TokenPayload tokenPayload;
         try {
         try {
-            tokenPayload = pwmApplication.getTokenService().createTokenPayload(TokenType.ACTIVATION, tokenMapData, userIdentity, destinationValues);
+            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());
             tokenKey = pwmApplication.getTokenService().generateNewToken(tokenPayload, pwmRequest.getSessionLabel());
         } catch (PwmOperationalException e) {
         } catch (PwmOperationalException e) {
             throw new PwmUnrecoverableException(e.getErrorInformation());
             throw new PwmUnrecoverableException(e.getErrorInformation());

+ 10 - 4
src/main/java/password/pwm/http/servlet/UpdateProfileServlet.java

@@ -30,14 +30,13 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.bean.TokenVerificationProgress;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
-import password.pwm.config.value.data.ActionConfiguration;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
-import password.pwm.config.value.data.FormConfiguration;
-import password.pwm.util.form.FormUtility;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.option.TokenStorageMethod;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.LdapProfile;
 import password.pwm.config.profile.UpdateAttributesProfile;
 import password.pwm.config.profile.UpdateAttributesProfile;
+import password.pwm.config.value.data.ActionConfiguration;
+import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
@@ -61,6 +60,8 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenType;
+import password.pwm.util.form.FormUtility;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.ActionExecutor;
 import password.pwm.util.operations.ActionExecutor;
@@ -674,6 +675,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
             }
             }
         }
         }
 
 
+        LOGGER.trace(pwmRequest, "determined required verification phases: " + StringUtil.collectionToString(returnObj,","));
+
         return returnObj;
         return returnObj;
     }
     }
 
 
@@ -693,6 +696,7 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
             }));
             }));
         }
         }
 
 
+        final UpdateAttributesProfile profile = getProfile(pwmRequest);
         final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine(pwmApplication);
         final MacroMachine macroMachine = pwmRequest.getPwmSession().getSessionManager().getMacroMachine(pwmApplication);
         final Configuration config = pwmApplication.getConfig();
         final Configuration config = pwmApplication.getConfig();
         final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile(pwmRequest.getConfig());
         final LdapProfile ldapProfile = pwmRequest.getUserInfoIfLoggedIn().getLdapProfile(pwmRequest.getConfig());
@@ -705,6 +709,7 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
                 try {
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                             TokenType.UPDATE_SMS,
                             TokenType.UPDATE_SMS,
+                            profile.getTokenDurationSMS(),
                             Collections.emptyMap(),
                             Collections.emptyMap(),
                             pwmRequest.getUserInfoIfLoggedIn(),
                             pwmRequest.getUserInfoIfLoggedIn(),
                             Collections.singleton(toNum)
                             Collections.singleton(toNum)
@@ -742,7 +747,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet {
                 try {
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                             TokenType.UPDATE_EMAIL,
                             TokenType.UPDATE_EMAIL,
-                            Collections.<String,String>emptyMap(),
+                            profile.getTokenDurationEmail(),
+                            Collections.emptyMap(),
                             pwmRequest.getUserInfoIfLoggedIn(),
                             pwmRequest.getUserInfoIfLoggedIn(),
                             Collections.singleton(toAddress)
                             Collections.singleton(toAddress)
                     );
                     );

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

@@ -35,12 +35,12 @@ import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.TokenDestinationItem;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.Configuration;
-import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.IdentityVerificationMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.RecoveryAction;
 import password.pwm.config.option.RecoveryAction;
 import password.pwm.config.profile.ForgottenPasswordProfile;
 import password.pwm.config.profile.ForgottenPasswordProfile;
+import password.pwm.config.value.data.FormConfiguration;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
@@ -56,6 +56,7 @@ import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenType;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.macro.MacroMachine;
 import password.pwm.util.operations.PasswordUtility;
 import password.pwm.util.operations.PasswordUtility;
@@ -71,6 +72,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map;
 import java.util.Set;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 
 class ForgottenPasswordUtil {
 class ForgottenPasswordUtil {
     private static final PwmLogger LOGGER = PwmLogger.forClass(ForgottenPasswordUtil.class);
     private static final PwmLogger LOGGER = PwmLogger.forClass(ForgottenPasswordUtil.class);
@@ -409,7 +411,13 @@ class ForgottenPasswordUtil {
         final String tokenKey;
         final String tokenKey;
         final TokenPayload tokenPayload;
         final TokenPayload tokenPayload;
         try {
         try {
-            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(TokenType.FORGOTTEN_PW, tokenMapData, userIdentity, destinationValues);
+            tokenPayload = pwmRequest.getPwmApplication().getTokenService().createTokenPayload(
+                    TokenType.FORGOTTEN_PW,
+                    new TimeDuration(config.readSettingAsLong(PwmSetting.TOKEN_LIFETIME), TimeUnit.SECONDS),
+                    tokenMapData,
+                    userIdentity,
+                    destinationValues
+            );
             tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken(tokenPayload, pwmRequest.getSessionLabel());
             tokenKey = pwmRequest.getPwmApplication().getTokenService().generateNewToken(tokenPayload, pwmRequest.getSessionLabel());
         } catch (PwmOperationalException e) {
         } catch (PwmOperationalException e) {
             throw new PwmUnrecoverableException(e.getErrorInformation());
             throw new PwmUnrecoverableException(e.getErrorInformation());

+ 3 - 0
src/main/java/password/pwm/http/servlet/newuser/NewUserUtils.java

@@ -457,6 +457,7 @@ class NewUserUtils {
             }));
             }));
         }
         }
 
 
+        final NewUserProfile newUserProfile = NewUserServlet.getNewUserProfile(pwmRequest);
         final Configuration config = pwmApplication.getConfig();
         final Configuration config = pwmApplication.getConfig();
         final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload(pwmRequest, newUserBean);
         final Map<String, String> tokenPayloadMap = NewUserFormUtils.toTokenPayload(pwmRequest, newUserBean);
         final MacroMachine macroMachine = createMacroMachineForNewUser(pwmApplication, pwmRequest.getSessionLabel(), newUserBean.getNewUserForm());
         final MacroMachine macroMachine = createMacroMachineForNewUser(pwmApplication, pwmRequest.getSessionLabel(), newUserBean.getNewUserForm());
@@ -478,6 +479,7 @@ class NewUserUtils {
                 try {
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                             password.pwm.svc.token.TokenType.NEWUSER_SMS,
                             password.pwm.svc.token.TokenType.NEWUSER_SMS,
+                            newUserProfile.getTokenDurationSMS(),
                             tokenPayloadMap,
                             tokenPayloadMap,
                             null,
                             null,
                             Collections.singleton(outputDestTokenData.getSms())
                             Collections.singleton(outputDestTokenData.getSms())
@@ -523,6 +525,7 @@ class NewUserUtils {
                 try {
                 try {
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                     final TokenPayload tokenPayload = pwmApplication.getTokenService().createTokenPayload(
                             password.pwm.svc.token.TokenType.NEWUSER_EMAIL,
                             password.pwm.svc.token.TokenType.NEWUSER_EMAIL,
+                            newUserProfile.getTokenDurationEmail(),
                             tokenPayloadMap,
                             tokenPayloadMap,
                             null,
                             null,
                             Collections.singleton(outputDestTokenData.getEmail())
                             Collections.singleton(outputDestTokenData.getEmail())

+ 8 - 13
src/main/java/password/pwm/svc/token/DataStoreTokenMachine.java

@@ -2,8 +2,6 @@ package password.pwm.svc.token;
 
 
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplication;
 import password.pwm.bean.SessionLabel;
 import password.pwm.bean.SessionLabel;
-import password.pwm.config.Configuration;
-import password.pwm.config.PwmSetting;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.error.PwmUnrecoverableException;
@@ -15,12 +13,10 @@ import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.logging.PwmLogger;
 
 
 import java.time.Instant;
 import java.time.Instant;
-import java.util.concurrent.TimeUnit;
 
 
 public class DataStoreTokenMachine implements TokenMachine {
 public class DataStoreTokenMachine implements TokenMachine {
     private static final PwmLogger LOGGER = PwmLogger.forClass(DataStoreTokenMachine.class);
     private static final PwmLogger LOGGER = PwmLogger.forClass(DataStoreTokenMachine.class);
     private final TokenService tokenService;
     private final TokenService tokenService;
-    private final TimeDuration maxTokenPurgeAge;
 
 
     private final DataStore dataStore;
     private final DataStore dataStore;
 
 
@@ -34,11 +30,6 @@ public class DataStoreTokenMachine implements TokenMachine {
         this.pwmApplication = pwmApplication;
         this.pwmApplication = pwmApplication;
         this.tokenService = tokenService;
         this.tokenService = tokenService;
         this.dataStore = dataStore;
         this.dataStore = dataStore;
-
-        final Configuration configuration = pwmApplication.getConfig();
-
-        final long maxTokenAgeMS = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME) * 1000;
-        this.maxTokenPurgeAge = new TimeDuration(maxTokenAgeMS, TimeUnit.MILLISECONDS);
     }
     }
 
 
     @Override
     @Override
@@ -81,13 +72,16 @@ public class DataStoreTokenMachine implements TokenMachine {
         if (theToken == null) {
         if (theToken == null) {
             return false;
             return false;
         }
         }
-        final Instant issueDate = theToken.getDate();
+        final Instant issueDate = theToken.getIssueTime();
         if (issueDate == null) {
         if (issueDate == null) {
             LOGGER.error("retrieved token has no issueDate, marking as purgable: " + JsonUtil.serialize(theToken));
             LOGGER.error("retrieved token has no issueDate, marking as purgable: " + JsonUtil.serialize(theToken));
             return true;
             return true;
         }
         }
-        final TimeDuration duration = TimeDuration.fromCurrent(issueDate);
-        return duration.isLongerThan(maxTokenPurgeAge);
+        if (theToken.getExpiration() == null) {
+            LOGGER.error("retrieved token has no expiration, marking as purgable: " + JsonUtil.serialize(theToken));
+            return true;
+        }
+        return theToken.getExpiration().isBefore(Instant.now());
     }
     }
 
 
     public String generateToken(
     public String generateToken(
@@ -116,7 +110,7 @@ public class DataStoreTokenMachine implements TokenMachine {
             }
             }
 
 
             if (testIfTokenNeedsPurging(tokenPayload)) {
             if (testIfTokenNeedsPurging(tokenPayload)) {
-                LOGGER.trace("stored token key '" + storedHash + "', has an outdated issue date and will be purged");
+                LOGGER.trace("stored token key '" + storedHash + "', has an outdated issue/expire date and will be purged");
                 dataStore.remove(storedHash);
                 dataStore.remove(storedHash);
             } else {
             } else {
                 return tokenPayload;
                 return tokenPayload;
@@ -147,4 +141,5 @@ public class DataStoreTokenMachine implements TokenMachine {
     public boolean supportsName() {
     public boolean supportsName() {
         return true;
         return true;
     }
     }
+
 }
 }

+ 23 - 4
src/main/java/password/pwm/svc/token/TokenPayload.java

@@ -37,17 +37,35 @@ import java.util.Set;
 
 
 @Getter
 @Getter
 public class TokenPayload implements Serializable {
 public class TokenPayload implements Serializable {
-    private final Instant date;
+    @SerializedName("t")
+    private final Instant issueTime;
+
+    @SerializedName("e")
+    private final Instant expiration;
+
+    @SerializedName("n")
     private final String name;
     private final String name;
+
     private final Map<String,String> data;
     private final Map<String,String> data;
 
 
     @SerializedName("user")
     @SerializedName("user")
     private final UserIdentity userIdentity;
     private final UserIdentity userIdentity;
+
     private final Set<String> dest;
     private final Set<String> dest;
+
+    @SerializedName("g")
     private final String guid;
     private final String guid;
 
 
-    TokenPayload(final String name, final Map<String, String> data, final UserIdentity user, final Set<String> dest, final String guid) {
-        this.date = Instant.now();
+    TokenPayload(
+            final String name,
+            final Instant expiration,
+            final Map<String, String> data,
+            final UserIdentity user,
+            final Set<String> dest,
+            final String guid
+    ) {
+        this.issueTime = Instant.now();
+        this.expiration = expiration;
         this.data = data == null ? Collections.emptyMap() : Collections.unmodifiableMap(data);
         this.data = data == null ? Collections.emptyMap() : Collections.unmodifiableMap(data);
         this.name = name;
         this.name = name;
         this.userIdentity = user;
         this.userIdentity = user;
@@ -58,7 +76,8 @@ public class TokenPayload implements Serializable {
 
 
     public String toDebugString() {
     public String toDebugString() {
         final Map<String,String> debugMap = new HashMap<>();
         final Map<String,String> debugMap = new HashMap<>();
-        debugMap.put("date", JavaHelper.toIsoDate(date));
+        debugMap.put("issueTime", JavaHelper.toIsoDate(issueTime));
+        debugMap.put("expiration", JavaHelper.toIsoDate(expiration));
         debugMap.put("name", getName());
         debugMap.put("name", getName());
         if (getUserIdentity() != null) {
         if (getUserIdentity() != null) {
             debugMap.put("user", getUserIdentity().toDisplayString());
             debugMap.put("user", getUserIdentity().toDisplayString());

+ 38 - 30
src/main/java/password/pwm/svc/token/TokenService.java

@@ -80,6 +80,7 @@ import java.util.TimerTask;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 
 
 /**
 /**
  * This PWM service is responsible for reading/writing tokens used for forgotten password,
  * This PWM service is responsible for reading/writing tokens used for forgotten password,
@@ -92,17 +93,13 @@ public class TokenService implements PwmService {
 
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(TokenService.class);
     private static final PwmLogger LOGGER = PwmLogger.forClass(TokenService.class);
 
 
-    private static final TimeDuration MAX_CLEANER_INTERVAL_MS = new TimeDuration(1, TimeUnit.DAYS);
-    private static final TimeDuration MIN_CLEANER_INTERVAL_MS = new TimeDuration(5, TimeUnit.MINUTES);
-
     private ScheduledExecutorService executorService;
     private ScheduledExecutorService executorService;
 
 
     private PwmApplication pwmApplication;
     private PwmApplication pwmApplication;
     private Configuration configuration;
     private Configuration configuration;
     private TokenStorageMethod storageMethod;
     private TokenStorageMethod storageMethod;
-    private long maxTokenAgeMS;
     private TokenMachine tokenMachine;
     private TokenMachine tokenMachine;
-    private long counter;
+    private AtomicLong counter = new AtomicLong();
 
 
     private ServiceInfoBean serviceInfo = new ServiceInfoBean(Collections.emptyList());
     private ServiceInfoBean serviceInfo = new ServiceInfoBean(Collections.emptyList());
     private STATUS status = STATUS.NEW;
     private STATUS status = STATUS.NEW;
@@ -118,11 +115,18 @@ public class TokenService implements PwmService {
 
 
     public synchronized TokenPayload createTokenPayload(
     public synchronized TokenPayload createTokenPayload(
             final TokenType name,
             final TokenType name,
+            final TimeDuration lifetime,
             final Map<String, String> data,
             final Map<String, String> data,
             final UserIdentity userIdentity,
             final UserIdentity userIdentity,
             final Set<String> dest
             final Set<String> dest
     ) {
     ) {
-        final long count = counter++;
+        final long count = counter.getAndUpdate(operand -> {
+            operand++;
+            if (operand <= 0) {
+                operand = 0;
+            }
+            return operand;
+        });
         final StringBuilder guid = new StringBuilder();
         final StringBuilder guid = new StringBuilder();
         try {
         try {
             final SecureService secureService = pwmApplication.getSecureService();
             final SecureService secureService = pwmApplication.getSecureService();
@@ -132,7 +136,8 @@ public class TokenService implements PwmService {
         } catch (Exception e) {
         } catch (Exception e) {
             LOGGER.error("error making payload guid: " + e.getMessage(),e);
             LOGGER.error("error making payload guid: " + e.getMessage(),e);
         }
         }
-        return new TokenPayload(name.name(), data, userIdentity, dest, guid.toString());
+        final Instant expiration = lifetime.incrementFromInstant(Instant.now());
+        return new TokenPayload(name.name(), expiration, data, userIdentity, dest, guid.toString());
     }
     }
 
 
     public void init(final PwmApplication pwmApplication)
     public void init(final PwmApplication pwmApplication)
@@ -151,14 +156,6 @@ public class TokenService implements PwmService {
             status = STATUS.CLOSED;
             status = STATUS.CLOSED;
             throw new PwmOperationalException(errorInformation);
             throw new PwmOperationalException(errorInformation);
         }
         }
-        try {
-            maxTokenAgeMS = configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME) * 1000;
-        } catch (Exception e) {
-            final String errorMsg = "unable to parse max token age value: " + e.getMessage();
-            errorInformation = new ErrorInformation(PwmError.ERROR_INVALID_CONFIG,errorMsg);
-            status = STATUS.CLOSED;
-            throw new PwmOperationalException(errorInformation);
-        }
 
 
         try {
         try {
             DataStorageMethod usedStorageMethod = null;
             DataStorageMethod usedStorageMethod = null;
@@ -208,22 +205,19 @@ public class TokenService implements PwmService {
 
 
         final TimerTask cleanerTask = new CleanerTask();
         final TimerTask cleanerTask = new CleanerTask();
 
 
-        final long cleanerFrequency = Math.max(
-                MIN_CLEANER_INTERVAL_MS.getTotalMilliseconds(),
-                Math.min(
-                        maxTokenAgeMS / 2,
-                        MAX_CLEANER_INTERVAL_MS.getTotalMilliseconds()
-                ));
-
-
-        executorService.scheduleAtFixedRate(cleanerTask, 10 * 1000, cleanerFrequency, TimeUnit.MILLISECONDS);
-        LOGGER.trace("token cleanup will occur every " + TimeDuration.asCompactString(cleanerFrequency));
+        {
+            final int cleanerFrequencySeconds = Integer.parseInt(configuration.readAppProperty(AppProperty.TOKEN_CLEANER_INTERVAL_SECONDS));
+            final TimeDuration cleanerFrequency = new TimeDuration(cleanerFrequencySeconds, TimeUnit.SECONDS);
+            executorService.scheduleAtFixedRate(cleanerTask, 10 , cleanerFrequencySeconds, TimeUnit.SECONDS);
+            LOGGER.trace("token cleanup will occur every " + cleanerFrequency.asCompactString());
+        }
 
 
         final String counterString = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.TOKEN_COUNTER, String.class);
         final String counterString = pwmApplication.readAppAttribute(PwmApplication.AppAttribute.TOKEN_COUNTER, String.class);
         try {
         try {
-            counter = Long.parseLong(counterString);
+            final long storedCounter = Long.parseLong(counterString);
+            counter = new AtomicLong(storedCounter);
         } catch (Exception e) {
         } catch (Exception e) {
-            /* noop */
+            LOGGER.trace("can not parse stored last counter position, setting issue counter at 0");
         }
         }
 
 
         verifyPwModifyTime = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.TOKEN_VERIFY_PW_MODIFY_TIME));
         verifyPwModifyTime = Boolean.parseBoolean(configuration.readAppProperty(AppProperty.TOKEN_VERIFY_PW_MODIFY_TIME));
@@ -367,13 +361,16 @@ public class TokenService implements PwmService {
         if (theToken == null) {
         if (theToken == null) {
             return false;
             return false;
         }
         }
-        final Instant issueDate = theToken.getDate();
+        final Instant issueDate = theToken.getIssueTime();
         if (issueDate == null) {
         if (issueDate == null) {
             LOGGER.error(sessionLabel, "retrieved token has no issueDate, marking as expired: " + theToken.toDebugString());
             LOGGER.error(sessionLabel, "retrieved token has no issueDate, marking as expired: " + theToken.toDebugString());
             return true;
             return true;
         }
         }
-        final TimeDuration duration = TimeDuration.fromCurrent(issueDate);
-        return duration.isLongerThan(maxTokenAgeMS);
+        if (theToken.getExpiration() == null) {
+            LOGGER.error(sessionLabel, "retrieved token has no expiration timestamp, marking as expired: " + theToken.toDebugString());
+            return true;
+        }
+        return theToken.getExpiration().isBefore(Instant.now());
     }
     }
 
 
     private static String makeRandomCode(final Configuration config) {
     private static String makeRandomCode(final Configuration config) {
@@ -772,4 +769,15 @@ public class TokenService implements PwmService {
             return displayDestAddress.toString();
             return displayDestAddress.toString();
         }
         }
     }
     }
+
+    static TimeDuration maxTokenAge(final Configuration configuration) {
+        long maxValue = 0;
+        maxValue = Math.max(maxValue, configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME));
+        maxValue = Math.max(maxValue, configuration.readSettingAsLong(PwmSetting.TOKEN_LIFETIME));
+        for (NewUserProfile newUserProfile : configuration.getNewUserProfiles().values()) {
+            maxValue = Math.max(maxValue, newUserProfile.readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_EMAIL));
+            maxValue = Math.max(maxValue, newUserProfile.readSettingAsLong(PwmSetting.NEWUSER_TOKEN_LIFETIME_SMS));
+        }
+        return new TimeDuration(maxValue, TimeUnit.SECONDS);
+    }
 }
 }

+ 2 - 1
src/main/java/password/pwm/util/cli/commands/TokenInfoCommand.java

@@ -57,7 +57,8 @@ public class TokenInfoCommand extends AbstractCliCommand {
         } else {
         } else {
             out("  name: " + tokenPayload.getName());
             out("  name: " + tokenPayload.getName());
             out("  user: " + tokenPayload.getUserIdentity());
             out("  user: " + tokenPayload.getUserIdentity());
-            out("issued: " + JavaHelper.toIsoDate(tokenPayload.getDate()));
+            out("issued: " + JavaHelper.toIsoDate(tokenPayload.getIssueTime()));
+            out("expire: " + JavaHelper.toIsoDate(tokenPayload.getExpiration()));
             for (final String key : tokenPayload.getData().keySet()) {
             for (final String key : tokenPayload.getData().keySet()) {
                 final String value = tokenPayload.getData().get(key);
                 final String value = tokenPayload.getData().get(key);
                 out("  payload key: " + key);
                 out("  payload key: " + key);

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

@@ -268,6 +268,7 @@ telemetry.senderImplementation=password.pwm.svc.telemetry.HttpTelemetrySender
 telemetry.senderSettings={"url":"https://www.pwm-project.org/pwm-data-service/telemetry"}
 telemetry.senderSettings={"url":"https://www.pwm-project.org/pwm-data-service/telemetry"}
 telemetry.sendFrequencySeconds=259203
 telemetry.sendFrequencySeconds=259203
 telemetry.minimumAuthentications=10
 telemetry.minimumAuthentications=10
+token.cleaner.intervalSeconds=3605
 token.mask.email.regex=(?<=.).(?=[^@]*?@)|(?:(?<=@.)|(?!^)\\G(?=[^@]*$)).(?=.*\\.)
 token.mask.email.regex=(?<=.).(?=[^@]*?@)|(?:(?<=@.)|(?!^)\\G(?=[^@]*$)).(?=.*\\.)
 token.mask.email.replace=*
 token.mask.email.replace=*
 token.mask.sms.regex=\\d(?!.{0,3}$)
 token.mask.sms.regex=\\d(?!.{0,3}$)

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

@@ -2657,6 +2657,16 @@
             <value>true</value>
             <value>true</value>
         </default>
         </default>
     </setting>
     </setting>
+    <setting hidden="false" key="newUser.token.lifetime" level="1" required="true">
+        <default>
+            <value>0</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="newUser.token.lifetime.sms" level="1" required="true">
+        <default>
+            <value>0</value>
+        </default>
+    </setting>
     <setting hidden="false" key="guest.enable" level="1" required="true">
     <setting hidden="false" key="guest.enable" level="1" required="true">
         <default>
         <default>
             <value>false</value>
             <value>false</value>
@@ -2935,6 +2945,21 @@
             <value></value>
             <value></value>
         </default>
         </default>
     </setting>
     </setting>
+    <setting hidden="false" key="updateAttributes.token.lifetime" level="1" required="true">
+        <default>
+            <value>0</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="updateAttributes.token.lifetime.sms" level="1" required="true">
+        <default>
+            <value>0</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="updateAttributes.enable" level="1">
+        <default>
+            <value>false</value>
+        </default>
+    </setting>
     <setting hidden="false" key="shortcut.enable" level="1">
     <setting hidden="false" key="shortcut.enable" level="1">
         <default>
         <default>
             <value>false</value>
             <value>false</value>

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

@@ -478,6 +478,8 @@ Setting_Description_newUser.profile.visible=Show this New User profile to users
 Setting_Description_newUser.promptForPassword=Prompt user for password during user registration.  If not enabled, a random password will be assigned to the user.  In most cases you will want this enabled.
 Setting_Description_newUser.promptForPassword=Prompt user for password during user registration.  If not enabled, a random password will be assigned to the user.  In most cases you will want this enabled.
 Setting_Description_newUser.redirectUrl=URL to redirect user to after new user registration process is completed.
 Setting_Description_newUser.redirectUrl=URL to redirect user to after new user registration process is completed.
 Setting_Description_newUser.sms.verification=Enable this option to have @PwmAppName@ send an SMS to the new user's mobile phone number before it creates the account. The NewUser must verify receipt of the SMS before @PwmAppName@ creates the account.
 Setting_Description_newUser.sms.verification=Enable this option to have @PwmAppName@ send an SMS to the new user's mobile phone number before it creates the account. The NewUser must verify receipt of the SMS before @PwmAppName@ creates the account.
+Setting_Description_newUser.token.lifetime=Specify the lifetime a new user email token is valid (in seconds). The default is 0.   When set to 0, the effective value is inherited from the setting <code>@PwmSettingReference\:token.lifetime@</code>
+Setting_Description_newUser.token.lifetime.sms=Specify the lifetime a new user SMS token is valid (in seconds). The default is 0.  When set to 0, the effective value is inherited from the setting <code>@PwmSettingReference\:token.lifetime@</code>
 Setting_Description_newUser.username.definition=<p>Specify the entry ID of the newly created LDAP entry. In some directories this is often used as the "user name", though many directories separate the concepts and values of entry ID and user name.</p><br/><br/><p>Values can (and usually do) include macros.  In case the first value already exists in the directory, @PwmAppName@ tries each successive value until it finds a free value.  Though @PwmAppName@ has not yet created the user when it evaluates the macros, the LDAP macros use the data provided on the new user form.  Other macros might not be useful as there no data yet available on the user.</p><br/><br/><p>If blank, the user name must be present in the form, defined as the LDAP naming attribute value.</p>
 Setting_Description_newUser.username.definition=<p>Specify the entry ID of the newly created LDAP entry. In some directories this is often used as the "user name", though many directories separate the concepts and values of entry ID and user name.</p><br/><br/><p>Values can (and usually do) include macros.  In case the first value already exists in the directory, @PwmAppName@ tries each successive value until it finds a free value.  Though @PwmAppName@ has not yet created the user when it evaluates the macros, the LDAP macros use the data provided on the new user form.  Other macros might not be useful as there no data yet available on the user.</p><br/><br/><p>If blank, the user name must be present in the form, defined as the LDAP naming attribute value.</p>
 Setting_Description_newUser.writeAttributes=Specify the actions the system takes when it creates a user.  The actions will be executed just after the user is created in the LDAP directory.    You can use macros in this setting.
 Setting_Description_newUser.writeAttributes=Specify the actions the system takes when it creates a user.  The actions will be executed just after the user is created in the LDAP directory.    You can use macros in this setting.
 Setting_Description_notes.noteText=Specify any configuration notes about your system. This option allows you to keep notes about any specific configuration options you have made with the system.
 Setting_Description_notes.noteText=Specify any configuration notes about your system. This option allows you to keep notes about any specific configuration options you have made with the system.
@@ -663,7 +665,7 @@ Setting_Description_template.storage=<p>This setting changes the default values
 Setting_Description_token.characters=Specify the available characters for the email token.
 Setting_Description_token.characters=Specify the available characters for the email token.
 Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@ uses when you enable the LDAP Token Storage Method to store and search for tokens.
 Setting_Description_token.ldap.attribute=Specify the attribute that @PwmAppName@ uses when you enable the LDAP Token Storage Method to store and search for tokens.
 Setting_Description_token.length=Specify the length of the email token
 Setting_Description_token.length=Specify the length of the email token
-Setting_Description_token.lifetime=Specify how long a lifetime token is valid (in seconds). The default is one hour.
+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.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_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.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 must verify receipt of the email before @PwmAppName@ updates the account.
 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 must verify receipt of the email before @PwmAppName@ updates the account.
@@ -674,6 +676,8 @@ Setting_Description_updateAttributes.profile.list=Update Attributes Profiles
 Setting_Description_updateAttributes.queryMatch=Add an LDAP query that only allows users who match this query to update their profiles.
 Setting_Description_updateAttributes.queryMatch=Add an LDAP query that only allows users who match this query to update their profiles.
 Setting_Description_updateAttributes.showConfirmation=Enable this option to show the update attributes to the users after they configure them.  This gives your users an opportunity to read and review their attributes before submitting, however, it shows the responses on the screen and makes them visible to anyone else watching the users' screens.
 Setting_Description_updateAttributes.showConfirmation=Enable this option to show the update attributes to the users after they configure them.  This gives your users an opportunity to read and review their attributes before submitting, however, it shows the responses on the screen and makes them visible to anyone else watching the users' screens.
 Setting_Description_updateAttributes.sms.verification=Enable this option to send an SMS to the users' mobile phone numbers before updating the account.  The user must verify receipt of the SMS before @PwmAppName@ updates the account.
 Setting_Description_updateAttributes.sms.verification=Enable this option to send an SMS to the users' mobile phone numbers before updating the account.  The user must verify receipt of the SMS before @PwmAppName@ updates the account.
+Setting_Description_updateAttributes.token.lifetime=Specify the lifetime a update profile email token is valid (in seconds). The default is 0.   When set to 0, the effective value is inherited from the setting <code>@PwmSettingReference\:token.lifetime@</code>
+Setting_Description_updateAttributes.token.lifetime.sms=Specify the lifetime a new user update profile SMS token is valid (in seconds). The default is 0.  When set to 0, the effective value is inherited from the setting <code>@PwmSettingReference\:token.lifetime@</code>
 Setting_Description_updateAttributes.customLinks=Create custom links for users to navigate to while updating their profile data.
 Setting_Description_updateAttributes.customLinks=Create custom links for users to navigate to while updating their profile data.
 Setting_Description_updateAttributes.writeAttributes=Add actions to execute after @PwmAppName@ populates a user's attributes.
 Setting_Description_updateAttributes.writeAttributes=Add actions to execute after @PwmAppName@ populates a user's attributes.
 Setting_Description_urlshortener.classname=Specify the URL Shortening Service class name. The Java full class name that implements a short URL service. You must include the corresponding JAR or ZIP file in the classpath, typically in the <i>WEB-INF/lib</i> directory or the application server's lib directory.
 Setting_Description_urlshortener.classname=Specify the URL Shortening Service class name. The Java full class name that implements a short URL service. You must include the corresponding JAR or ZIP file in the classpath, typically in the <i>WEB-INF/lib</i> directory or the application server's lib directory.
@@ -957,6 +961,8 @@ Setting_Label_newUser.profile.visible=Profile Visible on Menu
 Setting_Label_newUser.promptForPassword=Prompt User for Password
 Setting_Label_newUser.promptForPassword=Prompt User for Password
 Setting_Label_newUser.redirectUrl=After Registration Redirect URL
 Setting_Label_newUser.redirectUrl=After Registration Redirect URL
 Setting_Label_newUser.sms.verification=Enable New User SMS Verification
 Setting_Label_newUser.sms.verification=Enable New User SMS Verification
+Setting_Label_newUser.token.lifetime=New User Email Token Maximum Lifetime
+Setting_Label_newUser.token.lifetime.sms=New User SMS Token Maximum Lifetime
 Setting_Label_newUser.customLinks=Enable New User Custom links
 Setting_Label_newUser.customLinks=Enable New User Custom links
 Setting_Label_newUser.username.definition=LDAP Entry ID Definition
 Setting_Label_newUser.username.definition=LDAP Entry ID Definition
 Setting_Label_newUser.writeAttributes=New User Actions
 Setting_Label_newUser.writeAttributes=New User Actions
@@ -1155,6 +1161,8 @@ Setting_Label_updateAttributes.profile.list=List of Update Attribute profiles.
 Setting_Label_updateAttributes.queryMatch=Update Profile Match
 Setting_Label_updateAttributes.queryMatch=Update Profile Match
 Setting_Label_updateAttributes.showConfirmation=Show Update Profile Confirmation
 Setting_Label_updateAttributes.showConfirmation=Show Update Profile Confirmation
 Setting_Label_updateAttributes.sms.verification=Enable SMS Verification
 Setting_Label_updateAttributes.sms.verification=Enable SMS Verification
+Setting_Label_updateAttributes.token.lifetime=Update Profile Email Token Maximum Lifetime
+Setting_Label_updateAttributes.token.lifetime.sms=Update Profile SMS Token Maximum Lifetime
 Setting_Label_updateAttributes.customLinks=Custom Links 
 Setting_Label_updateAttributes.customLinks=Custom Links 
 Setting_Label_updateAttributes.writeAttributes=Update Profile Actions
 Setting_Label_updateAttributes.writeAttributes=Update Profile Actions
 Setting_Label_urlshortener.classname=Enable URL Shortening Service Class
 Setting_Label_urlshortener.classname=Enable URL Shortening Service Class

+ 9 - 1
src/main/webapp/WEB-INF/jsp/admin-tokenlookup.jsp

@@ -105,7 +105,15 @@
                     Issue Date
                     Issue Date
                 </td>
                 </td>
                 <td>
                 <td>
-                    <span class="timestamp"><%= JavaHelper.toIsoDate(tokenPayload.getDate()) %></span>
+                    <span class="timestamp"><%= JavaHelper.toIsoDate(tokenPayload.getIssueTime()) %></span>
+                </td>
+            </tr>
+            <tr>
+                <td class="key">
+                    Expiration Date
+                </td>
+                <td>
+                    <span class="timestamp"><%= JavaHelper.toIsoDate(tokenPayload.getExpiration()) %></span>
                 </td>
                 </td>
             </tr>
             </tr>
             <tr>
             <tr>

+ 11 - 2
src/test/java/password/pwm/config/PwmSettingTest.java

@@ -76,14 +76,23 @@ public class PwmSettingTest {
     @Test
     @Test
     public void testProperties() throws PwmUnrecoverableException, PwmOperationalException {
     public void testProperties() throws PwmUnrecoverableException, PwmOperationalException {
         for (PwmSetting pwmSetting : PwmSetting.values()) {
         for (PwmSetting pwmSetting : PwmSetting.values()) {
-            pwmSetting.getProperties();
+            try {
+                pwmSetting.getProperties();
+            } catch (Throwable t) {
+                throw new IllegalStateException("unable to read properties for setting '" + pwmSetting.toString() + "', error: " + t.getMessage(),t);
+            }
         }
         }
     }
     }
 
 
     @Test
     @Test
     public void testOptions() throws PwmUnrecoverableException, PwmOperationalException {
     public void testOptions() throws PwmUnrecoverableException, PwmOperationalException {
         for (PwmSetting pwmSetting : PwmSetting.values()) {
         for (PwmSetting pwmSetting : PwmSetting.values()) {
-            pwmSetting.getOptions();
+            try {
+                pwmSetting.getOptions();
+            } catch (Throwable t) {
+                throw new IllegalStateException("unable to read options for setting '" + pwmSetting.toString() + "', error: " + t.getMessage(),t);
+            }
+
         }
         }
     }
     }
 
 

+ 24 - 0
src/test/java/password/pwm/util/java/AtomicLoopIntIncrementerTest.java

@@ -0,0 +1,24 @@
+package password.pwm.util.java;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class AtomicLoopIntIncrementerTest {
+
+    @Test
+    public void testIncrementer() {
+        AtomicLoopIntIncrementer atomicLoopIntIncrementer = new AtomicLoopIntIncrementer(5);
+        for (int i = 0; i < 5; i++) {
+            int next = atomicLoopIntIncrementer.next();
+            Assert.assertEquals(i, next);
+        }
+
+        Assert.assertEquals(atomicLoopIntIncrementer.next(), 0);
+
+        for (int i = 0; i < 5; i++) {
+            atomicLoopIntIncrementer.next();
+        }
+
+        Assert.assertEquals(atomicLoopIntIncrementer.next(), 1);
+    }
+}