Conversation
…itself Executor performed the OAuth refresh exchange itself, so it had to ask the credential provider for the stored refresh token and the client secret, which a sealed store cannot hand over. CredentialProvider gains an optional refreshGrant: the provider spends the refresh token, seals the new tokens under the same item ids and returns only lifetime and scope, which the host re-validates. Providers without it, client_credentials grants and first-party apps keep the existing host path unchanged.
This was referenced Sep 20, 2026
This branch has not been deployed
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.
Executor performs the OAuth refresh exchange itself, which means it first asks the credential
provider to hand over the stored refresh token and the app's client secret. A provider whose whole
purpose is to keep those values sealed cannot answer that, so it has to refuse the refresh item and
lose refresh entirely. The refresh grant is the one exchange where a long-lived stored secret must
be spent and where the reply is itself a fresh credential, so it is exactly the place a sealed
store has no way through.
CredentialProvidergains an optionalrefreshGrant. When a provider implements it, Executor asksit to perform the exchange rather than to reveal anything: the provider spends the refresh token,
seals the newly minted access token and any rotated refresh token under the same item ids, and
returns only the granted lifetime and scope. Secrets cross the seam as item ids, never as values,
so the host resolves neither the refresh token nor the client secret on that path. Executor
re-validates the returned metadata before it persists anything, rebuilding scope from the grant set
it already trusts and converting the lifetime against its own clock, then reads the access token
back through
getlike every other credential. A refusal comes back as a closed set ofstandards-defined token-endpoint codes, so a provider refusal classifies re-authentication and arms
the known-dead grant gate exactly as a host-side refusal does. Providers that do not implement
refreshGrantare unaffected, and so are the grants and the apps that have nothing to delegate.This narrows what the host holds; it does not empty it. The access token still passes through the
host at the moment it is spent, because that is what invoking a tool with it requires. What the
change removes from the host's data path is the long-lived material: the refresh token that can
mint indefinitely, and the client secret that authenticates the app. The grant parameters are also
still authored by the caller, so a provider that withholds credentials from that caller has to
authenticate the whole tuple against its own enrollment metadata before it opens anything.
Tests cover the delegated path end to end against a real test authorization server, pinning that
the host never resolves the refresh token or the client secret and never posts a refresh grant
itself, that the host path is unchanged without the capability, that each rejection code classifies
like its host-side equivalent, that malformed and hostile provider results stay inside the provider
boundary, and that a connection with no recorded scope keeps refreshing.
This is part of the credential-handling series described in #1585, which lists every change and the reasoning behind it.