fix(oauth): advertise offline_access without letting it touch the tier clamp (#159) - #165
Merged
Merged
Conversation
ChatGPT checks discovery metadata for offline_access before trusting refresh-token renewal. Advertise it in both documents, and strip it at authorize and refresh before the scope decision, so a client sending only that word is neither pinned to read nor narrowed on renewal. Closes #159 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApYKEKcBnpuz8w6BX2agfH
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.
Closes #159
What
Both OAuth discovery documents now list
offline_accessinscopes_supported, and a client that sends that scope, at authorize or at refresh, is treated exactly as if it had not: it is never mapped to a tier, and it never changes what is granted.Why
OpenAI's "Developer mode and MCP apps in ChatGPT" page (re-read 2026-09-07) says ChatGPT inspects a provider's discovery metadata for
offline_accessbefore trusting refresh-token renewal, and that without it "ChatGPT may lose access after the original authorization expires". Saddle issues refresh tokens unconditionally and advertises therefresh_tokengrant, but never listed the scope. WithACCESS_TTL = 3600the failure mode is a connector that works for one hour and then needs reconnecting, which is indistinguishable from the "connected but broken" reports already being fielded.How
Saddle_OAuth::REFRESH_SCOPEandadvertised_scopes().SCOPESis untouched and a test pins it at exactly three; the tier clamp never sees the new word.Saddle_OAuth::strip_refresh_scope()runs before the authorize endpoint's "did the client name a scope?" decision. Without it, a client sending onlyoffline_accesswould have been treated as one that asked foropenid profile: pinned to read forever, the pre-fix(mcp): answer the handshake the way strict clients require (#97) #98 ChatGPT outcome all over again.offline_accessnormalized tosaddle:readand silently narrowed an admin grant on renewal.scopenever containsoffline_access, so the invariant that every granted scope is a subset ofSCOPESholds.Testing
composer lint— 0 errorscomposer test— 657 tests (was 654), 1 pre-existing skipoffline_accessalone reaches consent proposing the site's own level;saddle:read offline_accessis not widened; a refresh naming onlyoffline_accesskeeps the granted scope