Authentication improvements (#21194)

* jwt permissions

* add old password to body req

* add model and migration

need to track the datetime that passwords were changed for the jwt

* auth api backend changes

- use os.open to create jwt secret with restrictive permissions (0o600: read/write for owner only)
- add backend validation for password strength
- add iat claim to jwt so the server can determine when a token was issued and reject any jwts issued before a user's password_changed_at timestamp, ensuring old tokens are invalidated after a password change
- set logout route to public to avoid 401 when logging out
- issue new jwt for users who change their own password so they stay logged in

* improve set password dialog

- add field to verify old password
- add password strength requirements

* frontend tweaks for password dialog

* i18n

* use verify endpoint for existing password verification

avoid /login side effects (creating a new session)

* public logout

* only check if password has changed on jwt refresh

* fix tests

Fix migration 030 by using raw sql to select usernames (avoid ORM selecting nonexistent columns)

* add multi device warning to password dialog

* remove password verification endpoint

Just send old_password + new password in one request, let the backend handle verification in a single operation
This commit is contained in:
Josh Hawkins
2025-12-08 09:02:28 -07:00
committed by GitHub
parent 28b0ad782a
commit 152e585206
12 changed files with 554 additions and 101 deletions
+20 -12
View File
@@ -57,6 +57,8 @@ export default function AuthenticationView({
const [showCreateRole, setShowCreateRole] = useState(false);
const [showEditRole, setShowEditRole] = useState(false);
const [showDeleteRole, setShowDeleteRole] = useState(false);
const [passwordError, setPasswordError] = useState<string | null>(null);
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const [selectedUser, setSelectedUser] = useState<string>();
const [selectedUserRole, setSelectedUserRole] = useState<string>();
@@ -70,12 +72,15 @@ export default function AuthenticationView({
}, [t]);
const onSavePassword = useCallback(
(user: string, password: string) => {
(user: string, password: string, oldPassword?: string) => {
setIsPasswordLoading(true);
axios
.put(`users/${user}/password`, { password })
.put(`users/${user}/password`, { password, old_password: oldPassword })
.then((response) => {
if (response.status === 200) {
setShowSetPassword(false);
setPasswordError(null);
setIsPasswordLoading(false);
toast.success(t("users.toast.success.updatePassword"), {
position: "top-center",
});
@@ -86,14 +91,10 @@ export default function AuthenticationView({
error.response?.data?.message ||
error.response?.data?.detail ||
"Unknown error";
toast.error(
t("users.toast.error.setPasswordFailed", {
errorMessage,
}),
{
position: "top-center",
},
);
// Keep dialog open and show error
setPasswordError(errorMessage);
setIsPasswordLoading(false);
});
},
[t],
@@ -563,8 +564,15 @@ export default function AuthenticationView({
</div>
<SetPasswordDialog
show={showSetPassword}
onCancel={() => setShowSetPassword(false)}
onSave={(password) => onSavePassword(selectedUser!, password)}
onCancel={() => {
setShowSetPassword(false);
setPasswordError(null);
}}
initialError={passwordError}
onSave={(password, oldPassword) =>
onSavePassword(selectedUser!, password, oldPassword)
}
isLoading={isPasswordLoading}
/>
<DeleteUserDialog
show={showDelete}