Repository navigation
Conversation
When a provider rejects an extension slot's refresh token with invalid_grant (expired, e.g. Google's 7-day limit for OAuth apps in Testing, or revoked), the broker transport returned a plain "auth broker: refresh: token refresh failed: invalid_grant" error. The Calendar add-source dialogs and account hook panels only show the Grant Calendar Access button for "additional consent required", so a user whose calendar token had died (and who had no existing source to Reauthorize) had no way to recover in the UI. - oauth2: wrap invalid_grant refresh failures in ErrInvalidGrant (message text unchanged). - auth broker: map ErrInvalidGrant to ErrAdditionalConsentRequired, carrying the scopes the client was issued for, but only for slots an extension grant flow can re-grant (the *-calendar and *-contacts slots StartIncrementalConsent resolves via oauth2.GetProvider). Mail slots (custom-mail, and google-/microsoft-mail used for core-routed scopes) keep the original refresh error: only mail re-authorization can replace those tokens, and a Grant button there would loop.
CaseyLabs
force-pushed
the
fix/reauth-on-invalid-grant
branch
from
October 6, 2026 16:13
b856eef to
b147877
Compare
…setup - Add oauth2.IncrementalConsentProvider as the single source of truth for whether a slot can be re-granted; StartIncrementalConsent and the auth transport both use it. - Factor the three bearerRefreshTransport constructions into Broker.newClient. - Trim redundant test assertions and narrow the slot table test.
- Add an optional Reason to ErrAdditionalConsentRequired; the transport sets it on invalid_grant so the message no longer claims scopes are missing. - Clone the scopes slice when building a broker client so the transport doesn't alias the caller's slice.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
If the refresh token for an extension OAuth slot (e.g.
google-calendar) is rejected by the provider withinvalid_grant, there is no way to recover from the UI.This happens routinely with self-created Google OAuth clients (see #336): while the app is in Testing, Google expires refresh tokens after 7 days. Mail keeps working once re-authorized, but the calendar slot's token dies separately.
Repro:
google-calendartoken whose refresh token is expired/revoked.Result: the dialog shows
... auth broker: refresh: token refresh failed: invalid_grant - Bad Requestand no Grant Calendar Access button. That button (inAddGoogleCalendarDialog,AddMicrosoftCalendarDialogand both account hook panels) only appears when the error containsadditional consent required, which the broker only raises when the token row is missing or lacks scopes, not when it exists but is dead. The per-source Reauthorize button in Calendar Settings doesn't help when there is no source. The only workaround is deleting theoauth_tokensrow by hand.Fix
internal/oauth2:RefreshTokenWithProviderwrapsinvalid_grantresponses in a newErrInvalidGrantsentinel. The error message text is unchanged.internal/extensions/auth: when a refresh fails withErrInvalidGrant,bearerRefreshTransportreturns*coreapi.ErrAdditionalConsentRequiredfor that slot, with the scopes the client was issued for. The existing UI then offers the grant flow, andStartIncrementalConsentoverwrites the dead token. The error carries a new optionalReason(refresh token expired or revoked), so its message no longer claims scopes are missing. It still starts withadditional consent required, which is the text the calendar dialogs match.*-calendarand*-contactsslots with credentials configured. A newoauth2.IncrementalConsentProvidermakes that decision, andStartIncrementalConsentnow uses the same function, so the two can't drift apart. Mail slots (custom-mailfor bring-your-own-app accounts, andgoogle-mail/microsoft-mailthat Contacts uses for its read scopes) keep the original refresh error. Only mail re-authorization can replace those tokens, so a Grant button there would just fail again.Other refresh failures (network errors,
invalid_client, etc.) keep the existing error.Tests
TestRefreshTokenWithProvider_InvalidGrant/_OtherErrorIsNotInvalidGrant: sentinel matching against anhttptesttoken endpoint.TestTransport_InvalidGrantOnMailSlotKeepsRefreshError: end to end throughHTTPClientForExtensionfor a custom account, with stub API (401) and token (invalid_grant) servers. Thecustom-mailslot must return theErrInvalidGrantrefresh error, not a consent error.TestErrAdditionalConsentRequired_Error: the message with and withoutReason.TestCanRegrant: which slots get the consent error. An extension slot with credentials does; an extension slot without credentials and the mail slots don't.The calendar and contacts slots refresh against Google's and Microsoft's real token URLs, so there's no end-to-end test for the consent conversion itself;
TestCanRegrantcovers which slots qualify.go test ./internal/... ./extensions/...passes, exceptTestSerializeVEVENT_AllDay. That test also fails onmainwhen run outside UTC; it's unrelated to this change.Notes
Brokernow builds every extension HTTP client through onenewClienthelper instead of three copies of the same setup. The helper copies the scopes list, so the client doesn't share the caller's slice.additional consent required for account … under google-calendar: refresh token expired or revokedas itslast_errorinstead of the rawinvalid_granttext. The Reauthorize button was already shown for those sources.