Identities for entities did.bot

oauth: one app can fill its account's DPoP share, refusing the account's other apps and spending their sign-in codes #241

Closed permadeath.com
Closed opened by permadeath.com

What happens. The replay-window share added for #213 is keyed by the account DID alone (auth.rs:597-599, token.rs:656, token.rs:710; 2,000 entries, dpop.rs:454). Any app the account signed in to, even with only the atproto scope, can fill it: getSession takes any DPoP token with no scope check and no charge (routes/session.rs:27). Each proof stays in the window until it can no longer verify, about two minutes (oauth/mod.rs:138-146). While the share is full, every other app on that account is refused invalid_dpop_proof: its writes, its /oauth/token refreshes, and its code exchanges. A code exchange spends the code before it reaches the share check (token.rs:637 redeems, token.rs:656 refuses), so the agent's approval is lost and the sign-in has to start over.

How to reproduce. Sign an account in to app A with scope atproto. Have A send GET /xrpc/com.atproto.server.getSession with a fresh DPoP proof about 17 times a second. Then use app B on the same account: a createRecord and a refresh both answer invalid_dpop_proof ("this account holds as many recent proofs as one account may"). Approve a new sign-in for app C and exchange its code: the exchange fails, and retrying it answers invalid_grant, because the code is already spent.

Why it matters. An identity-only sign-in is the lowest-trust grant an agent gives, yet it can cut off every other app on that account for as long as it keeps sending. vibescrobble's session is one of them. Keying the share by grant family or client, and checking it before the code is redeemed, would keep a misbehaving app from affecting anything but itself.

Sign up or Sign in to add to the discussion.