Skip to main content

Module token

Module token 

Source
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§

AccessGrant
What an OAuth access token authenticates: an account, through the application the token was issued to.
AuthorizationCode 🔒
A confirmed grant, ready to be redeemed at token — minted the instant consent::confirm approves, which is why it carries everything token needs without a second lookup into anything PAR- or authorize-owned; both of those have already one-shot-consumed their own state by this point.
AuthorizationCodeRecord
The fields AuthorizationCodeStore::issue takes — the same shape as AuthorizationCode, public because a caller assembling one from a confirmed consent::PendingConsent needs to name every field.
GateAdmission
TokenAdmission by plan/policy.md’s gate: the token is judged as a didbot_policy::Subject::Token, within the account’s chain of principals, the way a write by that account is.
MemoryAuthorizationCodeStore
The in-process authorization code store.
OAuthTokenStore
OAuth access/refresh token issuance and rotation.
OAuthTokens
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§

TokenError
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§

AuthorizationCodeStore
Where authorization codes live between consent confirmation and exchange.
TokenAdmission
Asked before any token is issued: whether client_id may hold one for did, carrying scope, 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 own Display.
authenticated_as 🔒
Whether presented may spend a code or login client_id holds, bound to bound: 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 approved may do now: that set as ceiling grants it for this account and client, by the same verdict POST /oauth/par reached. Empty when the ceiling grants none of it.
random_code 🔒
standing
Judges one live grant the way POST /oauth/token would 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_verifier against the code_challenge an authorization code was issued with. S256 only — this server refuses plain at PAR, so nothing here needs to handle it.