publish date:
Draft thread (2026-08-02), six posts, each under 300 characters. Full
evidence, reproduction, and fault verdict: 001 — bsky.social silently
drops rpc: scopes.
bsky.social’s OAuth silently drops requested scopes from access tokens, then its own PDS 403s the very call that needs them. We hit this adding external sign-in to our app. Here’s the exact chain, the evidence, and who we think is at fault.
Our app lets any atproto handle sign in. For bsky-hosted users we call com.atproto.server.getServiceAuth on their home PDS (aud = our PDS, lxm = com.atproto.server.createAccount) to adopt the DID. Per the spec we request scope rpc:com.atproto.server.createAccount?aud=did:web:zds.linji.at, declared in our client metadata.
What actually happened:
The contradiction: the AS accepts a scope at PAR it never grants, the user is never told, and the resource server then demands it. Decoded token: jti tok-5eaa2950…, scope “atproto”, client_id app0.linji.at, iss bsky.social. Reproduced twice.
Who’s at fault? Mostly bsky’s AS: a partial grant with zero signal, and its own PDS requiring a scope its AS won’t issue. But RFC 6749 s3.3 says the token response must include the granted scope — bsky did. A client checking it would have noticed. And nothing steers third parties to the scope that actually works.
The fix: request “atproto transition:generic”. bsky grants it, and its PDS treats it as covering rpc permission for any non-chat method (ScopePermissionsTransition.allowsRpc), which is all we need. Takeaway: check the granted scope on every token response, and for getServiceAuth against bsky, use transition:generic.