didbot_serve/oauth/mod.rs
1//! OAuth 2.0 for atproto (`plan/oauth.md`).
2//!
3//! Built in slices; see each submodule's own doc for what it covers and what
4//! it deliberately does not. [`dpop`] is the RFC 9449 verifier side, and
5//! [`scope`] is `didbot-scope`, the granular atproto scope grammar as a typed
6//! value with containment and intersection — the substrate a ceiling is
7//! expressed over, shared with every other program that computes one.
8//!
9//! The authorization server itself — pushed authorization requests,
10//! `authorize`, `token`, refresh rotation, client metadata — is sibling work
11//! landing in sibling files under this module. Each of those endpoints holds a
12//! [`dpop::DpopVerifier`] and calls [`dpop::DpopVerifier::verify`] once per
13//! request.
14
15pub mod authorize;
16pub mod browser;
17pub mod client_auth;
18pub mod client_metadata;
19pub mod consent;
20pub mod decision;
21pub mod discovery;
22pub mod dpop;
23pub mod dpop_seam;
24pub mod error;
25pub mod par;
26pub mod permission_set;
27pub mod token;
28
29pub use didbot_scope as scope;
30
31use std::sync::Arc;
32
33use time::Duration;
34
35/// Every collaborator the OAuth authorization server routes need, bundled so
36/// [`crate::routes::AuthState`] carries one field instead of a dozen.
37///
38/// [`OAuthState::default`] is what every router this crate builds gets
39/// unless a deployment wires its own: in-memory stores, plus the two things
40/// that judge a sign-in — [`OAuthState::grant_gate`] and
41/// [`OAuthState::scope_policy`] — with no denial loaded. A policy denies and
42/// an empty policy set permits everything (`plan/policy.md`), so the default
43/// grants every requested scope inside [`authorize::GrantAnyScope`]'s
44/// ceiling; [`consent`]'s identity check is what stands between a client and
45/// a token once the record says `allow`. Both are read at
46/// `POST /oauth/par`, and the gate once more before `POST /oauth/token`
47/// issues anything. DPoP verification is the one hook this crate
48/// does own end to end (RFC 9449 is not a deployment policy choice the way
49/// a scope ceiling is), so [`OAuthState::default`] wires the real
50/// [`dpop::DpopVerifier`] rather than [`dpop_seam::RefusingDpopVerifier`] —
51/// see `DPOP_SKEW` (this module, private) for the window it is built with.
52#[derive(Clone)]
53pub struct OAuthState {
54 /// Fetches client metadata documents on a deployment that reaches its
55 /// own machine (`client_metadata::LoopbackClients::Fetched`). Every other
56 /// deployment fetches each one through a client pinned to the addresses
57 /// it checked; see `oauth::client_metadata::resolve_client`. Nothing else
58 /// in this crate shares it; `crate::relay` and `crate::operator_poll`
59 /// each build their own. [`OAuthState::default`] builds it with
60 /// [`client_metadata_http_client`]'s bounds, but a deployment may wire
61 /// its own, so `resolve_client` applies the per-request bounds it needs
62 /// regardless of what arrives here.
63 pub http: reqwest::Client,
64 /// Where fetched client metadata documents are cached; see
65 /// `oauth::client_metadata`.
66 pub client_cache: Arc<dyn client_metadata::ClientMetadataCache>,
67 /// Where pushed authorization requests live between `par` and
68 /// `authorize`; see `oauth::par`.
69 pub par_store: Arc<dyn par::ParStore>,
70 /// The deployment's scope ceiling, intersected with what a client asks
71 /// for at PAR; see `oauth::authorize`.
72 pub scope_policy: Arc<dyn authorize::ScopePolicy>,
73 /// Where pending consent decisions live; see `oauth::consent`.
74 pub consent_store: Arc<dyn consent::ConsentStore>,
75 /// Where the decision records PAR mints live; see `oauth::decision`.
76 pub decision_store: Arc<dyn decision::DecisionStore>,
77 /// The browsers signed in as an account, and the tickets that sign one
78 /// in; see `oauth::browser`.
79 pub browser_sessions: Arc<browser::BrowserSessions>,
80 /// The stop this process serves until, so
81 /// `bot.did.listPendingAuthorizations`'s long poll comes back at once
82 /// rather than holding a connection open for the rest of its window.
83 /// A poll that begins after the stop does not wait at all.
84 ///
85 /// Only the long poll waits on anything here; every other route answers
86 /// and returns. The default never stops; `didbot-pds` passes the stop
87 /// its listener closes on.
88 pub shutdown: crate::Stopping,
89 /// `plan/policy.md`'s gate, asked about a `Subject::Grant` at PAR and a
90 /// `Subject::Token` at `POST /oauth/token`. A deployment wires the same
91 /// gate its writes are judged by. Denies only, and
92 /// [`OAuthState::default`] wires the gate with nothing loaded. A client
93 /// this deployment will not admit is denied here, by a `denyClient`
94 /// policy, rather than by a check of its own.
95 pub grant_gate: Arc<dyn didbot_pds::policy::PolicyGate>,
96 /// Where a narrowed or denied grant is written down, payload-free; see
97 /// `didbot_pds::evaluation_log`.
98 pub evaluation_log: Arc<didbot_pds::evaluation_log::EvaluationLog>,
99 /// Where issued authorization codes live between consent confirmation
100 /// and exchange; see `oauth::token`.
101 pub code_store: Arc<dyn token::AuthorizationCodeStore>,
102 /// OAuth access/refresh token issuance and rotation; see
103 /// `oauth::token`.
104 pub tokens: Arc<token::OAuthTokenStore>,
105 /// The DPoP verification seam; see `oauth::dpop_seam`.
106 pub dpop: Arc<dyn dpop_seam::DpopVerifier>,
107 /// Where a confidential client's assertion is checked at `par` and
108 /// `token`, and its `jti` held; see `oauth::client_auth`.
109 pub client_assertions: Arc<client_auth::ClientAssertionVerifier>,
110 /// Where the permission set an `include:` scope names is resolved at
111 /// `par`, and cached for every account; see `oauth::permission_set`.
112 pub permission_sets: Arc<permission_set::PermissionSets>,
113 /// Rate limits `GET /oauth/authorize`; see `crate::rate_limit`.
114 pub authorize_rate_limiter: Arc<crate::rate_limit::RateLimiter>,
115 /// Rate limits `POST /oauth/par` per caller address, the same way
116 /// [`OAuthState::authorize_rate_limiter`] bounds the step after it.
117 ///
118 /// PAR is unauthenticated and does more work than `authorize` does: it
119 /// fetches a document from a host the *caller* names (see
120 /// [`client_metadata_http_client`]), then mints a decision record
121 /// addressed to an account the caller names. Both are costs an anonymous
122 /// client can spend without proving anything, which is the same argument
123 /// `crate::routes::AuthState::provision_rate_limiter` makes for the other
124 /// unauthenticated route that does real work.
125 pub par_rate_limiter: Arc<crate::rate_limit::RateLimiter>,
126 /// Rate limits `POST /oauth/token` per caller address.
127 ///
128 /// Also unauthenticated until the credential in the body checks out, and
129 /// every call costs a DPoP proof verification, an ECDSA check made
130 /// before that credential is read. Without this a caller with a fresh
131 /// key can spend those checks with proofs naming no token at all.
132 pub token_rate_limiter: Arc<crate::rate_limit::RateLimiter>,
133}
134
135/// How far a DPoP proof's `iat` may sit from this server's own clock, either
136/// direction — and, per [`dpop::DpopVerifier::new`]'s own doc, also how long
137/// a `jti` is retained in the replay cache, since the two are the same
138/// number for the reason named there. Matches [`token::ACCESS_TTL`]: a proof
139/// need only stay fresh for about as long as the token it mints or presents
140/// is good for.
141const DPOP_SKEW: Duration = Duration::minutes(5);
142
143/// How long a DPoP nonce is handed out before a fresh one replaces it.
144///
145/// The replaced nonce is accepted for as long again, so one nonce is good
146/// for two minutes at most. The atproto profile gives a nonce five.
147const DPOP_NONCE_TTL: Duration = Duration::minutes(1);
148
149/// The client `OAuthState::default` fetches client metadata documents with.
150///
151/// `POST /oauth/par` is unauthenticated, so the host this client talks to is
152/// chosen by whoever called it. Two bounds are set here, at the client, as
153/// well as per-request in `client_metadata::resolve_client`: a total deadline
154/// of [`client_metadata::FETCH_TIMEOUT`], and no redirect followed at all —
155/// `reqwest`'s default is to follow up to ten, and a client metadata document
156/// that arrives from anywhere but its own `client_id` is not that client's
157/// registration (see `client_metadata::ClientMetadataError::Redirected`).
158/// Refusing here means the second request is never issued; the check in
159/// `resolve_client` is what still holds if a deployment supplies its own
160/// client.
161///
162/// Falls back to a default-configured client if the builder fails — the same
163/// fallback `crate::relay::RelayClient::new` makes, for the same reason: a
164/// startup path that refuses to construct is worse than a degraded one, and
165/// `resolve_client`'s own bounds survive the fallback.
166pub fn client_metadata_http_client() -> reqwest::Client {
167 didbot_http::builder()
168 .timeout(client_metadata::FETCH_TIMEOUT)
169 .redirect(reqwest::redirect::Policy::none())
170 .build()
171 .unwrap_or_else(|error| {
172 tracing::warn!(
173 %error,
174 "building the client metadata HTTP client failed; falling back to reqwest's own defaults, which follow redirects and set no deadline — `client_metadata::resolve_client`'s own bounds still hold"
175 );
176 reqwest::Client::default()
177 })
178}
179
180impl Default for OAuthState {
181 fn default() -> Self {
182 Self {
183 http: client_metadata_http_client(),
184 client_cache: Arc::new(client_metadata::MemoryClientMetadataCache::new()),
185 par_store: Arc::new(par::MemoryParStore::new()),
186 scope_policy: Arc::new(authorize::GrantAnyScope),
187 consent_store: Arc::new(consent::MemoryConsentStore::new()),
188 decision_store: Arc::new(decision::MemoryDecisionStore::new()),
189 browser_sessions: Arc::new(browser::BrowserSessions::default()),
190 shutdown: crate::Stopping::never(),
191 grant_gate: Arc::new(didbot_pds::policy::NoPolicyGate),
192 evaluation_log: Arc::new(didbot_pds::evaluation_log::EvaluationLog::disabled()),
193 code_store: Arc::new(token::MemoryAuthorizationCodeStore::new()),
194 tokens: Arc::new(token::OAuthTokenStore::new()),
195 dpop: Arc::new(dpop::DpopVerifier::new(DPOP_SKEW, DPOP_NONCE_TTL, true)),
196 client_assertions: Arc::new(client_auth::ClientAssertionVerifier::new()),
197 permission_sets: Arc::new(permission_set::PermissionSets::default()),
198 authorize_rate_limiter: Arc::new(crate::rate_limit::RateLimiter::default()),
199 par_rate_limiter: Arc::new(crate::rate_limit::RateLimiter::default()),
200 token_rate_limiter: Arc::new(crate::rate_limit::RateLimiter::default()),
201 }
202 }
203}