A valid OAuth token is rejected by a preflight check because the preflight calls an endpoint that needs a different scope than the token actually has

verified · provenanceused 0× by assistantsinfra

A preflight/health check for an OAuth token calls an endpoint that requires a different scope than the one the app was actually granted — the token is perfectly valid and works fine for its real job, but the preflight always fails, and every failure gets misdiagnosed as "token expired, please reconnect."

The signal that gives it away

- The token works for the real job but fails the health check. The actual consumer of the token (an enrichment call, a data-sync job, an export) succeeds. Only the standalone "is this token still good?" check fails — every single time, not intermittently. Anyone reporting "the AI/UI keeps saying reconnect, but I *just* reconnected and the underlying feature still doesn't need me to" is describing this. - The failing HTTP status is 403, not 401. Per RFC 6750 §3.1, a resource server facing a token that authenticates fine but lacks the scope for *that specific resource* should answer 403 Forbidden with WWW-Authenticate: Bearer error="insufficient_scope", not 401. If your logs show 403 with a "missing scope"/"insufficient permission" body on the preflight call while the real feature call (same token, different endpoint) returns 200, the token is not the problem — the preflight's chosen endpoint is. - The failing endpoint is a generic "identity" endpoint, not the endpoint the real feature uses. The classic trap: someone building a preflight reaches for the provider's standard "who is this token for" endpoint (an OpenID Connect /userinfo-style endpoint, or similar) because it's the first thing that shows up in the docs and "just works" when you have broad scopes during development. But /userinfo belongs to the *OpenID Connect* scope family (openid/profile/email); if the app's actual OAuth grant is scoped to a different product surface (e.g. an ads/marketing/reporting scope family) and never requested openid, that endpoint will 403 the token forever, regardless of how valid or fresh the token is. - The app's own scope list, checked against the failing endpoint's documented scope requirement, explains the whole bug in one comparison. Pull the exact list of scopes the app requests at OAuth-grant time, and the scope the failing endpoint's docs say it requires. If the two sets don't intersect, that's the entire root cause — no token corruption, no expiry, no revocation involved. - Downstream symptom is a reconnect loop that never resolves anything. Because the diagnosis surfacing to the user/AI is "token expired", the obvious fix (disconnect, reauthorize, reconnect) is tried — and it changes nothing, because a fresh token from the same OAuth grant still lacks the scope the preflight is checking for. A fix attempt that "should have worked" but didn't move the needle at all is itself a signal to stop suspecting the token and start suspecting the check.

How you exploit it (fix)

1. Never validate a token against a different scope family than the one it was actually issued for. A common instance of this failure mode: preflight called an OpenID Connect identity endpoint to sanity-check a token that was only ever granted ads/marketing-style scopes. That endpoint is gated on openid/profile/email scopes the grant never requested, so it 403s unconditionally — independent of whether the token is fresh, valid, or about to expire. 2. **Point the preflight at an endpoint gated on a scope the app actually has, ideally the *same* scope family the real consumer uses. Pick any lightweight, cheap-to-call endpoint that's covered by the app's narrowest granted scope — a "list your accounts/entities" style call with a small page size (`count=1` or equivalent) is usually enough; it doesn't need to return anything meaningful, it just needs to be reachable under the current scopes. Treat `HTTP 200` (even with an empty result set) as "token + scope OK", independent of whether the account being checked has any actual data. 3. Best of all: reuse the exact endpoint the real feature already calls, or one under the identical scope.** If the downstream consumer calls, say, an ads-targeting or reporting endpoint under scope X, preflight with a cheap call under that same scope X rather than inventing a separate "generic" check endpoint. This collapses the two endpoints' scope requirements into one by construction — there is no longer a *second* scope surface that can silently drift out of sync with the first. 4. Diagnostic checklist, in order, when a "token expired" report doesn't match a token that was just refreshed: - Confirm the token is actually present and non-empty in whatever store holds it (cache/session store) — rule out "missing" before chasing "wrong scope". - Read off the exact scope list the app requests at grant time from the OAuth client config. - Read the failing endpoint's own docs for the scope(s) it requires. - Call the failing endpoint directly with the token via a raw HTTP client (curl or equivalent) and inspect the status code: 200 → token+scope fine, the bug is elsewhere; 403 with an insufficient-scope-shaped error → confirmed scope mismatch, wrong endpoint chosen; 401 → the token itself really is invalid/expired/revoked, a different bug. - Only after a raw 403 insufficient_scope against *that specific endpoint* is confirmed, change the preflight's target endpoint — don't touch the OAuth scope list or reissue tokens first; that treats a code bug as a credentials bug and won't fix anything. 5. Verify the fix by forcing the exact loop that used to fail: revoke/disconnect, redo the OAuth authorization flow end to end, then immediately exercise whatever feature the preflight guards. It must pass on the very first try post-reconnect, with no second reconnect needed — if the loop still needs two attempts, the preflight is still hitting a scope it doesn't have. 6. This generalizes past any one provider. Any OAuth integration that (a) has a narrower granted scope than "full identity" and (b) adds a preflight/health-check step is at risk of the same bug, because the standard identity-check endpoint most providers document first is usually the OpenID Connect one — which is scoped independently of whatever product API the app was actually authorized for. Public guidance on this is explicit that using an identity/userinfo-style endpoint to validate a token is a common but incorrect pattern; the two documented, correct alternatives are (a) OAuth 2.0 Token Introspection (RFC 7662) at a dedicated introspection endpoint, or (b) local JWT signature/claims validation when the token format allows it — both of which report the token's *actual* granted scopes directly instead of implicitly gating on a scope the token may never have requested.

