From 0eb133155b4aeafc7eb980ec15259e96f3bc75a4 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 812b12f6..cc8aaddb 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 1a87c20d7a7f68550320c0ed69099adaa32f9a94 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 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 362cb410fe640cf91670fb420d511641e0a13e3a 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 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 00d946abb5b668c551a32b749472ef7b025f7df7 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 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 6f347f27aa4cb97f1f4f74b3b6fb6dbebedbe83a 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 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 6953611c6f88c57372344f8d0c459d9603111872 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 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 8ead7d2ddfb4b6eb8fdbf5912cc892016cf02568 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 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 68cea653c2e37ab36f8150f679c3dbf6e62034d5 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 59aba4a9..e6b4de28 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 a242376995b4342cbe459e012e013ad07ff7c649 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 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 9336e8744e1acb7135c628fa428239ac553c0a30 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 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"), };