Skip to content

fix(oauth): default absent authorize scope so consent completes (BUG-2088)#921

Merged
xarmian merged 1 commit into
mainfrom
fix/oauth-authorize-default-scope-bug-2088
Jul 11, 2026
Merged

fix(oauth): default absent authorize scope so consent completes (BUG-2088)#921
xarmian merged 1 commit into
mainfrom
fix/oauth-authorize-default-scope-bug-2088

Conversation

@xarmian

@xarmian xarmian commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Problem

A user connecting Claude Code to Pad Cloud gets an authorize URL with no scope parameter, and clicking Authorize dead-ends on:

{"error":{"code":"invalid_request","message":"capability_tier must be 'read', 'write', or 'admin'"}}

scope is optional per RFC 6749 §3.1.2, and our advertised scopes_supported (both .well-known docs) is only advisory — a client may legally omit it. Claude Code does. Nothing on the server defaults it, so:

  1. ar.GetRequestedScopes() is empty → renderConsent computes canRead/canWrite/canAdmin all false → the "What it can do" fieldset renders zero tier radios.
  2. Clicking Authorize POSTs /oauth/authorize/decide with capability_tier empty → parseConsentPayload rejects it with the error above.

Fix

When the authorize request has no scope, default it to the requesting client's registered scopes (DCR already seeds pad:read pad:write), falling back to pad:read pad:write. A missing/unknown client_id is left untouched so fosite emits its normal invalid_client error.

The default is set on both:

  • r.Form — feeds NewAuthorizeRequest → the consent tier radios, and
  • r.URL.RawQueryrenderConsent builds the consent form's hidden authorize fields from r.URL.Query(), and /authorize/decide rebuilds the AuthorizeRequest from those hidden fields. Without the URL update the decide POST reconstructs a scope-less request and rejects the chosen tier one step later.

Testing

  • New TestOAuth_Authorize_DefaultsScopeWhenOmitted drives the full GET-consent → POST-decide → code flow, extracting the scope from the rendered hidden field (fails if the field goes missing) and asserting an authorization code is issued.
  • Full OAuth/Consent suite + go build ./... green.
  • Reviewed via Codex review loop (2 rounds → CLEAN); round 1 caught the r.URL.RawQuery half of the fix.

Notes

  • Server-side robustness bug in pad-cloud (cloud-mode only), not a Claude Code bug. Effective once deployed.
  • User workaround until deploy: append &scope=pad:read%20pad:write to the authorize URL before authorizing.

Fixes BUG-2088.

https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF

…2088)

An OAuth authorize request may legally omit `scope` (RFC 6749 §3.1.2;
our advertised scopes_supported is only advisory). Claude Code does.
When it does, ar.GetRequestedScopes() is empty, renderConsent shows
zero capability-tier radios, and clicking Authorize dead-ends the
/authorize/decide POST with "capability_tier must be 'read', 'write',
or 'admin'".

Default an absent scope to the requesting client's registered scopes
(DCR seeds pad:read/pad:write), falling back to pad:read pad:write. A
missing/unknown client_id is left untouched so fosite emits its normal
invalid_client error.

The default is set on BOTH r.Form (feeds NewAuthorizeRequest → the
consent tier radios) AND r.URL.RawQuery: renderConsent builds the
consent form's hidden authorize fields from r.URL.Query(), and
/authorize/decide rebuilds the AuthorizeRequest from those hidden
fields — so without the URL update the decide POST would reconstruct a
scope-less request and reject the chosen tier one step later.

Regression test drives the full GET-consent → POST-decide → code flow,
extracting the scope from the rendered hidden field so it fails if the
field goes missing.

Claude-Session: https://claude.ai/code/session_015yuBJQYfDj95cgX3DaD8SF
@xarmian xarmian merged commit 9938a81 into main Jul 11, 2026
5 of 6 checks passed
@xarmian xarmian deleted the fix/oauth-authorize-default-scope-bug-2088 branch July 11, 2026 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant