Skip to main content

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}