Fix 2FA Remember to actually be 30 days (#6929)

Currently we always regenerate the 2FA Remember token, and always send that back to the client.
This is not the correct way, and in turn causes the remember token to never expire.

While this might be convenient, it is not really safe.
This commit changes the 2FA Remember Tokens from random string to a JWT.
This JWT has a lifetime of 30 days and is validated per device & user combination.

This does mean that once this commit is merged, and users are using this version, all their remember tokens will be invalidated.
From my point of view this isn't a bad thing, since those tokens should have expired already.

Only users who recently checked the remember checkbox within 30 days have to login again, but that is a minor inconvenience I think.

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw
2026-03-23 23:12:07 +01:00
committed by GitHub
parent c0a78dd55a
commit 235cf88231
3 changed files with 52 additions and 10 deletions
+15 -6
View File
@@ -757,7 +757,6 @@ async fn twofactor_auth(
use crate::crypto::ct_eq;
let selected_data = _selected_data(selected_twofactor);
let mut remember = data.two_factor_remember.unwrap_or(0);
match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => {
@@ -789,13 +788,23 @@ async fn twofactor_auth(
}
Some(TwoFactorType::Remember) => {
match device.twofactor_remember {
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
}
// When a 2FA Remember token is used, check and validate this JWT token, if it is valid, just continue
// If it is invalid we need to trigger the 2FA Login prompt
Some(ref token)
if !CONFIG.disable_2fa_remember()
&& (ct_eq(token, twofactor_code)
&& auth::decode_2fa_remember(twofactor_code)
.is_ok_and(|t| t.sub == device.uuid && t.user_uuid == user.uuid)) => {}
_ => {
// Always delete the current twofactor remember token here if it exists
if device.twofactor_remember.is_some() {
device.delete_twofactor_remember();
// We need to save here, since we send a err_json!() which prevents saving `device` at a later stage
device.save(true, conn).await?;
}
err_json!(
_json_err_twofactor(&twofactor_ids, &user.uuid, data, client_version, conn).await?,
"2FA Remember token not provided"
"2FA Remember token not provided or expired"
)
}
}
@@ -826,10 +835,10 @@ async fn twofactor_auth(
TwoFactorIncomplete::mark_complete(&user.uuid, &device.uuid, conn).await?;
let remember = data.two_factor_remember.unwrap_or(0);
let two_factor = if !CONFIG.disable_2fa_remember() && remember == 1 {
Some(device.refresh_twofactor_remember())
} else {
device.delete_twofactor_remember();
None
};
Ok(two_factor)