invoke-ai / invoke-ai/InvokeAI

Changing your own password signs you out when a token refresh was accepted in the last 60s

Open
#9,541 0 comments 0 reactions 1 assignee Claimed by @lstein View on GitHub
Dominant language
Python
Stars
28.2k
Forks
3k
Avg merge
6d 5h
Merged PRs (30d)
19

Description

## Summary

In multiuser mode, changing your own password signs you out — but only if a sliding-window token
refresh happened to be accepted in the previous 60 seconds, which makes it look intermittent.

The server goes to some trouble to prevent exactly this: a password change bumps the user's
revocation epoch, which kills the token that authenticated the request, so the route hands back a
replacement token in `X-Refreshed-Token` (`_issue_replacement_token`, `invokeai/app/api/routers/auth.py:41-63`,
called at `:773` for `PATCH /auth/me` and `:642` for an admin resetting their own password). The
client discards that replacement whenever its refresh throttle is active.

## Steps to reproduce

1. Run with multiuser mode enabled and sign in.
2. Use the app normally for a few seconds — any mutating request will do, and simply navigating
the UI persists client state, so this is the usual state of affairs.
3. Within 60 seconds of that, open the profile page and change your password. The request
succeeds (HTTP 200, the password really is changed).
4. On the next request the session ends and you are returned to the login screen.

Wait more than 60 seconds after the last accepted refresh before changing the password, and the
session survives — which is what makes this look flaky rather than deterministic.

## Root cause

1. `PATCH /api/v1/auth/me` bumps `token_epoch` and returns the replacement token in
`X-Refreshed-Token` (`routers/auth.py:698-776`).
2. The response goes through `dynamicBaseQuery`, which hands the header to `acceptRefreshedToken`
(`invokeai/frontend/web/src/services/api/index.ts:151-157`).
3. `acceptRefreshedToken` returns early when `isTokenRefreshThrottled()` is true — a flat 60s
window since the last accepted refresh (`features/auth/store/authTokenRefresh.ts:16-24, 60-62`).
The replacement is dropped silently; `localStorage.auth_token` keeps the superseded token.
4. That token now fails the epoch check in `resolve_authorized_user`
(`invokeai/app/api/auth_dependencies.py:68-69`), so the next request 401s.
5. `shouldEndSessionForUnauthorized` is true (the stale token is still the live one), so the client
dispatches `sessionExpiredLogout()` (`services/api/index.ts:147-149`).

There is no recovery path: the sliding-window middleware correctly refuses to refresh a token whose
epoch is already stale, so nothing re-mints it.

## Why the throttle exists

It is right for what it was written for: the middleware mints a replacement on *every* mutating
request, and each acceptance costs a serialized media-cookie round trip under a cross-tab lock, so
re-committing once a minute is plenty for a token whose lifetime is measured in days. The bug is
that it treats a revocation-advancing replacement as if it were one of those routine slides.

## Suggested fix

Throttle routine slides only. A replacement whose `token_epoch` differs from the token it replaces
is not a slide — it is the only credential that will work from that moment on, and dropping it
strands the session. Gating the early return on the epoch being unchanged is enough; the identity
(`user_id`) still has to match, which `shouldAcceptRefreshedToken` already enforces.

PR #9540 adds a claims decoder and a `user_id:token_epoch` session key to `authSlice`, which would
make that check a one-liner, but the fix does not depend on it.

## Impact

Not data-destructive — the password change itself succeeds, and signing back in with the new
password works. But the user is bounced to the login screen immediately after a successful
password change, which reads as a failure, and any in-flight work in that tab is interrupted.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.