Skip to content

fix(oauth): align scopes_supported and pdscheck probing with the spec#185

Merged
ascorbic merged 6 commits into
mainfrom
fix/oauth-scopes-supported-spec-compliant
May 24, 2026
Merged

fix(oauth): align scopes_supported and pdscheck probing with the spec#185
ascorbic merged 6 commits into
mainfrom
fix/oauth-scopes-supported-spec-compliant

Conversation

@ascorbic
Copy link
Copy Markdown
Owner

Background

The atproto OAuth spec on `scopes_supported`:

`scopes_supported` (array of strings, required): must include `atproto`. If supporting the transitional scopes, they should be included here as well.

The reference oauth-provider lists exactly the four spec-defined values and explicitly comments "Other atproto scopes can't be enumerated as they are dynamic." Granular scopes (`repo:`, `rpc:`, `blob:`, `account:<…>`, `identity:<…>`) and permission-set scopes (`include:`) are parameterised — bare prefixes aren't valid scope values that can be granted.

Cirrus was advertising bare `repo`, `rpc`, `blob`, `account`, `identity`, and `include` alongside the four spec values. pdscheck used the presence of bare `repo` as its signal for "this AS supports granular" and only exercised the granular boundary tests against Cirrus — never against the reference. The whole feedback loop was internal to a non-spec convention we'd invented.

Changes

`packages/oauth-provider/src/provider.ts` — trim `scopes_supported` to the four spec-defined values. Drops `repo`, `rpc`, `blob`, `account`, `identity`, and the conditional `include` entry.

`apps/check/src/lib/oauth-flow.ts` — `flow.select-scope` no longer reads `scopes_supported`. It now sends a probe PAR with `GRANULAR_SCOPE` and:

  • accepts `request_uri` → use granular.
  • `invalid_scope` / `invalid_request` → fall back to `LEGACY_SCOPE` with a warn.

Step reordered after `flow.generate-pkce` and `flow.generate-dpop-key` so the probe has the required inputs. Same flow now works identically against Cirrus, bsky.social, or any other spec-compliant AS that supports granular scopes.

`apps/check/src/lib/oauth-flow.ts` — `flow.par-rejects-invalid-include` drops its advertising gate; always probes with a bogus include NSID.

`apps/check/src/lib/oauth-flow.ts` — `flow.par-accepts-advertised-include` removed. Its purpose ("AS accepts the include: it advertised") only made sense under the non-spec convention. `flow.par-accepts-known-permission-set` still exercises include resolution via a real published lexicon.

`apps/check/src/checks/oauth-discovery.ts` — `oauth-discovery.scope-resource-buckets` and `oauth-discovery.scope-permission-sets` removed. Both asserted on non-spec advertising patterns.

Test plan

  • `pnpm --filter @getcirrus/oauth-provider test` — 116 passing.
  • `pnpm --filter @getcirrus/pds test` — 313 unit + 84 CLI passing.
  • `apps/check` typecheck clean.
  • Manual OAuth flow against pds.mk.gg after deploy: confirm `flow.select-scope` probes granular, accepts, runs boundary tests.
  • Manual OAuth flow against bsky.social: confirm probe falls back to LEGACY, boundary tests skip.

…he spec

The atproto OAuth spec requires `atproto` in scopes_supported and recommends
the transitional scopes when supported. Everything else — granular resource
scopes (`repo:<nsid>`, `rpc:<lxm>`, `blob:<mime>`, `account:<…>`, `identity:<…>`)
and permission-set scopes (`include:<nsid>`) — is parameterised and can't be
enumerated. The reference oauth-provider explicitly says so. Cirrus was
advertising bare prefixes (`repo`, `rpc`, `blob`, `account`, `identity`,
`include`) that aren't valid as standalone scope values, which only worked
because pdscheck used the same non-spec convention to detect granular support.

Changes:

- Cirrus oauth-provider: trim scopes_supported to the four spec-defined values.
- pdscheck flow.select-scope: replace the scopes_supported lookup with a probe
  PAR using GRANULAR_SCOPE; on accept use granular, on invalid_scope fall back
  to LEGACY_SCOPE. Move the step after PKCE/DPoP-key generation so the probe
  has the required inputs.
- pdscheck flow.par-rejects-invalid-include: drop the advertising gate and
  always probe with a bogus include NSID.
- pdscheck: remove flow.par-accepts-advertised-include — the
  par-accepts-known-permission-set probe covers include acceptance via a real
  published lexicon.
- pdscheck: remove oauth-discovery.scope-resource-buckets and
  scope-permission-sets — both asserted on the non-spec advertising convention.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
pdscheck a12dded May 24 2026, 10:24 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
cirrusdocs a12dded Commit Preview URL

Branch Preview URL
May 24 2026, 10:24 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 24, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
atproto-pds a12dded May 24 2026, 10:24 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 24, 2026

Open in StackBlitz

npm i https://pkg.pr.new/create-pds@185
npm i https://pkg.pr.new/@getcirrus/oauth-provider@185
npm i https://pkg.pr.new/@getcirrus/pds@185

commit: a12dded

ascorbic added 5 commits May 24, 2026 22:58
…sion set

flow.select-scope was probing GRANULAR_SCOPE which combined repo: and
include:site.standard.authFull. The include NSID isn't a real published
permission set, so any AS with lexicon resolution wired up (the reference,
bsky.social) rejected the whole probe with invalid_scope — and pdscheck
mis-attributed this to "AS doesn't support granular".

Split: GRANULAR_SCOPE is now just `atproto repo:earth.cirrus.check.testrecord`,
testing granular repo scope acceptance. The known-permission-set probe
moves to `include:app.bsky.authFullApp` — a real published permission set
from the atproto reference lexicons.
…t probe

site.standard.authFull is a real published permission set
(_lexicon.standard.site TXT → did:plc:re3ebnp5v7ffagz6rb6xfei4),
copied from leaflet.pub's OAuth metadata. Keeping it means the
probe surfaces whether an AS has lexicon resolution wired up.
The GRANULAR_SCOPE split from the previous commit stays — that
isolates granular-repo support from include-resolution support.
clientId() was embedding `scope=<activeScope>` in the localhost client_id
URL. activeScope starts as LEGACY_SCOPE, so when flow.select-scope probed
with GRANULAR_SCOPE, the request asked for `repo:…` but the client metadata
only declared `atproto transition:generic`. Reference rejects with
\"Scope ... is not declared in the client metadata\".

The activeScope is for what we *request*; the client_id metadata is for
what we're *allowed* to request. Decouple them: the loopback client_id
now declares the union of every scope any probe might use. Matches the
deployed public/client-metadata.json which already does this.
… too

Splitting GRANULAR_SCOPE into "granular only" surfaced the granular-repo
support cleanly but stripped the include from the real PAR — the consent
UI then only listed the repo: scope. Probe in tiers instead:

1. granular + include (GRANULAR_WITH_INCLUDE_SCOPE) — best case; consent
   shows both.
2. granular only — middle case; permission-set tests skip.
3. legacy — worst case; boundary tests skip.

The probe still isolates failures (granular vs include) and the user sees
every scope the AS will actually grant in their consent.
@ascorbic ascorbic enabled auto-merge (squash) May 24, 2026 22:24
@ascorbic ascorbic merged commit aed8e1b into main May 24, 2026
7 checks passed
@ascorbic ascorbic deleted the fix/oauth-scopes-supported-spec-compliant branch May 24, 2026 22:24
@mixie-bot mixie-bot Bot mentioned this pull request May 24, 2026
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