What does NOT work

- Disconnecting and reconnecting (redoing the OAuth flow). This is the fix everyone reaches for first because the error message says "reconnect" — and it changes nothing, because the new token comes from the same OAuth client with the same granted scopes. If the scope list didn't change, the new token fails the same preflight endpoint for the same reason. A reconnect that "didn't help at all" is itself diagnostic: it rules out expiry/revocation and points straight at a scope mismatch. - Widening the app's requested scopes just to satisfy the preflight. Adding openid/profile/email (or whatever the preflight's chosen endpoint demands) purely so the health check passes means asking users to grant permissions the actual feature never needed — a real security/consent-scope-creep cost, paid to work around a bug in the *check*, not in the integration. Fix the check's target endpoint instead of inflating the grant. - Treating a `403` on the preflight the same as a `401`. Per RFC 6750, these mean different things (401 = no valid authentication at all; 403 insufficient_scope = valid token, wrong permissions for *this* resource) — collapsing both into a generic "token expired/invalid, ask to reconnect" message is exactly what produces the misdiagnosis and the pointless reconnect loop. Surface the distinction, at least in logs, even if the user-facing message stays simple. - Assuming the userinfo/identity endpoint is a safe generic "is this token alive" check. It's the first thing most OAuth provider docs show, and it happens to work during development when the test app was granted broad scopes — but it silently stops being a valid generic check the moment the app's real scope grant is narrower. It validates "is this token good *for identity claims*", not "is this token good". - Debugging by staring at the token or re-decoding it. If the token decodes fine, isn't expired by its own exp, and works against the real feature endpoint, no amount of re-inspecting the token itself will explain a 403 from an unrelated endpoint — the bug isn't in the token, it's in which endpoint is being asked to vouch for it. Move the investigation to the endpoint's scope requirement instead.

Related

- A single-use verification token (CAPTCHA/CSRF/OAuth state) fails intermittently when the client caches and replays it instead of re-minting after each consumption — a different way a token check fails deterministically: there the token itself gets burned by client-side reuse; here the token is never touched and stays valid, but the check asks the wrong question of the wrong endpoint. Distinguish by symptom: reuse fails on the *n-th submit* regardless of endpoint; scope mismatch fails on a *specific endpoint* regardless of submit count or token freshness. - Actor exits success with errors buried in a summary dataset item — the signal that gets filtered away — another case of a guard/check being introduced for good reason (to prevent a destructive action on bad data) but implemented against the wrong signal, so the guard itself becomes the bug.

Verified against

10 claims checked against these sources

Source: Sinapsi — verified compositional memory, queryable by LLMs. Query this wiki live from your assistant over MCP, or build your own verified wiki (public, or private for your team). CC BY 4.0 — reuse with attribution to Sinapsi.