Commit edfd414
## Description
Fixes #8863.
`PersistedCredentials` did not record when its token was received, and
`install_persisting_credential_store` hardcoded `token_received_at:
None` when seeding the rmcp credential store. The result: rmcp's
pre-emptive refresh check at
[`AuthorizationManager::get_access_token`](https://github.com/warpdotdev/rmcp/blob/c0f65dc/crates/rmcp/src/transport/auth.rs#L617-L633)
— which requires both `expires_in` and `token_received_at` — was skipped
for the lifetime of every OAuth-authenticated session.
After the first authorization, the cached access token was used past its
TTL. When a request finally hit a 401, rmcp's
[`handle_response`](https://github.com/warpdotdev/rmcp/blob/c0f65dc/crates/rmcp/src/transport/auth.rs#L710-L720)
returns `AuthorizationRequired` without attempting recovery — the user
sees their MCP server disconnect and is forced to re-authenticate. That
matches the reproducer in #8863 exactly: "after logging in the first
time if you wait 1hr… the MCP server disconnects and requires you to
login again."
The cached-credentials path partially masked the bug because of the
unconditional `auth_manager.refresh_token()` workaround at
[`oauth.rs:272`](app/src/ai/mcp/templatable_manager/oauth.rs#L272) —
that call writes back fresh credentials with `received_at` properly set,
papering over the missing seed value for the duration of that session.
The fresh-OAuth path has no such fallback, so users hit the bug on their
first session.
### Fix
1. Add `token_received_at: Option<u64>` to `PersistedCredentials`.
`#[serde(default)]` keeps existing on-disk credentials deserializable —
they come back with `None` and the next refresh populates the field.
2. Forward `token_received_at` from `StoredCredentials` through
`PersistingCredentialStore::save` to the persist channel so the value
reaches secure storage.
3. Thread `token_received_at` as a parameter into
`install_persisting_credential_store` and seed the inner store with it
instead of hardcoded `None`.
4. Cached-creds call site (`make_authenticated_client`): pass the value
loaded from `PersistedCredentials`.
5. Fresh-OAuth call site: stamp `token_received_at` to
`now_epoch_secs()` at the point we save the credentials, and pass the
same value into the seed.
Spec reference: [RFC 6749 §5.1
`expires_in`](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1)
and [§6 refresh-token
semantics](https://datatracker.ietf.org/doc/html/rfc6749#section-6);
rmcp's pre-emptive refresh implements the recommended client behavior of
refreshing before expiry to avoid in-flight 401s.
### Things deliberately left alone
- The unconditional connect-time `auth_manager.refresh_token()` call at
[oauth.rs:272](app/src/ai/mcp/templatable_manager/oauth.rs#L272) and the
stale comment above it claiming rmcp's fork lacks expiry detection. The
fork at the pinned revision (c0f65dc, "Fixes for oauth expiry and
scopes") *does* now have proper detection per upstream
[rust-sdk#680](modelcontextprotocol/rust-sdk#680),
so the workaround is now redundant. Removing it is a behavior change
worth its own PR — out of scope here.
- 401-on-active-request retry. rmcp's `handle_response` still bubbles up
`AuthorizationRequired` rather than refresh-and-retry; pre-emptive
refresh covers the common case but not surprise expiries (clock skew,
server-side revocation). A reactive layer is a follow-up.
## Testing
Six new unit tests in `app/src/ai/mcp/templatable_manager/oauth.rs`:
- `persisted_credentials_round_trip_through_serde_preserves_received_at`
— value preservation.
- `persisted_credentials_deserializes_legacy_format_without_received_at`
— **regression-protects existing users on upgrade**: credentials
persisted by older Warp must still deserialize, with `received_at`
defaulting to `None`. Failing this test would mean existing users lose
their MCP OAuth tokens.
- `save_forwards_token_received_at_to_persist_channel` — the core
regression test for the fix.
- `save_forwards_none_when_received_at_is_none` — defensive: don't
substitute a fake value if rmcp passes `None`.
- `save_skips_persist_when_token_response_absent` — no regression of the
existing `if let Some(token_response)` branch.
- `save_carries_forward_refresh_token_and_preserves_received_at` —
combined check that the existing refresh-token carry-forward (RFC 6749
§6) and the new `received_at` propagation don't interfere.
- `now_epoch_secs_returns_recent_unix_time` — sanity check on the
helper.
I was not able to run `./script/presubmit` locally (`warpui` build needs
`xcrun metal` which requires full Xcode.app, only CommandLineTools is
installed here). Trusting CI for `cargo clippy --workspace --all-targets
--tests -- -D warnings` and `cargo nextest run --workspace`.
## Server API dependencies
N/A — client-only change.
- [x] Is this change necessary to make the client compatible with a
desired [server API breaking
change](https://www.notion.so/warpdev/How-to-safely-introduce-server-API-breaking-changes-0aa805ff5d5d41fd8834f3c95caba0b4):
**No**
## Agent Mode
- [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode
## Changelog Entries for Stable
CHANGELOG-BUG-FIX: Fix MCP OAuth servers disconnecting after
access-token TTL expires; refresh tokens are now used to obtain new
access tokens before expiry (#8863).
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6289aec commit edfd414
1 file changed
Lines changed: 262 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
| 2 | + | |
2 | 3 | | |
3 | 4 | | |
4 | 5 | | |
| |||
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
44 | 68 | | |
45 | 69 | | |
46 | 70 | | |
| |||
108 | 132 | | |
109 | 133 | | |
110 | 134 | | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
111 | 138 | | |
112 | 139 | | |
113 | 140 | | |
114 | 141 | | |
115 | 142 | | |
116 | 143 | | |
117 | 144 | | |
| 145 | + | |
118 | 146 | | |
119 | 147 | | |
120 | 148 | | |
| |||
141 | 169 | | |
142 | 170 | | |
143 | 171 | | |
| 172 | + | |
144 | 173 | | |
145 | 174 | | |
146 | 175 | | |
| |||
150 | 179 | | |
151 | 180 | | |
152 | 181 | | |
153 | | - | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
154 | 187 | | |
155 | 188 | | |
156 | 189 | | |
157 | 190 | | |
158 | 191 | | |
159 | 192 | | |
160 | 193 | | |
161 | | - | |
| 194 | + | |
162 | 195 | | |
163 | 196 | | |
164 | 197 | | |
| |||
233 | 266 | | |
234 | 267 | | |
235 | 268 | | |
| 269 | + | |
236 | 270 | | |
237 | 271 | | |
238 | 272 | | |
| |||
262 | 296 | | |
263 | 297 | | |
264 | 298 | | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
265 | 302 | | |
266 | 303 | | |
267 | 304 | | |
268 | 305 | | |
269 | 306 | | |
| 307 | + | |
270 | 308 | | |
271 | 309 | | |
272 | 310 | | |
| |||
407 | 445 | | |
408 | 446 | | |
409 | 447 | | |
410 | | - | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
411 | 453 | | |
412 | 454 | | |
413 | 455 | | |
414 | 456 | | |
415 | 457 | | |
416 | 458 | | |
| 459 | + | |
417 | 460 | | |
418 | 461 | | |
419 | 462 | | |
| |||
427 | 470 | | |
428 | 471 | | |
429 | 472 | | |
430 | | - | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
431 | 481 | | |
432 | 482 | | |
433 | 483 | | |
| |||
569 | 619 | | |
570 | 620 | | |
571 | 621 | | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
0 commit comments