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
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):
- Proxied XRPC (
atproto-proxy: did:web:<host>#<serviceId>).pipethrough.jsin 0.4.211 authorises withpermissions.assertRpc({ lxm, aud })whereaudis the bare DID fromparseProxyInfo(...). But@atproto/oauth-scopes0.3.1 (isAudParam→isAtprotoAudience) only accepts a DID service reference (did:web:host#service) as anrpcaudience, so the only scope a client can be granted isrpc:*?aud=did:web:host%23service, anddid:web:host#service !== did:web:host. Every proxied call fails with403 ScopeMissingError: Missing required scope "rpc:<lxm>?aud=did:web:host", a scope no authorization server will ever issue. com.atproto.server.getServiceAuth. Itsaudparameter 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 viagetTokenInfo()): the two permission sets,atproto account:email blob:*/*and therpc:*?aud=…%23certified_group_serviceentry. 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.register→400 aud must be a valid did.- Same client, same CGS, account on bsky.social: proxied call →
200, group registered. - Adding
transition:genericto the client scope ondev.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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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