From 1f2c2cf63d26b4503ab9ba81a1c82e56a1a1cdca 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/12] 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 4fb103fa..b13f4a66 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 830d395c5d76d81db7084baf7f73b02979f662b9 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/12] 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 ab41898f..3ffb5d22 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 bd06258240b4ad79910b8139abd7c6fed2c013d2 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/12] 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 3ffb5d22..5c09a13b 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 cc9043005746413728a94602b853479c95f37126 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/12] 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 5c09a13b..e2096da6 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 19cce482bcb5e9d780936f3f5c46c39c93409341 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/12] 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 e2096da6..95bb8ebe 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 }; @@ -1253,14 +1251,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 b4f5581b5fe8b207682d95917ff4538be573033b 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/12] 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 95bb8ebe..daf90cf0 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1251,14 +1251,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 5a90dbc369d70b9a6da637fc58b4c43a529ff1ed 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/12] 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 ee6d707a..deb78ea0 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -373,7 +373,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 2515d46a7fcc49cbb0114ecba6195a5a0ede13e9 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/12] 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 f5f2afd6..2053f16b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -424,7 +424,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, conn, ip).await } From 99f061d5f76aac1d7651f4ea7ac1d22605a89cb6 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/12] 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 daf90cf0..2e7f510b 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 78bd39ef25c02874c791e71f311b22f0c6c49180 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/12] 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 2e7f510b..2fd65582 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1251,14 +1251,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"), }; From 0844f57ba87d65fa9d3500c1c6fa5478e2f178f6 Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:43:10 +0100 Subject: [PATCH 11/12] SSO match new AuthTokens::new signature --- src/sso.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sso.rs b/src/sso.rs index deb78ea0..94e6302f 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -345,7 +345,7 @@ pub async fn redeem( _create_auth_tokens(device, auth_user.refresh_token, access_claims, auth_user.access_token) } else { - Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id)) + Ok(AuthTokens::new(device, user, AuthMethod::Sso, client_id, None)) } } From 7b889e57a8389add1435d1dbdd85f88a67bb671e Mon Sep 17 00:00:00 2001 From: Momi-V <83947761+Momi-V@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:55:07 +0100 Subject: [PATCH 12/12] make formatting check happy --- src/auth.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 2fd65582..1b967dc2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1175,9 +1175,14 @@ impl AuthTokens { } // Create refresh_token and access_token with default validity - pub fn new(device: &Device, user: &User, sub: AuthMethod, client_id: Option, existing_refresh_claims: Option<&RefreshJwtClaims>) -> 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); let validity = if device.is_mobile() { @@ -1258,7 +1263,9 @@ pub async fn refresh_tokens( } 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.clone(), 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"), };