Skip to content

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

Description

@lstein

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions