From f7db782e1c81b134e8c6b644494b456bb52d9e00 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:12:49 +0100 Subject: [PATCH 01/10] Add option to disable refresh token renewal Add a new configuration option to disable refresh token renewal, requiring full reauthentication every 30/90 days. --- src/config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config.rs b/src/config.rs index e1a2f4c2..ee140998 100644 --- a/src/config.rs +++ b/src/config.rs @@ -706,6 +706,10 @@ make_config! { /// Note that the checkbox would still be present, but ignored. disable_2fa_remember: bool, true, def, false; + /// Disable refresh token renewal |> If true, disables sliding window for refresh token expiry. + /// This only renews the token on a full login (Password (+2FA), SSO, etc.) forcing a full reauth every 30 days (90 for the native app) + disable_refresh_token_renewal: bool, true, def, false; + /// 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. authenticator_disable_time_drift: bool, true, def, false; From 790df6826264b6c772d40edb5517769249f9d574 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:49:48 +0100 Subject: [PATCH 02/10] Add logic to make token renew optional --- src/auth.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index e10de615..960fa743 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1186,7 +1186,7 @@ impl AuthTokens { *DEFAULT_REFRESH_VALIDITY }; - let refresh_claims = RefreshJwtClaims { + let default_refresh_claims = RefreshJwtClaims { nbf: time_now.timestamp(), exp: (time_now + validity).timestamp(), iss: JWT_LOGIN_ISSUER.to_string(), @@ -1195,6 +1195,22 @@ impl AuthTokens { token: None, }; + let refresh_claims = if CONFIG.disable_refresh_token_renewal() { + match decode_refresh(&device.refresh_token) { + Ok(original_claims) => RefreshJwtClaims { + nbf: original_claims.nbf, + exp: original_claims.exp, + iss: original_claims.iss.clone(), + sub: original_claims.sub.clone(), + device_token: original_claims.device_token.clone(), + token: original_claims.token.clone(), + }, + Err(_) => default_refresh_claims, + } + } else { + default_refresh_claims + }; + Self { refresh_claims, access_claims, From 3a000cc59c4e830759c779c947a68b2703e6520d Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:19:46 +0100 Subject: [PATCH 03/10] Add Clone trait to TokenWrapper enum --- src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index 960fa743..a3b4aaf0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1129,7 +1129,7 @@ impl AuthMethod { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum TokenWrapper { Access(String), Refresh(String), From e38a5fc0bb6806061e218df0978da394bd4b8ed7 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:14:42 +0100 Subject: [PATCH 04/10] Reuse original claims struct for refresh token renewal --- src/auth.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index a3b4aaf0..eaa8d205 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1197,14 +1197,7 @@ impl AuthTokens { let refresh_claims = if CONFIG.disable_refresh_token_renewal() { match decode_refresh(&device.refresh_token) { - Ok(original_claims) => RefreshJwtClaims { - nbf: original_claims.nbf, - exp: original_claims.exp, - iss: original_claims.iss.clone(), - sub: original_claims.sub.clone(), - device_token: original_claims.device_token.clone(), - token: original_claims.token.clone(), - }, + Ok(original_claims) => original_claims, // reuse the original struct Err(_) => default_refresh_claims, } } else { From aea2b84de98167e4694ce3f297cd18a8b9703a0f Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:18:08 +0100 Subject: [PATCH 05/10] pass original refresh_claim into renewal function --- src/auth.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index eaa8d205..f750ae2f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1175,7 +1175,7 @@ impl AuthTokens { } // Create refresh_token and access_token with default validity - pub fn new(device: &Device, user: &User, sub: AuthMethod, client_id: Option) -> Self { + pub fn new(device: &Device, user: &User, sub: AuthMethod, client_id: Option, existing_refresh_claims: Option<&RefreshJwtClaims>) -> Self { let time_now = Utc::now(); let access_claims = LoginJwtClaims::default(device, user, &sub, client_id); @@ -1196,10 +1196,8 @@ impl AuthTokens { }; let refresh_claims = if CONFIG.disable_refresh_token_renewal() { - match decode_refresh(&device.refresh_token) { - Ok(original_claims) => original_claims, // reuse the original struct - Err(_) => default_refresh_claims, - } + // Use existing_refresh_claims if passed and config is enabled + existing_refresh_claims.cloned().unwrap_or(default_refresh_claims) } else { default_refresh_claims }; @@ -1241,14 +1239,14 @@ pub async fn refresh_tokens( let auth_tokens = match refresh_claims.sub { AuthMethod::Sso if CONFIG.sso_enabled() && CONFIG.sso_auth_only_not_session() => { - AuthTokens::new(&device, &user, refresh_claims.sub, client_id) + AuthTokens::new(&device, &user, refresh_claims.sub, client_id, refresh_claims) } AuthMethod::Sso if CONFIG.sso_enabled() => { sso::exchange_refresh_token(&device, &user, client_id, refresh_claims).await? } AuthMethod::Sso => err!("SSO is now disabled, Login again using email and master password"), AuthMethod::Password if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO is now required, Login again"), - AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id), + AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id, refresh_claims), _ => err!("Invalid auth method, cannot refresh token"), }; From 9b44d0474fe3087f5035405c991b91db512ecbfe Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:30:02 +0100 Subject: [PATCH 06/10] Pass correct type --- src/auth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index f750ae2f..5919cb04 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1239,14 +1239,14 @@ pub async fn refresh_tokens( let auth_tokens = match refresh_claims.sub { AuthMethod::Sso if CONFIG.sso_enabled() && CONFIG.sso_auth_only_not_session() => { - AuthTokens::new(&device, &user, refresh_claims.sub, client_id, refresh_claims) + AuthTokens::new(&device, &user, refresh_claims.sub, client_id, Some(&refresh_claims)) } AuthMethod::Sso if CONFIG.sso_enabled() => { sso::exchange_refresh_token(&device, &user, client_id, refresh_claims).await? } AuthMethod::Sso => err!("SSO is now disabled, Login again using email and master password"), AuthMethod::Password if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO is now required, Login again"), - AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id, refresh_claims), + AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id, Some(&refresh_claims)), _ => err!("Invalid auth method, cannot refresh token"), }; From 86a57fab1dc41fd06dc0bc3bb05fa3d5418d4aee Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:33:15 +0100 Subject: [PATCH 07/10] Add optional parameter existing_refresh_claims to AuthTokens --- src/sso.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sso.rs b/src/sso.rs index 90e4d677..22c555b7 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -361,7 +361,7 @@ pub fn create_auth_tokens( _create_auth_tokens(device, refresh_token, access_claims, access_token) } else { - Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id, None)) } } From 3be4384f7170994e75bea73f74504062432bd570 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:33:58 +0100 Subject: [PATCH 08/10] Add optional parameter existing_refresh_claims to AuthTokens --- src/api/identity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/identity.rs b/src/api/identity.rs index 92b6c1e4..6c9008d8 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -433,7 +433,7 @@ async fn _password_login( let twofactor_token = twofactor_auth(&mut user, &data, &mut device, ip, client_version, conn).await?; - let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id); + let auth_tokens = auth::AuthTokens::new(&device, &user, AuthMethod::Password, data.client_id, None); authenticated_response(&user, &mut device, auth_tokens, twofactor_token, &now, conn, ip).await } From 36d55aa252bf5bd197f51fc0fbaad24823ffbb83 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:37:13 +0100 Subject: [PATCH 09/10] Add Clone trait to RefreshJwtClaims struct --- src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index 5919cb04..aa3ab287 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1135,7 +1135,7 @@ pub enum TokenWrapper { Refresh(String), } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RefreshJwtClaims { // Not before pub nbf: i64, From b3fc25ecacb12ad432dc75fe8da6ecc355a5911d Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:43:59 +0100 Subject: [PATCH 10/10] Clone refresh_claims.sub --- src/auth.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index aa3ab287..20191c91 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1239,14 +1239,14 @@ pub async fn refresh_tokens( let auth_tokens = match refresh_claims.sub { AuthMethod::Sso if CONFIG.sso_enabled() && CONFIG.sso_auth_only_not_session() => { - AuthTokens::new(&device, &user, refresh_claims.sub, client_id, Some(&refresh_claims)) + AuthTokens::new(&device, &user, refresh_claims.sub.clone(), client_id, Some(&refresh_claims)) } AuthMethod::Sso if CONFIG.sso_enabled() => { sso::exchange_refresh_token(&device, &user, client_id, refresh_claims).await? } AuthMethod::Sso => err!("SSO is now disabled, Login again using email and master password"), AuthMethod::Password if CONFIG.sso_enabled() && CONFIG.sso_only() => err!("SSO is now required, Login again"), - AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub, client_id, Some(&refresh_claims)), + AuthMethod::Password => AuthTokens::new(&device, &user, refresh_claims.sub.clone(), client_id, Some(&refresh_claims)), _ => err!("Invalid auth method, cannot refresh token"), };