publish date:

bsky.social OAuth drops your requested scopes, then 403s you for lacking them

Draft thread (2026-08-02), six posts, each under 300 characters. Full evidence, reproduction, and fault verdict: 001 — bsky.social silently drops rpc: scopes.

1

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.

2

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.

3

What actually happened:

4

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.

5

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.

6

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.