diff --git a/.env.template b/.env.template index 0d922774..bf025d8d 100644 --- a/.env.template +++ b/.env.template @@ -587,6 +587,9 @@ ## Note that the checkbox would still be present, but ignored. # DISABLE_2FA_REMEMBER=false ## +## Number of days before a remembered 2FA login expires (min: 1, max: 365). +# TWO_FACTOR_REMEMBER_DAYS=30 +## ## Authenticator Settings ## Disable authenticator time drifted codes to be valid. ## TOTP codes of the previous and next 30 seconds will be invalid diff --git a/src/auth.rs b/src/auth.rs index 88a59b4b..994e53dd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -473,13 +473,34 @@ pub fn generate_2fa_remember_claims(device_uuid: DeviceId, user_uuid: UserId) -> let time_now = Utc::now(); TwoFactorRememberClaims { nbf: time_now.timestamp(), - exp: (time_now + TimeDelta::try_days(30).unwrap()).timestamp(), + exp: two_factor_remember_expiration(time_now, CONFIG.two_factor_remember_days()), iss: JWT_2FA_REMEMBER_ISSUER.to_string(), sub: device_uuid, user_uuid, } } +fn two_factor_remember_expiration(time_now: DateTime, remember_days: i64) -> i64 { + (time_now + TimeDelta::try_days(remember_days).expect("TWO_FACTOR_REMEMBER_DAYS is validated")).timestamp() +} + +#[cfg(test)] +mod two_factor_remember_tests { + use chrono::TimeZone; + + use super::*; + + #[test] + fn expiration_uses_configured_number_of_days() { + let time_now = Utc.with_ymd_and_hms(2026, 7, 14, 12, 0, 0).unwrap(); + + assert_eq!( + two_factor_remember_expiration(time_now, 45), + Utc.with_ymd_and_hms(2026, 8, 28, 12, 0, 0).unwrap().timestamp() + ); + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct BasicJwtClaims { // Not before diff --git a/src/config.rs b/src/config.rs index 49281b6c..9ef31b61 100644 --- a/src/config.rs +++ b/src/config.rs @@ -705,6 +705,8 @@ make_config! { /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time. /// Note that the checkbox would still be present, but ignored. disable_2fa_remember: bool, true, def, false; + /// Two-Factor remember duration |> Number of days before a remembered Two-Factor login expires (min: 1, max: 365). + two_factor_remember_days: i64, true, def, 30; /// Disable authenticator time drifted codes to be valid |> Enabling this only allows the current TOTP code to be valid /// TOTP codes of the previous and next 30 seconds will be invalid. @@ -1209,6 +1211,10 @@ fn validate_config(cfg: &ConfigItems, on_update: bool) -> Result<(), Error> { err!("`INVITATION_EXPIRATION_HOURS` has a minimum duration of 1 hour") } + if !(1..=365).contains(&cfg.two_factor_remember_days) { + err!("`TWO_FACTOR_REMEMBER_DAYS` must be between 1 and 365 days") + } + // Validate schedule crontab format if !cfg.send_purge_schedule.is_empty() && cfg.send_purge_schedule.parse::().is_err() { err!("`SEND_PURGE_SCHEDULE` is not a valid cron expression")