fix: revoke existing sessions when a password changes (#28725)

Changing a password left every other logged-in device working until the JWT expired on its own, up to four weeks with the default settings. The hardening docs already promise the opposite: with Redis configured a password change is supposed to put the user's tokens on the revocation list, but only sign-out and OIDC back-channel logout ever wrote to it.

Both password-change paths, self-service and an admin resetting someone's password, now stamp the per-user revocation marker that token validation already checks, so every session issued before the change stops working. The acting device is signed out as well and asked to sign in again, which is the safer default when the password is being changed precisely because the old one may be compromised. Without Redis nothing can be revoked, as before, and the backend now logs a warning saying so.

The marker is written through one shared helper, so its lifetime follows the configured JWT lifetime instead of a fixed 30 days and never expires at all when JWT_EXPIRES_IN disables expiry. Back-channel logout picks that up too, where a long or disabled JWT lifetime previously let the marker expire while the tokens it revoked were still valid. API keys keep working, they are separate credentials with their own lifecycle.

Discussed in #28647.
This commit is contained in:
Classic298
2026-08-17 13:56:29 -07:00
committed by GitHub
parent 3fc491d22f
commit 21e390561d
6 changed files with 49 additions and 14 deletions
@@ -1,7 +1,8 @@
<script lang="ts">
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { updateUserPassword } from '$lib/apis/auths';
import { updateUserPassword, userSignOut } from '$lib/apis/auths';
import { user } from '$lib/stores';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
@@ -23,7 +24,20 @@
);
if (res) {
toast.success($i18n.t('Successfully updated.'));
// This session is no longer trusted once the password it was issued under changes
toast.success($i18n.t('Password updated. Please sign in again.'));
localStorage.removeItem('token');
user.set(null);
const signOutRes = await userSignOut().catch((error) => {
console.error(error);
return null;
});
if (signOutRes?.redirect_url) {
location.href = signOutRes.redirect_url;
}
}
currentPassword = '';
@@ -1997,6 +1997,7 @@
"Passthrough params": "",
"Password": "",
"Password for the bind DN.": "",
"Password updated. Please sign in again.": "",
"Passwords do not match.": "",
"Paste inserted prompts as rich text when possible.": "",
"Paste Large Text as File": "",