hypercerts-org / hypercerts-org/ePDS

OAuth clients with granular scopes cannot call a third-party service (CGS) through this PDS: proxy checks the rpc permission against the bare DID

Open
#250 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
8
Forks
4
Avg merge
5d 11h
Merged PRs (30d)
3

Description

Summary

An OAuth client using the granular permission scopes (include:…, rpc:…) cannot obtain service auth for a third-party service such as the Certified Group Service through this PDS. Both routes a client has are closed by the @atproto/pds version the ePDS currently runs (certified.one and dev.certified.app both report 0.4.211 on /xrpc/_health):

  1. Proxied XRPC (atproto-proxy: did:web:<host>#<serviceId>). pipethrough.js in 0.4.211 authorises with permissions.assertRpc({ lxm, aud }) where aud is the bare DID from parseProxyInfo(...). But @atproto/oauth-scopes 0.3.1 (isAudParamisAtprotoAudience) only accepts a DID service reference (did:web:host#service) as an rpc audience, so the only scope a client can be granted is rpc:*?aud=did:web:host%23service, and did:web:host#service !== did:web:host. Every proxied call fails with 403 ScopeMissingError: Missing required scope "rpc:<lxm>?aud=did:web:host", a scope no authorization server will ever issue.
  2. com.atproto.server.getServiceAuth. Its aud parameter is validated as a plain DID in 0.4.211 and rejects the service-reference form: 400 InvalidRequest: aud must be a valid did. Passing the bare DID instead fails the scope check for the same reason as above.

So with granular scopes there is no way to mint or obtain a JWT for did:web:groups.certified.app. Only transition:generic (which implies rpc:*?aud=*) gets through.

Upstream fix

@atproto/pds 0.5.x fixed both sides (pipethrough.js in 0.5.31, comment in the source): the proxy checks the permission against the did#serviceId form "so OAuth callers' rpc:?aud=did#service scopes match", while the outbound service-auth JWT keeps the bare-DID aud; getServiceAuth accepts "a valid atproto DID or did#serviceId reference" and uses it as the JWT audience. bsky.social already runs this, which is why Bluesky-hosted accounts work against the same client and CGS while ePDS accounts do not.

How this was verified (2026-09-09)

  • Client metadata scope: atproto account:email include:org.hypercerts.authWrite include:app.certified.authWrite blob:*/* rpc:*?aud=did%3Aweb%3Adev.groups.certified.app%23certified_group_service.
  • Granted token scope from dev.certified.app (logged via getTokenInfo()): the two permission sets, atproto account:email blob:*/* and the rpc:*?aud=…%23certified_group_service entry. Requesting the bare-DID form as well: dropped silently, as expected.
  • session.fetchHandler("/xrpc/app.certified.group.register", { headers: { "atproto-proxy": "did:web:dev.groups.certified.app#certified_group_service" } })403 ScopeMissingError.
  • getServiceAuth?aud=did:web:dev.groups.certified.app%23certified_group_service&lxm=app.certified.group.register400 aud must be a valid did.
  • Same client, same CGS, account on bsky.social: proxied call → 200, group registered.
  • Adding transition:generic to the client scope on dev.certified.app: proxied call → 200.

Ask

Upgrade the ePDS base to @atproto/pds ≥ 0.5 (or backport the two changes above). Until then, clients that need CGS have to request transition:generic in addition to their permission sets, which defeats the point of granular scopes and shows a broad "Repository" grant on the consent screen. The funding demo carries that shim with a pointer back here: holkexyz/qf-platform#113.

Related: the CGS side is already on the service-DID aud model (certified-group-service docs/aud-migration.md), so nothing is needed there.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the ePDS dependency/version configuration with @atproto/pds 0.5.x, focusing on pipethrough.js and getServiceAuth. Upgrade the base or backport both service-reference audience changes, then verify that granular CGS proxy calls and service-auth requests succeed without requiring transition:generic.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
authentication, authorization, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.