Skip to content

persist rotated refresh tokens - #378

Open
fredrikekre wants to merge 2 commits into
doy:mainfrom
fredrikekre:fe/refresh-tokens
Open

fredrikekre wants to merge 2 commits into
doy:mainfrom
fredrikekre:fe/refresh-tokens

Conversation

@fredrikekre

Copy link
Copy Markdown

rbw only deserializes access_token from the refresh response, so a rotated
refresh_token is dropped and the original is replayed on every subsequent
refresh. Harmless against Bitwarden, which does not rotate, but breaks every
refresh after login against an SSO-backed Vaultwarden, where the identity
provider consumes the token on exchange.

Deserialize it, thread it through with_exchange_refresh_token, and persist it
at the call sites. Option, so non-rotating servers keep their current behaviour.

I found this after being rate limited on my Authelia instance. 🤖 wrote the patch
but I have reviewed it and looks ok (although I am not a Rust programmer).

rbw only deserializes access_token from the refresh response, so a rotated
refresh_token is dropped and the original is replayed on every subsequent
refresh. Harmless against Bitwarden, which does not rotate, but breaks every
refresh after login against an SSO-backed Vaultwarden, where the identity
provider consumes the token on exchange.

Deserialize it, thread it through with_exchange_refresh_token, and persist it
at the call sites. Option, so non-rotating servers keep their current behaviour.

Assisted-by: Claude Code (Fable 5.1)
pschmitt added a commit to pschmitt/rbw that referenced this pull request Sep 14, 2026
rbw only deserialized access_token from the OAuth refresh response, so a
rotated refresh_token was silently dropped and the stale one kept getting
replayed on every subsequent refresh. Harmless against Bitwarden, which
doesn't rotate, but breaks every refresh after the first one against an
SSO-backed Vaultwarden, where the identity provider consumes the refresh
token on exchange.

Deserialize it (Option, so non-rotating servers are unaffected), thread it
through as a new RefreshedTokens { access_token, refresh_token } struct
alongside the existing access_token, and persist it at every call site that
already persists a rotated access_token -- this fork has grown a much larger
surface of token-refreshing actions (org/collection management, bulk import,
mirror, TUI) than upstream, so this adapts and extends doy#378 rather
than cherry-picking it directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VjzznAfr5RkgQAFYKTaV9A
A refresh that fails with invalid_grant will never succeed: no password or
second factor is involved, so the only thing the server can be rejecting is the
refresh token itself. rbw treated it like any other error, so the agent retried
on every sync_interval and kept doing so until the user happened to run rbw
login. Against an SSO-backed Vaultwarden that meant one rejected exchange per
interval, indefinitely.

Check the response status before deserializing, classify invalid_grant as a
distinct error, and clear the stored tokens when it occurs. That makes
db.needs_login() true, so the next command takes the ordinary login path and the
syncs in between fail locally without reaching the network. The match is on the
error field rather than the status code, since RFC 6749 mandates 400 but
providers also return 401; transport errors and unrecognised responses still
retry as before.

The tokens are only cleared if the rejected one is still the one stored.
Rotating providers also return invalid_grant when two refreshes race, and there
the rejected token has already been replaced by a valid one.

Assisted-by: Claude Code (Fable 5.1)
@fredrikekre

Copy link
Copy Markdown
Author

Added a second commit that stops retrying if the refresh token is rejected (partly overlaps with #362 although in that PR the agent keeps retrying).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant