Expand description
POST /oauth/token: authorization code exchange and refresh rotation.
Issued tokens are DPoP-bound (RFC 9449) and opaque bearer strings — a
signature scheme and claims format nothing here needs, since a token
authenticates by lookup rather than by being verified on its own. A
client presents one as Authorization: DPoP <token>, which the repo
write routes accept through crate::auth::require_agent_token_or_dpop. A token names the client it
was issued to, and every write it presents is judged with that client.
§A login keeps what the agent approved, and the ceiling is asked every time
A login stores what the decision record granted — the set the agent was
shown and answered, which is the client’s request as the ceiling stood at
PAR. What a token may do is that set as the ceiling in force grants it
now (in_force), asked at exchange, at every refresh, and at every
write. A ceiling tightened since sign-in narrows a login at once. A
ceiling loosened afterwards changes nothing for a live login: an atom the
record named as cut was never approved, so a wider ceiling cannot hand
it back without asking the agent again.
§A login has an end
REFRESH_TTL is measured from the moment a login was minted and a
refresh does not move it, so an approval an agent gave once is spent
within one bounded window rather than renewed indefinitely by a client
that keeps refreshing.
§A confidential client’s login keeps its key
A login a confidential client began is bound to the key it signed its
client assertion with at PAR (ClientAuthKey). The code, and every
refresh after it, must come with an assertion signed by that same key,
found in the key set the client publishes now. A request with no
assertion is invalid_client. One signed with another key, or one that
brings an assertion to a public client’s login, is invalid_grant, and
nothing rotates. See crate::oauth::client_auth.
The lifetimes below are the same for both kinds of client. The atproto profile limits a public client’s login to two weeks and a confidential client’s refresh token to 180 days, and fourteen days is inside both.
§Access token lifetime
ACCESS_TTL is 5 minutes. See plan/oauth.md for the reasoning; in
short, this deployment has no revocation endpoint by design (withdrawal
is refusal at the write path) and no per-token introspection, so by the
atproto profile’s own rule (“5 minutes is recommended” when a server
lacks individual revocation) this is the number, not a compromise toward
it.
Structs§
- Access
Grant - What an OAuth access token authenticates: an account, through the application the token was issued to.
- Authorization
Code 🔒 - A confirmed grant, ready to be redeemed at
token— minted the instantconsent::confirmapproves, which is why it carries everythingtokenneeds without a second lookup into anything PAR- or authorize-owned; both of those have already one-shot-consumed their own state by this point. - Authorization
Code Record - The fields
AuthorizationCodeStore::issuetakes — the same shape asAuthorizationCode, public because a caller assembling one from a confirmedconsent::PendingConsentneeds to name every field. - Gate
Admission TokenAdmissionbyplan/policy.md’s gate: the token is judged as adidbot_policy::Subject::Token, within the account’s chain of principals, the way a write by that account is.- Memory
Authorization Code Store - The in-process authorization code store.
- OAuth
Token Store - OAuth access/refresh token issuance and rotation.
- OAuth
Tokens - One minted access/refresh pair.
- Standing
- Where one live login stands right now. Neither field is stored; both are asked of the ceiling and the gate as they are at this moment.
Enums§
- Token
Error - Why token issuance or refresh was refused.
Constants§
- ACCESS_
TTL - How long a minted access token is good for. See the module doc.
- AUTHORIZATION_
CODE_ TTL - How long an authorization code may be exchanged before it expires. RFC 6749 §4.1.2 asks for “a maximum authorization code lifetime of 10 minutes”; this is well inside that, since the code is minted only after consent has already been confirmed and exchange should follow immediately.
- CODE_
BYTES 🔒 - Random bytes an authorization code is drawn from.
- REFRESH_
TTL - How long a login lasts, from the moment its first pair was minted.
Traits§
- Authorization
Code Store - Where authorization codes live between consent confirmation and exchange.
- Token
Admission - Asked before any token is issued: whether
client_idmay hold one fordid, carryingscope, right now.
Functions§
- approved 🔒
- What a stored login was approved at. A string that will not parse grants
nothing, which is the safe direction for a value this crate rendered with
ScopeSet’s ownDisplay. - authenticated_
as 🔒 - Whether
presentedmay spend a code or loginclient_idholds, bound tobound: no assertion for a public client’s, and an assertion from that client, signed with the key it began with, for a confidential one’s. - confirm_
and_ issue_ code - Confirms a consent reference and, on success, issues the authorization code — the combined operation a future stamped tool call reaches, so that wiring it in is a single call rather than two.
- in_
force - What a login approved at
approvedmay do now: that set asceilinggrants it for this account and client, by the same verdictPOST /oauth/parreached. Empty when the ceiling grants none of it. - random_
code 🔒 - standing
- Judges one live grant the way
POST /oauth/tokenwould judge it if the client presented its refresh token right now. - stored_
client_ 🔒id - The stored client id of a grant or code, as the one spelling policy matches. One that does not parse is refused rather than judged, since no rule could be trusted to match it.
- verify_
pkce 🔒 - Verifies a PKCE
code_verifieragainst thecode_challengean authorization code was issued with.S256only — this server refusesplainat PAR, so nothing here needs to handle it.