oauth_as/store.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! The storage seam. This crate never assumes what the host's persistence looks like: the host
5//! implements [`Storage`], and the server only ever talks through it. [`MemoryStorage`] is the
6//! reference implementation, used by this crate's tests and suitable for single-process embedding.
7//!
8//! CONTRACT NOTES the server relies on:
9//!
10//! - `take_*` operations are ATOMIC remove-and-return. They are how single-use artifacts (device
11//! codes at redemption, rotating refresh tokens, RFC 9126 pushed authorization request handles)
12//! stay single use under concurrency. A shared
13//! multi-node store must implement them with a genuinely atomic primitive (compare-and-set,
14//! `DELETE ... RETURNING`, or equivalent); a plain read-then-delete reintroduces the double-spend.
15//! - PURE READS hand back `Arc<T>`, and `take_*` hand back owned `T`. The split is deliberate and
16//! it is NOT a weakening of the atomicity contract above. A read is a question about a record
17//! that STAYS in the store, so the answer can be a second pointer to it; a `take_*` REMOVES the
18//! record, so there is nothing left for a shared pointer to be shared with, and handing back an
19//! owned value is what makes "exactly one caller got it" expressible in the type. A host must not
20//! read that asymmetry as "reads are cheap so they may be stale": an `Arc` this crate holds is a
21//! snapshot of the record as of the read, exactly as the previous owned clone was.
22//! MEASURED, with the counting allocator in `tests/allocation.rs`: `get_client` returning an
23//! owned `Client` cost 8 allocations per authenticated call against `MemoryStorage` (auth,
24//! grant types, redirect URIs, scope sets, name), and every token-plane request pays it. A store
25//! that already holds `Arc<Client>` now pays one atomic increment instead. A SQL-backed store
26//! that builds the `Client` per query pays ONE extra allocation for the `Arc` itself, on a path
27//! that has already done I/O.
28//! - `put_device_grant` upserts by `device_code` and must keep any user-code index consistent.
29//! "Consistent" has two halves, and both are load bearing: a put that CHANGES a grant's user
30//! code must retire the old index entry, and a put whose user code is already indexed for a
31//! DIFFERENT `device_code` must be REFUSED rather than repointing the index. See
32//! [`Storage::put_device_grant`].
33//! - User-code lookups are by NORMALIZED code (see [`crate::device::normalize_user_code`]), and
34//! the split of responsibility is exact rather than approximate, because an earlier version of
35//! this sentence got it wrong in a way that contradicted both bundled stores AND the conformance
36//! harness. A store NORMALIZES THE CODE IT IS GIVEN ON THE WAY IN, so that the index is keyed by
37//! the normalized form however the grant spells its `user_code`; it does NOT normalize the
38//! QUERY, so [`Storage::find_device_grant_by_user_code`] is a lookup of the exact key it is
39//! handed. The server normalizes before it ever calls in (RFC 8628 section 6.1), so a store that
40//! also normalized the query would make the display form "WDJB-MJHT" and the key "WDJBMJHT" two
41//! spellings of one entry, and the display form is precisely the input an attacker controls.
42//! [`crate::storage_conformance`]'s `user_code_index/store_does_not_normalize` check holds a
43//! store to BOTH halves: it plants a grant whose `user_code` is "WDJB-MJHT" and requires
44//! `find_device_grant_by_user_code("WDJBMJHT")` to resolve it and the two unnormalized spellings
45//! to miss.
46//! - `claim_replay_id` is an ATOMIC claim-if-absent, and it is what makes RFC 7523 client
47//! assertions and RFC 9449 DPoP proofs single use. A store that implements it as "look, then
48//! insert" has reintroduced exactly the replay the two RFCs require to be prevented, and unlike
49//! the `take_*` operations the damage is silent: nothing else in the system notices.
50//! - A WRITE MUST NOT RESURRECT STATE THAT A REVOCATION REMOVED. This is the rule the whole
51//! [`RevocationBarrier`] machinery below exists to enforce, and it is stated here rather than on
52//! one method because it is a property of the STORE, not of any single call. Every revocation in
53//! this trait removes records that some concurrent request may already be holding, mid
54//! read-modify-write, and every one of those requests ends in a write. Without a rule, the last
55//! writer wins, and the last writer is the one that was told to stop.
56//!
57//! There are exactly TWO shapes of evidence a write can be judged against, and the trait uses
58//! both because neither covers the other:
59//!
60//! 1. Where the revocation leaves DURABLE ABSENCE, absence is the evidence, and the write states
61//! what it believed the store held: [`Storage::compare_and_swap_client`],
62//! [`Storage::compare_and_swap_consent`], [`Storage::compare_and_swap_device_grant`]. A
63//! deleted record fails the comparison and the write does not happen.
64//! 2. Where the writer ITSELF removed the record, absence is the normal case and proves nothing:
65//! a rotation that took a refresh token cannot tell "I took this" from "a revocation took
66//! this". There the evidence is a [`RevocationBarrier`], recorded BY the revocation and
67//! consulted BY the write. [`Storage::put_token`] and [`Storage::put_refresh_token`] answer
68//! [`WriteOutcome::RefusedRevoked`] rather than writing.
69//!
70//! A host implementing this trait owes both. [`crate::storage_conformance`] checks both.
71//!
72//! FOUR WRITES ARE EXEMPT, and they are named here rather than left to be discovered:
73//!
74//! 1. [`Storage::put_authorization_code`], because refusing it would disarm replay detection at
75//! the moment a grant is being revoked. Its own doc gives the argument and states exactly
76//! what the exemption leaves behind, which is a row rather than a capability.
77//! 2. [`Storage::put_device_grant`], because the record it writes CANNOT be one a revocation
78//! removed. Both cascades reach device grants, so the method belongs on this list rather than
79//! being passed over in silence — but the only caller in this crate is the RFC 8628 section
80//! 3.1 device authorization endpoint, which MINTS a grant under a freshly drawn random
81//! `device_code` and never puts back a record it took. A grant that did not exist when the
82//! cascade ran is a grant established after the revocation, which is exactly what the
83//! `Client` and `Consent` scopes are documented to ADMIT (see [`RevocationWindow`]), so a
84//! barrier consulted here could only ever answer "write it", and the `TokenFamily` scope
85//! cannot reach a record that carries no `family_id`. The exemption is therefore about what
86//! the record IS, not about what refusing would cost. A host whose own code puts a device
87//! grant back after taking one has left that argument behind and owes the check itself.
88//! 3. [`Storage::put_client`], because it is PROVISIONING and not a put-back.
89//! [`Storage::delete_client`] removes the client row, so this method's record kind is one a
90//! cascade reaches and it belongs on this list rather than being passed over. The argument is
91//! the one [`RevocationWindow`] already makes: a `Client` barrier covers a write only when the
92//! grant behind it was established at or before `recorded_at`, exactly so that a host may
93//! re-provision a `client_id` it deleted, and a provisioning write establishes the
94//! registration NOW. A barrier consulted here could only ever answer "write it". The crate's
95//! two callers are both provisioning: RFC 7591 section 3.2 dynamic registration, which mints a
96//! `client_id` this store has never seen, and
97//! [`crate::server::AuthorizationServer::register_client`], which is a host stating its own
98//! configuration.
99//!
100//! THE DANGER IS REAL BUT IT IS NOT THIS METHOD, and the distinction is the whole exemption: an
101//! RFC 7592 section 2.2 read-modify-write that ENDS in `put_client` undoes a `delete_client`
102//! that landed in between, restoring the registration with its old credential. That is the
103//! resurrection, and the answer to it is [`Storage::compare_and_swap_client`], which this crate
104//! uses and which `put_client`'s own doc directs a host to in capitals. A host that reaches for
105//! `put_client` at the end of a read-modify-write has left this argument behind.
106//! 4. [`Storage::put_consent`], for the same reason in the same shape, and it is the one on this
107//! list with NO caller in this crate at all: consent records are written by the HOST, from its
108//! own approval UI. Both cascades reach consents ([`Storage::delete_client`] at the client
109//! scope, [`Storage::revoke_consent`] at the consent scope), so it belongs here. The record it
110//! writes is a decision the resource owner has JUST made, and `Consent` barriers are
111//! established-at-or-before comparisons for precisely that case: [`RevocationWindow`] documents
112//! that a user who withdraws an application and approves it again has made a new decision, and
113//! admitting it is the intent rather than a gap. So a barrier here could only answer "write
114//! it". Updating an EXISTING consent is the read-modify-write, and it has the same answer:
115//! [`Storage::compare_and_swap_consent`], whose `expected: Option<&ConsentRecord>` exists to
116//! carry it.
117//!
118//! THAT LIST WAS WRONG WHEN 0.9.1 FIRST CLAIMED IT, and the correction is worth more than the
119//! defect was. `put_pushed_authorization_request` was a SEVENTH site: it is written back by the
120//! cross-client refusal in `validate_pushed_authorization_request`, which must TAKE the record
121//! before it can read the `client_id` bound into it, so a `delete_client` landing in that window
122//! cascades nothing and the put-back restored a handle belonging to a deleted client. It was
123//! neither protected nor exempted, because the enumeration was asserted rather than derived. It
124//! consults the barrier now, and [`crate::storage_conformance`] holds a host to that with
125//! `revocation_barrier/refuses_put_pushed_authorization_request`.
126//!
127//! The count was ALSO wrong in the other direction, and the second error is the instructive one:
128//! the doc claimed the exemption count was "one, by construction" while `put_device_grant`,
129//! which no cascade spares, was named nowhere. Nothing was broken by that — the argument above
130//! holds — but an enumeration that omits a site because the site is harmless is an enumeration
131//! nobody can check, which is the same defect as the consent and PAR kinds missing from
132//! `delete_client`'s cascade list.
133//!
134//! AND IT WAS STILL SHORT AFTER THAT CORRECTION, by two: entries 3 and 4 above, `put_client` and
135//! `put_consent`, were absent from the list while a cascade in this trait removed the kind each
136//! one writes. Both were in fact safe, for the arguments now written beside them, which is exactly
137//! what made the omission the same mistake as `put_device_grant`'s: the list was derived by
138//! noticing harm rather than by reading the cascades. That is the THIRD time this enumeration has
139//! been found short by reading, and it is why it is no longer only prose:
140//! `tests/storage_cascade_definitions.rs`'s
141//! `every_storage_write_either_consults_the_barrier_or_is_an_argued_exemption` scans this file and
142//! requires every `fn put_*` on the trait to EITHER return [`WriteOutcome`], which is how a method
143//! says it consults the predicate, OR be named in the block above. It cannot judge an argument; it
144//! can insist one is written where a host reads, and that is the half that kept failing. A fifth
145//! write added without either fails the build.
146//!
147//! THE RULE FOR ANYONE ADDING A METHOD: a write on a record that any revocation cascade removes
148//! must consult this predicate or be listed above with its argument. Deriving that list means
149//! reading every cascade in this trait and asking what it removes, not counting the call sites
150//! that already do the right thing.
151//! - SWEEPING IS THE HOST'S JOB AND IT IS NOT OPTIONAL. Nothing in this crate evicts anything on
152//! a timer: there is no background task, by design. Expired records are reclaimed only when the
153//! HOST calls [`Storage::sweep_expired`] on a schedule of its own. A host that never calls it
154//! has not merely an untidy store: the RFC 8628 section 3.1 device authorization endpoint takes
155//! no credential from a public client, so an unswept deployment hands anyone who can open a
156//! socket an unbounded allocation loop. See [`Storage::sweep_expired`] for the obligation in
157//! full, and `examples/production_server.rs` for it wired up.
158
159use std::collections::HashMap;
160use std::fmt;
161use std::future::Future;
162use std::sync::{Arc, Mutex};
163
164use crate::authorization::AuthorizationCodeRecord;
165use crate::client::{Client, ClientId};
166use crate::device::{DeviceGrant, DeviceGrantState};
167use crate::token::{IssuedToken, RefreshTokenRecord};
168
169/// An opaque host-side storage failure. The server maps these to `server_error` on wire paths;
170/// the text is for the host's logs, never for the wire.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct StorageError(pub String);
173
174impl StorageError {
175 /// Wrap a failure description.
176 pub fn new(msg: impl Into<String>) -> Self {
177 StorageError(msg.into())
178 }
179}
180
181impl fmt::Display for StorageError {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 write!(f, "storage error: {}", self.0)
184 }
185}
186
187impl std::error::Error for StorageError {}
188
189/// What a revocation removed, in the terms a later write can be judged against.
190///
191/// A barrier is not a record and it is not a tombstone for one key. It names a SET of records, so
192/// that a write which was already in flight when the revocation ran can be refused without the
193/// store having to have seen that particular record at the time. That is the whole difficulty: the
194/// record a rotation is about to write back does not exist yet when the revocation runs, so there
195/// is nothing to delete and nothing to compare against.
196///
197/// The three variants are the three granularities at which this crate revokes, and they are not
198/// interchangeable. A family is one refresh chain; a consent is every family one client ever
199/// obtained for one user; a client is everything a registration ever held. A write is refused if
200/// ANY recorded barrier covers it.
201///
202/// # Covering a write is not the same as naming its identity
203///
204/// A barrier names an identity, and two of these three identities can legitimately be established
205/// AGAIN: a user who withdraws an application and approves it again has made a new decision, and a
206/// host may re-provision a `client_id` it deleted. So `Client` and `Consent` cover a write only
207/// when the GRANT behind it was established at or before [`RevocationWindow::recorded_at`].
208///
209/// `TokenFamily` covers UNCONDITIONALLY, and the asymmetry is deliberate. Rotation legitimately
210/// mints fresh records inside an EXISTING family, so comparing there would admit precisely the
211/// write RFC 9700 section 4.14.2 containment exists to refuse — the rotation that completes after
212/// the cascade. Nothing legitimate is lost by refusing always, because a new grant gets a new
213/// `family_id`.
214///
215/// The instant compared against is the GRANT's, never the write's. A rotation and a code
216/// redemption both write at `now`, so `now` cannot tell a grant that predates a revocation from
217/// one made after it: comparing the write's own instant would make every barrier either useless
218/// (family) or permanent (consent). See [`crate::token::IssuedToken::grant_established_at`].
219///
220/// This was got WRONG in 0.9.1 before the audit: barriers refused on identity alone, so a user who
221/// re-approved an application held a live consent record and could not obtain a token from it for
222/// as long as a refresh token lives. The refusal tests all passed, because refusing MORE is not
223/// something a test asking "did it refuse?" can see.
224///
225/// # Every scope is a NON-EMPTY identifier, and that is a requirement on the store
226///
227/// The empty string is not an identity. A store that keys barriers by value has to distinguish
228/// "no family" from "a family whose id happens to be empty", and every scheme for doing that
229/// either collides or needs a nullable column with the awkward equality semantics that follow. So
230/// [`Storage::delete_client`], [`Storage::revoke_token_family`] and [`Storage::revoke_consent`]
231/// REFUSE an empty scope with a [`StorageError`], and refuse it BEFORE removing anything, rather
232/// than one store accepting it and another rejecting it. That divergence is worse than either
233/// behaviour on its own, and it is not hypothetical: it is what the two bundled stores did, and it
234/// was found only by running the same call through both.
235///
236/// # Every barrier has a deadline, and it is not optional
237///
238/// A barrier only has to outlive the writes that could resurrect what it removed, and those are
239/// bounded: a request holding a record is holding it across at most one issuance. Keeping barriers
240/// forever would turn every revocation into permanent storage, on a path an ordinary user drives
241/// by clicking "log out", so [`Storage::sweep_expired`] reclaims them like anything else. Callers
242/// in this crate derive the deadline from the longest-lived thing the revocation removed, never
243/// from a policy of their own: see [`crate::server::AuthorizationServer`]'s revocation paths.
244/// # Deliberately NOT `#[non_exhaustive]`
245///
246/// This crate marks its feature-varying public types (see `tests/host_api_shape.rs`, which gates
247/// the rule), and this one is not feature varying: all three variants exist in every build, and
248/// only the writing of a `Consent` barrier is gated. The rule therefore does not reach it, and the
249/// SAFETY argument runs the other way.
250///
251/// A host implements the refusal predicate by MATCHING this enum. `#[non_exhaustive]` would force
252/// a wildcard arm into every one of those matches, and the only sensible thing a wildcard can
253/// return is `false`, which is "not revoked". A variant added later would then be silently ignored
254/// by every existing host store: a new revocation scope that refuses nothing, failing OPEN, with
255/// no diagnostic anywhere. Leaving the enum exhaustive makes that same change a COMPILE ERROR at
256/// the exact place that has to be updated, which is what this crate wants from a security
257/// predicate and is the same argument [`Storage`] makes for having no default method bodies.
258/// `Hash` is derived even though [`MemoryStorage`] does not key a map by this value (it keys by
259/// the identifier the barrier NAMES, so that the lookup can be probed with a `&str` the caller
260/// already holds rather than with a key it would have to build): a host whose own store wants a
261/// `HashMap<RevocationBarrier, _>` should be able to have one, and a derive costs this library
262/// nothing until something instantiates it.
263
264#[derive(Debug, Clone, PartialEq, Eq, Hash)]
265pub enum RevocationBarrier {
266 /// Everything a client registration was ever issued (RFC 7592 section 2.3 deletion).
267 Client(ClientId),
268 /// One refresh chain and every token minted along it (RFC 9700 section 4.14.2 reuse
269 /// detection, and RFC 7009 section 2.1's cascade from a revoked refresh token).
270 TokenFamily(Box<str>),
271 /// Everything one client ever obtained for one resource owner (consent withdrawal).
272 Consent {
273 /// The client the withdrawn consent named.
274 client_id: ClientId,
275 /// The resource owner who withdrew it.
276 subject: Box<str>,
277 },
278}
279
280/// A [`RevocationBarrier`] scope must be a NON-EMPTY identifier, and this is where that is
281/// enforced rather than assumed.
282///
283/// A barrier names an identity. The empty string is not one: a store that keys barriers by value
284/// has to distinguish "no family" from "a family whose id happens to be empty", and every scheme
285/// for doing that either collides or needs a nullable column with the awkward equality semantics
286/// that follow. `PostgresStorage` resolves it with a `''` sentinel plus CHECK constraints, which
287/// makes an empty identifier a hard error at the database.
288///
289/// So the contract is that an empty identifier is REFUSED, by every store, rather than accepted by
290/// one and rejected by another. That divergence is worse than either behaviour on its own: it was
291/// found by comparing the two backends, where `delete_client("")` cascaded everything in memory
292/// and — because the barrier insert runs first — deleted NOTHING through Postgres while returning
293/// an error.
294pub(crate) fn reject_empty_scope(what: &str, value: &str) -> Result<(), StorageError> {
295 if value.is_empty() {
296 return Err(StorageError::new(format!(
297 "a revocation needs a non-empty {what}; the empty string does not name an identity a \
298 barrier can be recorded for"
299 )));
300 }
301 Ok(())
302}
303
304/// WHEN a revocation happened, and how long its barrier stands.
305///
306/// The two instants answer different questions and a store needs both, which is why they travel
307/// together in one value rather than as two `SystemTime` parameters: both have the same type, so a
308/// positional pair could be passed the wrong way round and still compile, and the failure would be
309/// a barrier that refuses nothing (`recorded_at` far in the future is compared against by every
310/// write) or one that never expires. Naming them makes that mistake impossible to write.
311///
312/// - `recorded_at` is the instant the revocation was made. A write is refused only when the GRANT
313/// behind it was established at or before this instant, for the two scopes where an identity can
314/// legitimately be established again. See [`RevocationBarrier`].
315/// - `until` is the instant the barrier may be reaped, and is read only by
316/// [`Storage::sweep_expired`]. It must be at least as far out as the longest-lived record the
317/// revocation was entitled to kill.
318///
319/// Both are supplied by the caller rather than taken from the store's own clock, because only the
320/// caller knows the configured lifetimes, and because a store that read the wall clock could not be
321/// driven deterministically by a test.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub struct RevocationWindow {
324 /// The instant the revocation was made.
325 pub recorded_at: std::time::SystemTime,
326 /// The instant the barrier may be reaped.
327 pub until: std::time::SystemTime,
328}
329
330/// Whether a write happened, or was refused because a revocation covers it.
331///
332/// `#[must_use]`: a caller that ignores this has written exactly the bug this type exists to make
333/// impossible, and it is the kind of bug that is invisible until somebody's revocation quietly
334/// stops working. The server treats [`WriteOutcome::RefusedRevoked`] as a signal to UNDO the
335/// issuance it was in the middle of, not as an error to report.
336/// Exhaustive, for the same reason [`RevocationBarrier`] is: it does not vary with a feature, and
337/// a host matching it wants a third variant to be a compile error rather than a wildcard arm.
338#[derive(Debug, Clone, Copy, PartialEq, Eq)]
339#[must_use = "a refused write means the record was NOT stored; the caller must undo what it was issuing"]
340pub enum WriteOutcome {
341 /// The record is in the store.
342 Applied,
343 /// A [`RevocationBarrier`] covers this record, so it was NOT written. Whatever the caller was
344 /// in the middle of issuing has been revoked underneath it.
345 RefusedRevoked,
346}
347
348impl WriteOutcome {
349 /// True when the record was actually stored.
350 pub fn is_applied(self) -> bool {
351 matches!(self, WriteOutcome::Applied)
352 }
353
354 /// True when a revocation refused the write.
355 pub fn is_refused(self) -> bool {
356 matches!(self, WriteOutcome::RefusedRevoked)
357 }
358}
359
360/// What the authorization server needs from the host's persistence. All futures are `Send` so the
361/// server can be driven from any multi-threaded async runtime.
362///
363/// # Why every method is required, and none has a default
364///
365/// It is 29 methods at `--all-features` and not one of them has a default body. That is a decision,
366/// not an omission, and the first thing to say about it is the honest arithmetic: 9 of the 29 are
367/// feature gated (6 behind `consent`, 2 behind `par`, and `claim_replay_id` behind
368/// `client-assertion` or `dpop`), all features are off by default, so a default-features host
369/// implements 20.
370///
371/// 20 IS NOT A NUMBER THE HOST CHOOSES, and this has to be said here because the arithmetic above
372/// reads as though it were. Cargo unifies features across a dependency GRAPH, not per dependent: if
373/// anything anywhere in the host's tree enables `consent`, the host's own build of `oauth-as` has
374/// `consent`, this trait grows six methods, and the host's `impl Storage` stops compiling. They did
375/// not ask for it and they cannot prevent it. `tests/host_api_shape.rs` states the same hazard for
376/// public types, where the answer is `#[non_exhaustive]`; there is no such attribute for a trait,
377/// and there is no defaulting the six without reintroducing precisely the "accepts a write and keeps
378/// nothing" failure the paragraph below is about.
379///
380/// So a compile error is the RIGHT outcome and not a defect: the six methods appeared because the
381/// build now has a consent-aware authorization server in it, and a store that silently did nothing
382/// with consent records would be worse than one that will not build. What a host owes itself is
383/// planning for it: [`crate::delegate_storage`] forwards whichever of the 29 the build actually has,
384/// tracking the feature set rather than a fixed list, and it is the intended answer to this exact
385/// event. (It could not be, until 0.9.1: the macro gated its nine on the HOST crate's features
386/// instead of this one's, so it generated 20 forwarders no matter what. See the macro's docs.)
387///
388/// The rest is one argument. THERE IS NO METHOD HERE WHOSE OBVIOUS DEFAULT IS SAFE. A defaulted
389/// `put_refresh_token` that does nothing is a server issuing refresh tokens nobody can redeem. A
390/// defaulted [`Storage::revoke_token_family`] answering `Ok(0)` is a revocation that reports
391/// success and revokes nothing, on the RFC 9700 section 4.14.2 path that runs only on evidence of
392/// compromise. A defaulted `take_*` answering `Ok(None)` turns every redemption into a silent
393/// `invalid_grant`. A defaulted [`Storage::sweep_expired`] answering `Ok(0)` is precisely the
394/// memory exhaustion path that method's own docs are about. Every one of those compiles, passes a
395/// smoke test, and fails in production in the direction that loses credentials.
396///
397/// That matters more here than it would in a general-purpose trait. This crate has already found
398/// SIX separate places where a write silently undid a revocation, and each one took a test written
399/// specifically to catch it. A default that accepts a write and keeps nothing is that same defect
400/// shipped in the trait itself, with this crate's name on it, in the one place no host would think
401/// to test. So the compile error is the FEATURE: it is this crate saying "you have not implemented
402/// revocation yet" at build time, rather than at three in the morning.
403///
404/// What the trait owes an adopter instead of defaults is a way not to start from nothing:
405///
406/// - [`MemoryStorage`] is `pub`, not test-gated, exactly so it can be the on-ramp. It is a complete
407/// implementation of all 29, it is the store this crate's own tests run on, and for a
408/// single-process deployment it is an answer rather than a placeholder.
409/// - The mix-and-match case (clients in Postgres, codes in memory) is answered by DELEGATION rather
410/// than by defaults: a wrapper that forwards the methods it does not specialise. Forwarding is
411/// mechanical, so [`crate::delegate_storage`] writes it.
412/// - [`crate::storage_conformance`] (feature `test-util`) is how a host finds out its
413/// implementation is wrong before its users do. Write the 20, run the harness, read what it says.
414///
415/// # MUST NOT PANIC. Return a [`StorageError`] instead
416///
417/// Every method here MUST return `Err(StorageError)` for every failure it can have, including the
418/// ones a host would ordinarily assert on: a connection that is gone, a row that does not
419/// deserialize, a slice index that is out of range, a `Mutex` poisoned by an earlier panic. This
420/// crate catches no unwind anywhere on a request path, so a panic in a store is not caught, logged
421/// and turned into `server_error`. It unwinds through whatever the server was in the middle of.
422///
423/// NAMING THE CONSEQUENCE, because a rule with no consequence attached is one a host talks itself
424/// out of. Redemption is a TAKE followed by a WRITE, and the two are separate calls into this
425/// trait by construction: the atomicity the `take_*` methods promise is per call, not across the
426/// pair. A panic between them leaves byte for byte what a dropped future leaves.
427///
428/// - [`Storage::take_refresh_token`] has removed the record, and the spent marker that
429/// [`Storage::put_refresh_token`] was about to write is never written. RFC 9700 section 4.14.2
430/// reuse detection for that chain is now disarmed: the chain is gone, so a later presentation of
431/// the old token is an ordinary unknown token rather than the evidence of compromise it is.
432/// - [`Storage::take_authorization_code`] has removed the code, and the consumed record
433/// [`Storage::put_authorization_code`] was about to write back is never written. RFC 6749
434/// section 4.1.2 replay detection for that code is off permanently, because the alarm is the
435/// record.
436///
437/// Neither of those is a crash a host sees. Both are a quiet loss of a detection this server is
438/// relied on for, on the paths that only matter when something has already gone wrong. So: no
439/// `unwrap`, no `expect`, no indexing, no `panic!`, in any of the 29. [`MemoryStorage`] holds
440/// itself to this even where it would be entitled not to, which is why it recovers from a poisoned
441/// mutex rather than propagating the panic (see `MemoryStorage::lock`).
442///
443/// The same clause is on [`crate::client::SecretVerifier`] and [`crate::events::RateLimiter`], for
444/// the same reason and with the same absence of a net.
445///
446/// # Transient contention is the store's to resolve, not the caller's
447///
448/// This applies to the `take_*` operations and to the `compare_and_swap_*` operations, which are
449/// the only methods here that two requests can legitimately reach for the same record at the same
450/// instant.
451///
452/// A [`StorageError`] is NOT how a store says "somebody else got there first". Those methods
453/// already have a word for that and it is not an error: `take_*` answers `Ok(None)` and
454/// `compare_and_swap_*` answers `Ok(false)`, and the server knows what to do with both. A
455/// `StorageError` means something the caller cannot act on, and it is mapped to `server_error` on
456/// every wire path, so surfacing an ordinary overlap as one fails a legitimate redemption for a
457/// reason the client cannot fix and cannot understand.
458///
459/// THIS IS A REQUIREMENT ON THE STORE BECAUSE ONLY THE STORE CAN MEET IT. A backend using
460/// optimistic concurrency will see conflicts under exactly the concurrency these operations are
461/// FOR: PostgreSQL at `SERIALIZABLE` raises `40001`, CockroachDB the same, etcd answers a
462/// compare-and-swap mismatch, and a compare-and-swap loop over any of them observes a version that
463/// moved. Every one of those is transient by definition, and the caller has no way to tell it apart
464/// from a dead connection. So the store retries internally, and answers with the outcome the retry
465/// settled on.
466///
467/// What is NOT required is retrying forever. A bounded retry that gives up is a store saying the
468/// contention did not clear, and a [`StorageError`] is the right answer to that: the distinction
469/// this rule draws is between a conflict that has been RESOLVED (answer `Ok`) and one that has
470/// not, never between an error and a slow path.
471///
472/// [`crate::storage_conformance`] holds a store to this: its race checks fail a store whose
473/// concurrent `take_*` or `compare_and_swap_*` calls return [`StorageError`], and they cite this
474/// rule by name when they do, because a harness that fails a store on a rule the trait never
475/// stated is a harness the store cannot argue with.
476pub trait Storage: Send + Sync {
477 /// Look up a registered client.
478 ///
479 /// `Arc` rather than an owned `Client` because this is the single most called read in the
480 /// crate: every authenticated request on the token plane starts here, and the record is only
481 /// ever READ. A store that keeps its clients as `Arc<Client>` answers with a pointer clone; a
482 /// store that materialises one per query wraps what it built. See the module docs for the
483 /// measurement and for why this does not touch the `take_*` atomicity contract.
484 ///
485 /// # `client_id` IS ATTACKER-CHOSEN, AND THIS CRATE VALIDATES NOTHING ABOUT IT
486 ///
487 /// [`ClientId::new`] wraps a `String` and checks nothing, deliberately: RFC 6749 section 2.2
488 /// leaves the identifier's syntax to the server, and a host provisioning its own clients names
489 /// them whatever its own scheme names them. What that means HERE is that the value arriving in
490 /// this method is a string an unauthenticated stranger picked, on several routes at once:
491 ///
492 /// - `GET /authorize?client_id=...`, straight out of the query, filtered only for empty.
493 /// - `POST /token`, out of the form body or an RFC 7617 `Authorization` header.
494 /// - `GET`, `PUT` and `DELETE {registration_endpoint}/{client_id}` (RFC 7592), out of ONE
495 /// percent-decoded path segment. The router matches the prefix on the RAW path and refuses a
496 /// raw `/`, so no request can reach an endpoint mounted underneath the registration one — but
497 /// the segment is decoded AFTER that decision, so `%2F` becomes a real `/` here, `%2E%2E`
498 /// becomes `..`, `%00` becomes a NUL, and bytes that are not UTF-8 become U+FFFD (the decode
499 /// is lossy: see `crate::http`).
500 ///
501 /// So the identifier reaching this method may contain a path separator, a dot-dot segment, a
502 /// NUL, a control character, a newline or a replacement character, and it may be up to the
503 /// length of a URL or a request body. That is not a defect being described: `get_client` is a
504 /// LOOKUP, and refusing to look a value up is not this crate's decision to make when the host's
505 /// own naming scheme is the only thing that says what an identifier may look like — a host
506 /// whose ids are RFC 9728-style HTTPS URLs has `/` in every one of them.
507 ///
508 /// WHAT THE STORE MUST DO: treat this as an opaque key and nothing else. A `HashMap`, a
509 /// parameterised SQL query and a key-value `GET` are all safe as written; `MemoryStorage` and
510 /// `oauth-as-postgres` are both in that class. A store that interpolates this into a PATH (one
511 /// file per client), into an object key, into an LDAP filter or into SQL text is the case this
512 /// paragraph exists for, and it must encode or reject the identifier ITSELF. Rejecting is
513 /// always safe: answer `Ok(None)` for an id your scheme could not have minted, which is the
514 /// truth — this crate treats that as an unknown client and refuses on the same terms it refuses
515 /// any other.
516 ///
517 /// The same rule applies to every other method on this trait that takes a host-visible
518 /// identifier; it is stated here because this is the one an unauthenticated request reaches
519 /// first and from the most routes.
520 fn get_client(
521 &self,
522 client_id: &ClientId,
523 ) -> impl Future<Output = Result<Option<Arc<Client>>, StorageError>> + Send;
524
525 /// Insert or replace a client registration.
526 ///
527 /// This is PROVISIONING: it creates a registration or replaces one outright, and it is what
528 /// [`crate::server::AuthorizationServer::register_client`] and a host provisioning its own
529 /// clients call. It is an upsert, deliberately, because re-provisioning a `client_id` the host
530 /// chose is a legitimate thing for a host to do after deleting it.
531 ///
532 /// IT IS THE WRONG METHOD FOR A READ-MODIFY-WRITE, and that is not a style note. RFC 7592
533 /// section 2.2 updates read the registration, apply a metadata document to it, and write it
534 /// back; a blind upsert at the end of that sequence UNDOES a
535 /// [`Storage::delete_client`] that landed in between, restoring the client with its old
536 /// credential and its old `registration_access_token_hash`. Deleting a compromised
537 /// registration would then be defeatable by whoever holds the stolen token. Use
538 /// [`Storage::compare_and_swap_client`] for that, and see the resurrection rule in the module
539 /// docs for why this distinction exists at all.
540 fn put_client(&self, client: Client) -> impl Future<Output = Result<(), StorageError>> + Send;
541
542 /// Replace the registration stored under `updated.client_id` with `updated`, but ONLY if the
543 /// stored record is still exactly `expected`. Answers whether the write happened.
544 ///
545 /// This is [`Storage::put_device_grant`]'s compare-and-swap sibling, for the same reason and
546 /// with the same contract: the comparison and the write MUST happen as ONE atomic step. A
547 /// store that reads, compares, and then writes separately has reintroduced precisely the
548 /// window this closes, and it will do so silently.
549 ///
550 /// `Ok(false)` for a `client_id` that is not present, and the write MUST NOT insert. Absence
551 /// is the case that matters: it is what [`Storage::delete_client`] leaves behind, and an
552 /// upsert here would put a deleted registration back. `UPDATE ... WHERE` is the shape to reach
553 /// for; `INSERT ... ON CONFLICT` is the shape that reintroduces the defect.
554 ///
555 /// Comparing the WHOLE record rather than a version column is deliberate: it costs one
556 /// equality test on a path that runs once per management request, and it closes the lost
557 /// update between two concurrent RFC 7592 updates as well as the resurrection, without asking
558 /// every host to carry a revision field it would otherwise have no use for.
559 ///
560 /// Two concurrent management requests are what this method is FOR, so a store that resolves
561 /// its own concurrency optimistically will see conflicts here as a matter of course. See the
562 /// trait's rule that contention is the store's to resolve, not the caller's: `Ok(false)` is
563 /// how a loser is told it lost, and a [`StorageError`] is not.
564 fn compare_and_swap_client(
565 &self,
566 expected: &Client,
567 updated: Client,
568 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
569
570 /// Remove a client registration AND everything it was issued, returning whether a
571 /// registration was actually removed.
572 ///
573 /// The second half is a REQUIREMENT, not a convenience, and it is why this is one operation
574 /// rather than two. RFC 7592 section 2.3 deletes a registration and invalidates what that
575 /// registration holds; a store that removed only the row would leave every access token,
576 /// refresh chain and outstanding authorization code of a deleted client live until its own
577 /// expiry, which is a client that no longer exists still calling resource servers. Doing it
578 /// here rather than in the server is what lets a real database do it in ONE transaction: a
579 /// delete that half succeeded, in either order, is either an orphaned credential set or a
580 /// registration nobody can reach.
581 ///
582 /// "Everything it was issued" is DEFINED by the list below, and the list is what a host
583 /// implements against: nothing in the type system can check a cascade, so a kind this
584 /// enumeration omits is a kind that survives every deletion in a store that was written to the
585 /// letter of it. It means, for `client_id`, every record of every one of these kinds:
586 ///
587 /// - access tokens whose `client_id` is this one
588 /// - refresh records whose `client_id` is this one
589 /// - authorization codes whose `client_id` is this one, in either state
590 /// - device grants whose `client_id` is this one, WITH their user-code index entries. An index
591 /// entry left pointing at a reaped grant makes that user code resolve to nothing.
592 /// - pushed authorization requests (present only under the `par` feature) whose `client_id` is
593 /// this one. RFC 9126 section 2.2 binds a `request_uri` to the client that pushed it, so a
594 /// deleted client's outstanding handles are handles nobody may ever redeem.
595 /// - consent records (present only under the `consent` feature) whose `client_id` is this one.
596 /// This one is the least obvious and it is NOT optional. A consent left behind names an
597 /// application that no longer exists: `consents_for_subject` lists it to the user,
598 /// who cannot meaningfully withdraw it, and because `client_id` is chosen by the HOST
599 /// ([`crate::server::AuthorizationServer::register_client`] takes whatever it is given), a
600 /// later client provisioned under the same id inherits the old user's standing approval,
601 /// with its scope and its resource set, without that user ever being asked.
602 ///
603 /// Both bundled stores delete all six. The last two were MISSING from this enumeration while
604 /// both stores removed them, which is the worst way for a contract to be wrong: it is invisible
605 /// to the `storage_conformance` harness, which can only check what this text requires, so a host
606 /// store that certified clean was leaking exactly the two kinds nothing else ever reclaims.
607 ///
608 /// Removing a client that is already gone is `Ok(false)`, not an error. THE BARRIER BELOW IS
609 /// STILL RECORDED IN THAT CASE, and it is stated here rather than left as an implementation
610 /// detail because the natural shape a host reaches for gets it wrong: `if rows_deleted > 0 {
611 /// insert_barrier(..) }` satisfies every other word of this method and silently drops the
612 /// protection in exactly the interleaving that needs it. A client deleted twice, or deleted
613 /// while a first deletion is still committing, is a client whose in-flight issuances still
614 /// have to be refused, and absence of the registration row proves nothing about them: the
615 /// issuance is holding a `Client` it read before either deletion ran.
616 ///
617 /// An EMPTY `client_id` is REFUSED with a [`StorageError`], before anything is removed: the
618 /// empty string does not name an identity a barrier can be recorded for, and a store that
619 /// accepted it here would cascade against a scope no later write can be compared to. Refusing
620 /// BEFORE the first deletion is part of the requirement, so that a refusal leaves the store
621 /// untouched rather than half cascaded — this crate found the divergence by comparing its two
622 /// backends, where `delete_client("")` cascaded everything in memory and, because the barrier
623 /// insert ran first, deleted NOTHING through Postgres while returning an error.
624 /// [`RevocationBarrier`] says the same thing about the other two scopes.
625 ///
626 /// # It MUST also record a barrier, in the same step
627 ///
628 /// [`RevocationBarrier::Client`] for this `client_id`, over `window`, recorded
629 /// ATOMICALLY with the deletions above. Without it the cascade is only as good as the moment
630 /// it ran: a token issuance already in flight for this client completes afterwards and writes
631 /// an access token and a refresh chain for a registration that no longer exists, and nothing
632 /// ever reclaims them because the client they belong to is gone. See the resurrection rule in
633 /// the module docs.
634 ///
635 /// `window` is supplied by the caller because only the caller knows both instants. Its
636 /// `until` is how long an in-flight issuance could still be holding, and this crate passes the
637 /// longest access token or refresh chain lifetime it is configured to mint. Its `recorded_at`
638 /// is when the deletion happened, and a write is refused only when the GRANT behind it was
639 /// established at or before that instant — so a `client_id` a host RE-PROVISIONS after the
640 /// deletion is served rather than locked out for the barrier's whole life. See
641 /// [`RevocationWindow`].
642 fn delete_client(
643 &self,
644 client_id: &ClientId,
645 window: RevocationWindow,
646 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
647
648 /// Insert or replace a device grant, keyed by `device_code`, maintaining the user-code index.
649 ///
650 /// Two REQUIRED behaviours beyond a plain upsert, both of which a naive "insert the new
651 /// mapping" implementation gets wrong:
652 ///
653 /// 1. If the grant's normalized user code is already indexed for a DIFFERENT `device_code`,
654 /// this MUST fail with a [`StorageError`] and write nothing. RFC 8628 section 6.1 makes the
655 /// user code the credential a human types, so two live grants answering to one code is two
656 /// devices sharing an identity. Silently repointing the index also orphans both grants: the
657 /// older one can no longer be approved, and taking it removes an index entry that now names
658 /// the newer one.
659 /// 2. If a put CHANGES the user code of an existing `device_code`, the OLD index entry MUST be
660 /// retired. Leaving it behind means the superseded code goes on resolving to the grant.
661 ///
662 /// The server relies on (1) to make its user-code generation retry loop meaningful: it asks
663 /// the store whether a code is taken, but only the store can answer that without a race.
664 ///
665 /// This write does NOT consult a [`RevocationBarrier`], and it is one of the two exemptions
666 /// the module docs enumerate rather than an omission. The reason is about the record and not
667 /// about the cost of refusing: both cascades remove device grants, but the only caller in this
668 /// crate mints a grant under a freshly drawn `device_code`, so there is no record for a
669 /// cascade to have removed and nothing for a barrier to compare against that would not answer
670 /// "write it". A host that puts a grant BACK after taking one has left that argument behind.
671 fn put_device_grant(
672 &self,
673 grant: DeviceGrant,
674 ) -> impl Future<Output = Result<(), StorageError>> + Send;
675
676 /// Look up a device grant by device code.
677 fn get_device_grant(
678 &self,
679 device_code: &str,
680 ) -> impl Future<Output = Result<Option<DeviceGrant>, StorageError>> + Send;
681
682 /// Look up a device grant by NORMALIZED user code.
683 fn find_device_grant_by_user_code(
684 &self,
685 normalized_user_code: &str,
686 ) -> impl Future<Output = Result<Option<DeviceGrant>, StorageError>> + Send;
687
688 /// Atomically remove and return a device grant. This is the single-use redemption primitive:
689 /// under concurrent redemption exactly one caller receives the grant.
690 ///
691 /// IT ALSO RETIRES THE GRANT'S USER-CODE INDEX ENTRY, in the same step. That was documented
692 /// nowhere while both bundled stores did it and [`crate::storage_conformance`] enforced it
693 /// (`user_code_index/cleared_by_take`), which is the shape of contract error this trait is
694 /// least able to survive: a host implementing the method to the letter of the words above
695 /// fails a check whose requirement appears in no sentence it was given. A store that keeps the
696 /// index as a pointer into the primary table gets this for free, because the entry resolves to
697 /// nothing once the row is gone. A store whose index is its OWN row carrying its own copy of
698 /// the grant — the ordinary Redis or DynamoDB shape — does not, and there the code a human
699 /// typed goes on resolving to a grant that has already been exchanged for a token.
700 fn take_device_grant(
701 &self,
702 device_code: &str,
703 ) -> impl Future<Output = Result<Option<DeviceGrant>, StorageError>> + Send;
704
705 /// Replace the grant stored under `updated.device_code` with `updated`, but ONLY if the stored
706 /// record's [`DeviceGrantState`] is still `expected`. Answers whether the write happened.
707 ///
708 /// # Why this exists, which is the whole of it
709 ///
710 /// Three unrelated actors write one device grant: the DEVICE polling the token endpoint (which
711 /// restamps the RFC 8628 section 3.5 pacing fields), and the USER approving or denying at the
712 /// host's verification UI. Every one of those is a read-modify-write, and with only
713 /// [`Storage::put_device_grant`] to write through, the last writer wins by accident. The
714 /// interleaving that matters is a poll whose read saw `Pending` landing its write after the
715 /// user has already said no: the blind put reverts the record to `Pending`, the verification UI
716 /// has already told the user their refusal was recorded, and nothing anywhere reports an error.
717 /// A DECISION A USER ACTUALLY MADE IS SILENTLY THROWN AWAY.
718 ///
719 /// A poll TIMESTAMP is losable (the cost is one extra `slow_down`); a decision is not. This is
720 /// the primitive that expresses the difference, and it is a compare-and-swap rather than a
721 /// narrower "write only the pacing fields" call because the verification UI needs the same
722 /// guarantee against ITSELF: two host UI actions on one user code must not clobber each other
723 /// either, and there the field being written IS the state.
724 ///
725 /// # The contract
726 ///
727 /// The comparison and the write MUST happen as ONE atomic step. A store that implements this as
728 /// a read, a comparison, and a separate write has reintroduced precisely the window it is meant
729 /// to close, and it will do so silently, exactly as the `take_*` note at the top of this module
730 /// describes. `SELECT ... FOR UPDATE`, `UPDATE ... WHERE state = $expected`, a Redis
731 /// `WATCH`/`MULTI`, or a compare-and-set on a document revision all express it directly.
732 ///
733 /// `Ok(false)` for a `device_code` that is not present. A grant that has been redeemed or swept
734 /// is gone, and a swap must never bring it back: reinstating a consumed grant would make a
735 /// single-use device code redeemable twice. In particular the write MUST NOT be an
736 /// insert-or-update: `UPDATE ... WHERE` cannot create a row and is the shape to reach for,
737 /// whereas an upsert does not fail and does not no-op against a row that has just been
738 /// redeemed, it puts the grant back.
739 ///
740 /// BOTH HALVES OF THE USER-CODE INDEX CONTRACT ON [`Storage::put_device_grant`] APPLY HERE
741 /// TOO, and they are restated rather than referred to in passing because this trait has
742 /// already watched them drift. A swap that CHANGES the grant's user code must retire the old
743 /// entry, and a swap whose user code is already indexed for a DIFFERENT `device_code` must
744 /// fail with a [`StorageError`] and write nothing — a refusal rather than `Ok(false)`, because
745 /// `Ok(false)` means "the state moved on", which a caller answers by giving up quietly, and
746 /// this is a store-level conflict the caller has to hear about. The requirement was stated on
747 /// the put alone, and the reference implementation's own doc claimed the swap DELEGATED to it;
748 /// it did not, it duplicated it, and the duplicate was missing the refusal, so a swap could
749 /// hand one user code to two grants where a put would have refused.
750 ///
751 /// # THERE IS NO DEFAULT IMPLEMENTATION, deliberately
752 ///
753 /// One was provided at first, doing the read, the comparison and the write as three separate
754 /// calls, on the reasoning that it NARROWED the window even though it could not close it. That
755 /// reasoning was wrong twice over, and the shim is gone.
756 ///
757 /// It was wrong about the window, because narrowing it was not the only thing the shim did. Its
758 /// write went through [`Storage::put_device_grant`], which is an INSERT-OR-UPDATE: a grant
759 /// redeemed by [`Storage::take_device_grant`] between the shim's read and the shim's write was
760 /// put BACK, so the shim did not merely fail to prevent a lost update, it manufactured a
761 /// single-use device code that could be redeemed twice. That is a worse defect than the one it
762 /// was written to mitigate.
763 ///
764 /// And it was wrong about the signal. A default implementation that is silently incorrect is
765 /// worse than no default at all, because the host who never reads this paragraph gets NOTHING:
766 /// their store compiles, their tests pass, and RFC 8628 section 3.3's first-decision-wins
767 /// guarantee is void in production. Requiring the method makes that a compile error naming the
768 /// method, which is the loudest and cheapest signal available, and it costs a host who has
769 /// already written the other four device-grant methods one more.
770 ///
771 /// [`crate::storage_conformance`] checks all four properties (a swap that must apply, a swap
772 /// that must be refused, a swap that must not resurrect a redeemed grant, and — since the
773 /// atomicity above is the whole point and was for a long time the one thing nothing raced —
774 /// N callers swapping the same `expected` concurrently, of which exactly one may win). Run it.
775 ///
776 /// The polling device and the verification UI are different requests on different nodes, so
777 /// the race in that fourth check is the ordinary case rather than a manufactured one. See the
778 /// trait's rule that contention is the store's to resolve, not the caller's: the loser of that
779 /// race is told `Ok(false)`, never a [`StorageError`].
780 fn compare_and_swap_device_grant(
781 &self,
782 expected: &DeviceGrantState,
783 updated: DeviceGrant,
784 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
785
786 /// Insert or replace an authorization code record, keyed by its code string.
787 ///
788 /// # THE ONE WRITE ON A REVOCABLE RECORD THAT DOES NOT CONSULT A BARRIER
789 ///
790 /// The module docs state the resurrection rule without exceptions, so this one is stated here
791 /// rather than left for a host to discover by reading the implementation.
792 ///
793 /// It is exempt because refusing here would be WORSE than the resurrection it would prevent. A
794 /// redemption writes the consumed record BEFORE issuing, precisely so that a store failure
795 /// halfway through cannot take RFC 6749 section 4.1.2 replay detection offline with it. A
796 /// barrier that refused that write would disarm the alarm at exactly the moment a grant was
797 /// being revoked, which is when replay detection is most likely to matter.
798 ///
799 /// WHAT THE EXEMPTION ACTUALLY COSTS, measured against the rule rather than waved at: a
800 /// redemption that took a code before [`Storage::delete_client`] or [`Storage::revoke_consent`]
801 /// cascaded it away will write its record back, so the ROW comes back. That row mints nothing.
802 /// The issuance behind it calls [`Storage::put_token`], which the barrier refuses, so no
803 /// credential outlives the revocation. What is left is a consumed code belonging to no live
804 /// grant, which [`Storage::sweep_expired`] reclaims at its own expiry. A row, not a
805 /// capability.
806 ///
807 /// [`Storage::compare_and_swap_authorization_code`] is the conditional form, and it is what
808 /// the redemption's SECOND write uses, where refusing is exactly right.
809 fn put_authorization_code(
810 &self,
811 record: AuthorizationCodeRecord,
812 ) -> impl Future<Output = Result<(), StorageError>> + Send;
813
814 /// Replace the record stored under `updated.code` with `updated`, but ONLY if the stored
815 /// record's [`crate::authorization::AuthorizationCodeState`] is still `expected`. Answers whether the write happened.
816 ///
817 /// Same contract as [`Storage::compare_and_swap_device_grant`], and it is here for the same
818 /// class of reason: two actors write one authorization code record, and one of them is
819 /// suspended across an unbounded await while the other is deciding what to do.
820 ///
821 /// The interleaving, in full, because it is the reason this method exists. A redemption writes
822 /// `Consumed { access_token: None, .. }` BEFORE issuing, so that a store failure mid-issuance
823 /// cannot disarm replay detection, then suspends on the host's signer. A replay arriving in
824 /// that window sees `access_token: None`, correctly finds nothing to revoke, and marks the
825 /// record [`crate::authorization::AuthorizationCodeState::Replayed`]. When the redemption wakes and records what it
826 /// minted, THIS comparison fails, and the redemption undoes its own issuance instead of
827 /// handing out tokens that a detected replay was supposed to have contained.
828 ///
829 /// `Ok(false)` for a `code` that is not present, and the write MUST NOT insert: a code that
830 /// has been swept or cascaded away by [`Storage::delete_client`] or
831 /// [`Storage::revoke_consent`] must stay gone, for the reason the module docs give.
832 fn compare_and_swap_authorization_code(
833 &self,
834 expected: &crate::authorization::AuthorizationCodeState,
835 updated: AuthorizationCodeRecord,
836 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
837
838 /// Atomically remove and return an authorization code record. This is the single-use
839 /// redemption primitive for the authorization code grant: under concurrent redemption exactly
840 /// one caller receives the record and every other caller sees `None`.
841 ///
842 /// The server puts a CONSUMED record back after a successful redemption (see
843 /// [`crate::authorization::AuthorizationCodeState`]), so that a replay can be recognised as a
844 /// replay and revoke what the code already minted, rather than looking like a typo.
845 fn take_authorization_code(
846 &self,
847 code: &str,
848 ) -> impl Future<Output = Result<Option<AuthorizationCodeRecord>, StorageError>> + Send;
849
850 /// Insert or replace a pushed authorization request (RFC 9126 section 2.2), keyed by its
851 /// `request_uri`.
852 ///
853 /// UNLESS A REVOCATION COVERS IT, exactly as [`Storage::put_token`] and
854 /// [`Storage::put_refresh_token`] are, and for the same reason. A pushed request is a
855 /// revocable record: [`Storage::delete_client`]'s cascade removes the handles of the client
856 /// being deleted, because RFC 9126 section 2.2 binds a `request_uri` to the client that
857 /// pushed it.
858 ///
859 /// It needs the barrier rather than absence, because the caller may be the one who created
860 /// the absence. `validate_pushed_authorization_request` TAKES the record before it can check
861 /// which client the handle belongs to, and puts it back when the presenter was a stranger, so
862 /// a `delete_client` landing in that window finds nothing to cascade and the put-back would
863 /// otherwise restore a handle belonging to a client that no longer exists. If the host then
864 /// re-provisions the same `client_id`, which the trait explicitly permits, the restored
865 /// handle resolves against the NEW registration and carries authorization parameters its
866 /// owner never pushed.
867 #[cfg(feature = "par")]
868 fn put_pushed_authorization_request(
869 &self,
870 record: crate::par::PushedAuthorizationRequest,
871 ) -> impl Future<Output = Result<WriteOutcome, StorageError>> + Send;
872
873 /// Atomically remove and return a pushed authorization request. This is what makes a
874 /// `request_uri` single use: RFC 9126 section 4 says a client MUST use one once and section
875 /// 7.3 asks the server to enforce it rather than trust that, so under concurrent authorization
876 /// requests exactly one caller receives the record and every other caller sees `None`. A plain
877 /// read-then-delete reintroduces the replay this is here to prevent.
878 ///
879 /// Unlike [`Storage::take_authorization_code`], nothing is put back after a SUCCESSFUL
880 /// resolution: a spent handle minted no credential of its own, so there is nothing a later
881 /// presentation of it could need to be recognised for, and retaining it would only keep a live
882 /// capability string in the store. The server DOES put it back when the handle was presented by
883 /// the wrong client, so that a stranger cannot destroy a legitimate client's request.
884 #[cfg(feature = "par")]
885 fn take_pushed_authorization_request(
886 &self,
887 request_uri: &str,
888 ) -> impl Future<Output = Result<Option<crate::par::PushedAuthorizationRequest>, StorageError>> + Send;
889
890 /// Persist an issued access token, UNLESS a revocation covers it.
891 ///
892 /// The store derives the barriers to consult from the record itself: its `client_id`, its
893 /// `family_id` when it has one, and its `subject` paired with its `client_id`. A caller
894 /// therefore cannot forget to pass the right scope, because there is no scope to pass. If any
895 /// matching [`RevocationBarrier`] is recorded, this writes NOTHING and answers
896 /// [`WriteOutcome::RefusedRevoked`].
897 ///
898 /// The check and the write MUST be ONE atomic step, exactly as for the `take_*` operations: a
899 /// look-then-insert leaves the window this method exists to close.
900 ///
901 /// This is the write that makes revocation mean something under concurrency. A token minted
902 /// from a grant that was revoked while the signature was being computed is a token the user
903 /// was told did not exist, and with a host `Es256Signer` fronting a KMS that window is a
904 /// network round trip wide.
905 fn put_token(
906 &self,
907 token: IssuedToken,
908 ) -> impl Future<Output = Result<WriteOutcome, StorageError>> + Send;
909
910 /// Look up an access token (introspection).
911 ///
912 /// `Arc` for the same reason [`Storage::get_client`] is: the record is only READ here, and
913 /// with opaque tokens this is the read a resource server makes on every protected request,
914 /// which makes it the hottest read in the crate after `get_client`. MEASURED against
915 /// [`MemoryStorage`]: 7 allocations per call when it handed back an owned [`IssuedToken`],
916 /// none now.
917 fn get_token(
918 &self,
919 access_token: &str,
920 ) -> impl Future<Output = Result<Option<Arc<IssuedToken>>, StorageError>> + Send;
921
922 /// Remove an access token. Idempotent: removing a token that is already gone is success, as
923 /// RFC 7009 section 2.2 requires of revocation.
924 fn delete_token(
925 &self,
926 access_token: &str,
927 ) -> impl Future<Output = Result<(), StorageError>> + Send;
928
929 /// Persist a refresh token record, UNLESS a revocation covers it.
930 ///
931 /// Same contract as [`Storage::put_token`], and it carries more weight here, because this is
932 /// the method every refusal path of a rotation calls. [`Storage::take_refresh_token`] has
933 /// already REMOVED the record by then, so a cascade that ran in that window found nothing to
934 /// cascade to; writing the record back unconditionally restores a live, rotatable refresh
935 /// token that the user has been told was revoked. Absence cannot be the evidence on this path,
936 /// because the caller is the one who created it. A barrier can.
937 fn put_refresh_token(
938 &self,
939 record: RefreshTokenRecord,
940 ) -> impl Future<Output = Result<WriteOutcome, StorageError>> + Send;
941
942 /// Look up a refresh token record WITHOUT removing it.
943 ///
944 /// This exists so that a check ABOUT a refresh token never has to be built out of a
945 /// read-modify-write ON it. RFC 7009 section 2.1 requires revocation to verify that the token
946 /// was issued to the requesting client; doing that by taking the record and putting it back on
947 /// a mismatch is a destructive operation on a credential the caller was never entitled to
948 /// touch, and if the restoring write fails, the victim's chain is gone for good while the
949 /// endpoint still answers 200.
950 ///
951 /// `Arc`, and note the contrast with [`Storage::take_refresh_token`] directly below: this one
952 /// asks a question about a record that stays put, so a shared pointer answers it, while the
953 /// take REMOVES the record and must hand back an owned value because "exactly one caller got
954 /// it" is the whole of what rotation rests on. MEASURED: 7 allocations per call before.
955 fn get_refresh_token(
956 &self,
957 refresh_token: &str,
958 ) -> impl Future<Output = Result<Option<Arc<RefreshTokenRecord>>, StorageError>> + Send;
959
960 /// Atomically remove and return a refresh token record. This is what makes rotation single
961 /// use: under concurrent refresh exactly one caller wins and every other presentation of the
962 /// same token is `invalid_grant`.
963 ///
964 /// The server puts a SPENT record back after a successful rotation (see
965 /// [`crate::token::RefreshTokenState`]), so that a later presentation is recognisable as reuse
966 /// rather than as an unknown string.
967 fn take_refresh_token(
968 &self,
969 refresh_token: &str,
970 ) -> impl Future<Output = Result<Option<RefreshTokenRecord>, StorageError>> + Send;
971
972 /// Revoke EVERY token, access and refresh, carrying `family_id`, and return how many records
973 /// were removed.
974 ///
975 /// This is the RFC 9700 section 4.14.2 remedy for detected refresh token reuse: the AS
976 /// invalidates the presented token and revokes the tokens issued for that authorization grant.
977 /// Removing only the replayed token would leave the thief's rotated chain, and every access
978 /// token minted along it, entirely live.
979 ///
980 /// Implementations SHOULD make this reachable without a full scan (index `family_id` on both
981 /// the access token and the refresh token tables). It runs only on a detected compromise, so
982 /// it is not a hot path, but it must actually complete.
983 ///
984 /// NOT CHECKED, and it cannot be: [`crate::storage_conformance`] sees answers, not plans, so a
985 /// store that satisfies this by scanning is indistinguishable from one that satisfies it by
986 /// index at the sizes a harness can plant. It is a SHOULD for that reason rather than as a
987 /// softening. Said here so that a host reading "write the 20, run the harness, read what it
988 /// says" does not take a green as covering it.
989 ///
990 /// Removing records that are already gone is success: this runs on evidence of compromise and
991 /// must not be turned into an error by a concurrent revocation.
992 ///
993 /// An EMPTY `family_id` is the one input that is NOT success. It is REFUSED with a
994 /// [`StorageError`], before anything is removed, because the empty string does not name a
995 /// family a barrier can be recorded for; see [`RevocationBarrier`]. Every access token this
996 /// crate mints for a client credentials grant carries `family_id: None`, so a store that
997 /// treated `""` as a family would be one careless call away from matching them all.
998 ///
999 /// # It MUST also record a barrier, in the same step
1000 ///
1001 /// [`RevocationBarrier::TokenFamily`] for this `family_id`, over `window`,
1002 /// recorded ATOMICALLY with the removals. This is the variant with the sharpest failure mode
1003 /// in the crate. The rotation that this revocation is racing has already TAKEN its refresh
1004 /// record, so the scan above cannot see it; when the rotation then writes its spent record and
1005 /// its freshly minted tokens, the family is whole again, and the AS has answered a detected
1006 /// compromise by revoking nothing. RFC 9700 section 4.14.2 is the reason this path exists, and
1007 /// a revocation that a concurrent redemption can undo does not satisfy it.
1008 ///
1009 /// `window.until` must be at least the family's longest-lived token: a chain with an absolute
1010 /// lifetime uses that, and a chain without one uses the access token lifetime, which is the
1011 /// longest anything issued from it can outlive the revocation.
1012 ///
1013 /// A family barrier refuses UNCONDITIONALLY, so it is the one scope that does NOT compare
1014 /// against `window.recorded_at`. Rotation legitimately mints fresh records inside an existing
1015 /// family, so a comparison here would admit exactly the write this exists to refuse: the
1016 /// rotation that completes after the cascade. Nothing legitimate is lost, because a new grant
1017 /// gets a new `family_id`.
1018 fn revoke_token_family(
1019 &self,
1020 family_id: &str,
1021 window: RevocationWindow,
1022 ) -> impl Future<Output = Result<u64, StorageError>> + Send;
1023
1024 /// Insert or replace a consent record, keyed by its `consent_id`.
1025 ///
1026 /// The server keeps at most ONE live consent per (`client_id`, `subject`) pair and widens it
1027 /// in place, so a store that indexes that pair (see [`Storage::find_consent`]) must keep the
1028 /// index consistent with this write.
1029 ///
1030 /// Like [`Storage::put_client`], this is the unconditional form and it is NOT what the widen
1031 /// path uses: see [`Storage::compare_and_swap_consent`].
1032 #[cfg(feature = "consent")]
1033 fn put_consent(
1034 &self,
1035 record: crate::consent::ConsentRecord,
1036 ) -> impl Future<Output = Result<(), StorageError>> + Send;
1037
1038 /// Write `updated` only if the live consent for its (`client_id`, `subject`) pair is still
1039 /// exactly `expected`. Answers whether the write happened.
1040 ///
1041 /// `expected` is an `Option` because both transitions matter and they fail differently:
1042 ///
1043 /// - `Some(record)`: the caller read a consent and is WIDENING it. `Ok(false)` if the stored
1044 /// record has changed or is gone. Gone is the case that matters, because that is what
1045 /// [`Storage::revoke_consent`] leaves: without the comparison, a widen that was in flight
1046 /// when the user clicked withdraw puts the consent back, and every future authorization
1047 /// request is answered from a record the user believes they destroyed.
1048 /// - `None`: the caller found NO consent for the pair and is CREATING one. `Ok(false)` if the
1049 /// pair now has one. This is the half that closes the duplicate-record race the server's
1050 /// `record_consent` doc used to concede: two overlapping first-time approvals can no longer
1051 /// each create a record, so the pair really does hold at most one.
1052 ///
1053 /// Comparison and write MUST be ONE atomic step, and the comparison is against whatever
1054 /// [`Storage::find_consent`] would answer for the pair, NOT against the `consent_id`: a
1055 /// withdrawal removes the record the caller read, and a fresh one created after it has a
1056 /// different id, so comparing ids would miss exactly the interleaving this exists to catch.
1057 #[cfg(feature = "consent")]
1058 fn compare_and_swap_consent(
1059 &self,
1060 expected: Option<&crate::consent::ConsentRecord>,
1061 updated: crate::consent::ConsentRecord,
1062 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
1063
1064 /// Look up a consent record by its identifier.
1065 #[cfg(feature = "consent")]
1066 fn get_consent(
1067 &self,
1068 consent_id: &str,
1069 ) -> impl Future<Output = Result<Option<Arc<crate::consent::ConsentRecord>>, StorageError>> + Send;
1070
1071 /// The live consent for one (client, subject) pair, if there is one.
1072 ///
1073 /// This is what remembered consent is read from, and unlike the rest of the consent operations
1074 /// it runs on the AUTHORIZATION ENDPOINT'S path, so a store SHOULD index the pair rather than
1075 /// scanning. NOT CHECKED, for the reason [`Storage::revoke_token_family`] gives about its own
1076 /// indexing clause: the harness observes answers, and both shapes answer the same.
1077 #[cfg(feature = "consent")]
1078 fn find_consent(
1079 &self,
1080 client_id: &ClientId,
1081 subject: &str,
1082 ) -> impl Future<Output = Result<Option<Arc<crate::consent::ConsentRecord>>, StorageError>> + Send;
1083
1084 /// Every consent one resource owner has granted, so a host can show a user what they have
1085 /// approved. Order is not specified; a host that wants one sorts what it gets back.
1086 #[cfg(feature = "consent")]
1087 fn consents_for_subject(
1088 &self,
1089 subject: &str,
1090 ) -> impl Future<Output = Result<Vec<Arc<crate::consent::ConsentRecord>>, StorageError>> + Send;
1091
1092 /// WITHDRAW a consent: remove the record AND everything issued under it, returning how many
1093 /// records were removed (the consent record itself is not counted).
1094 ///
1095 /// This is [`Storage::revoke_token_family`] at a BROADER granularity, and it is deliberately
1096 /// the same primitive rather than a parallel mechanism. A family is one refresh chain and the
1097 /// tokens minted along it; a consent is every grant one client ever obtained for one user, and
1098 /// one consent spans many families over time because every fresh trip through the
1099 /// authorization endpoint mints another one. Withdrawing a consent and revoking only the newest
1100 /// family would leave every earlier chain live, which is this feature failing silently, and
1101 /// silently is the worst way for it to fail: the user has been told they stopped something they
1102 /// did not.
1103 ///
1104 /// "Everything issued under it" means, for the consent's (`client_id`, `subject`) pair:
1105 ///
1106 /// - access tokens for that subject;
1107 /// - refresh records for that subject, whatever family they belong to;
1108 /// - authorization codes issued to that subject, which are grants in flight and would otherwise
1109 /// mint a token seconds after the user said stop;
1110 /// - device grants that subject has APPROVED but the device has not yet polled, for the same
1111 /// reason. A PENDING device grant is left alone: nobody has consented to it yet, so there is
1112 /// nothing there to withdraw;
1113 /// - the USER-CODE INDEX ENTRIES of any device grant removed above. Not a record and not
1114 /// counted in the return, but a store that keeps such an index MUST retire the entry with the
1115 /// grant. Both bundled stores do. An entry left pointing at a removed grant is worse than
1116 /// untidy: [`Storage::put_device_grant`] must refuse a user code that is already indexed for
1117 /// a different `device_code`, so a stale entry takes that code out of circulation for good,
1118 /// and the server's generation loop cannot see the collision coming because the lookup it
1119 /// makes resolves to nothing.
1120 ///
1121 /// That list is the DEFINITION a host implements against: nothing in the type system can check
1122 /// a cascade, so a kind it omits is a kind that survives every withdrawal in a store written to
1123 /// the letter of it.
1124 ///
1125 /// # What the PAIR cannot reach, and why that is written here rather than left to be found
1126 ///
1127 /// Every clause above, and the barrier below, keys on the WITHDRAWN consent's `client_id`. RFC
1128 /// 8693 token exchange (the `token-exchange` feature) issues a token to the EXCHANGING client
1129 /// while carrying the subject token's resource owner and the instant its grant was established:
1130 /// the instant is inherited, the identity is not. So a token some other client exchanged out of
1131 /// this consent's tokens matches neither the retain predicates above nor
1132 /// [`RevocationBarrier::Consent`], and a host implementing this list exactly is not the reason
1133 /// — no store can see a descendant this crate never records a link to.
1134 ///
1135 /// It is BOUNDED and it is not indefinite: an exchanged token's expiry is clamped to the
1136 /// subject token's (see `crate::token_exchange`), and that clamp holds along a chain of
1137 /// exchanges, so the descendant dies when the token it came from would have. What it costs is
1138 /// the difference between "immediately" and "within one access token lifetime", on the one
1139 /// operation whose whole promise is the first of those. Closing it needs the origin grant's
1140 /// identity recorded ON the issued token and compared here, which is a persisted-record change
1141 /// and therefore a clause of this contract and a [`crate::storage_conformance`] case, not
1142 /// something a store can be left to infer.
1143 ///
1144 /// It is ONE operation rather than five so a real database can do it in one transaction. A
1145 /// withdrawal that half succeeded leaves a user believing they revoked something they did not,
1146 /// which is the failure this whole feature exists to prevent.
1147 ///
1148 /// Withdrawing a consent that is already gone is `Ok(0)`, not an error, for the same reason
1149 /// [`Storage::revoke_token_family`] tolerates a concurrent revocation: a user who clicks twice
1150 /// has not made a mistake.
1151 ///
1152 /// A consent whose `client_id` or `subject` is EMPTY is REFUSED with a [`StorageError`],
1153 /// before anything is removed and before the consent row itself is, because the pair is what
1154 /// the barrier is recorded for and the empty string does not name one; see
1155 /// [`RevocationBarrier`]. The scope is read from the STORED record rather than passed in, so
1156 /// unlike the other two revocations the refusal is reachable only through a record some
1157 /// earlier `put_consent` accepted — which is the argument for refusing here rather than
1158 /// trusting that nobody ever wrote one. A refusal must leave the consent standing: a
1159 /// withdrawal that removed the record and then declined to record the barrier the withdrawal
1160 /// depends on is the worst of both answers.
1161 ///
1162 /// This runs when a person clicks something, never on a token-plane request, so it is not a hot
1163 /// path. It must simply complete.
1164 ///
1165 /// # It MUST also record a barrier, in the same step
1166 ///
1167 /// [`RevocationBarrier::Consent`] for the withdrawn record's (`client_id`, `subject`) pair,
1168 /// over `window`, recorded ATOMICALLY with the cascade. The cascade above can
1169 /// only reach records that are IN the store when it runs, and this is the feature whose whole
1170 /// promise is that it reaches everything: a refresh rotation or an authorization code
1171 /// redemption in flight for this pair completes afterwards and writes a live token for a
1172 /// relationship the user just ended. A user who is told "you have revoked this application"
1173 /// and still has a working token is the exact failure this feature exists to prevent, so the
1174 /// barrier is part of the withdrawal rather than an optimisation of it.
1175 ///
1176 /// Withdrawing a consent that is already gone records NO barrier and answers `Ok(0)`: there is
1177 /// no pair to name.
1178 #[cfg(feature = "consent")]
1179 fn revoke_consent(
1180 &self,
1181 consent_id: &str,
1182 window: RevocationWindow,
1183 ) -> impl Future<Output = Result<u64, StorageError>> + Send;
1184
1185 /// Atomically CLAIM a single-use identifier, returning `true` when this caller is the first
1186 /// to claim it and `false` when it has already been claimed.
1187 ///
1188 /// This is the replay-prevention primitive behind two REQUIREMENTS, not two optimisations:
1189 /// RFC 7523 section 3 makes a client assertion's `jti` single use within the assertion's
1190 /// validity, and RFC 9449 section 4.3 makes a DPoP proof's `jti` single use within the proof's
1191 /// acceptance window. An implementation that verifies the signature and skips this has built a
1192 /// credential that anybody who observed one request can send again, which is the whole of what
1193 /// those two mechanisms exist to prevent.
1194 ///
1195 /// `expires_at` is when the claim may be reclaimed by [`Storage::sweep_expired`], and it is the
1196 /// caller's job to pass the instant past which the artifact would be refused on time alone
1197 /// (the assertion's `exp`, the proof's `iat` plus the acceptance window). Reclaiming EARLIER
1198 /// than that reopens the replay window; the two callers in this crate both derive it from the
1199 /// artifact rather than from a policy of their own.
1200 ///
1201 /// ATOMICITY IS THE CONTRACT, exactly as for the `take_*` operations above. A shared multi-node
1202 /// store must implement this with a genuinely atomic primitive (`INSERT ... ON CONFLICT DO
1203 /// NOTHING` and check the row count, `SET NX`, a compare-and-set); a read-then-write lets two
1204 /// concurrent presentations of the SAME assertion both be told they were first, which is the
1205 /// replay this method exists to refuse. Failing CLOSED on a storage error is the caller's job
1206 /// and this crate does it: a claim that could not be recorded is treated as a claim that
1207 /// failed.
1208 ///
1209 /// Claiming an id that is already present but EXPIRED is at the store's discretion: this crate
1210 /// never presents such an id, because the artifact carrying it would have been refused on time
1211 /// first. [`MemoryStorage`] treats a live entry as claimed regardless of its deadline and lets
1212 /// `sweep_expired` do the reclaiming, which is the conservative reading.
1213 #[cfg(any(feature = "client-assertion", feature = "dpop"))]
1214 #[cfg_attr(docsrs, doc(cfg(any(feature = "client-assertion", feature = "dpop"))))]
1215 fn claim_replay_id(
1216 &self,
1217 id: &str,
1218 expires_at: std::time::SystemTime,
1219 ) -> impl Future<Output = Result<bool, StorageError>> + Send;
1220
1221 /// Remove every record that is dead at `now`, and return how many were removed.
1222 ///
1223 /// # THE HOST MUST CALL THIS, ON A TIMER, FOREVER
1224 ///
1225 /// It is an OBLIGATION of running this crate, not a tuning knob. This crate has no background
1226 /// task and will never grow one (see the crate docs on zero cost until enabled), so this
1227 /// method runs when the host runs it and at no other time. Nothing else reclaims storage:
1228 /// consumed authorization codes are retained deliberately until their expiry, spent refresh
1229 /// records are retained deliberately until theirs, and expired access tokens and abandoned
1230 /// device grants are simply never looked at again.
1231 ///
1232 /// What a host that never calls it has built is a MEMORY EXHAUSTION PATH, not an untidy
1233 /// store. The RFC 8628 section 3.1 device authorization endpoint takes no client credential
1234 /// from a public client (it sends only its `client_id`, which RFC 6749 section 2.2 says is
1235 /// not a secret), so anyone who can open a socket can allocate a device grant plus a
1236 /// user-code index entry per request, in a loop, and none of it is ever reclaimed. The growth
1237 /// is attacker-paced and it ends with the process dying.
1238 ///
1239 /// Expiry ITSELF is enforced on read, so an unswept store is not INSECURE, it is UNBOUNDED.
1240 /// That is why the interval matters much less than the existence of the task: sweeping every
1241 /// few minutes and sweeping every few seconds are both fine, and never sweeping is not.
1242 ///
1243 /// One task per PROCESS. It must be safe to call concurrently with request handling (see
1244 /// below), so every node sweeping is harmless; a host that would rather not have N nodes
1245 /// deleting the same rows runs it from one of them, or from a scheduled job that calls the
1246 /// same method. A sweep failure must be logged and retried on the next tick, never allowed
1247 /// to end the task: a silently stopped sweeper shows up hours later as memory growth.
1248 ///
1249 /// `crates/oauth-as/examples/production_server.rs` wires this, with the interval reasoning.
1250 ///
1251 /// ```ignore
1252 /// // Once per process, at startup.
1253 /// tokio::spawn(async move {
1254 /// let mut ticker = tokio::time::interval(Duration::from_secs(60));
1255 /// loop {
1256 /// ticker.tick().await;
1257 /// if let Err(e) = server.store().sweep_expired(SystemTime::now()).await {
1258 /// // Log and continue. Do not return: returning stops the sweep forever.
1259 /// eprintln!("sweep failed, retrying next tick: {e}");
1260 /// }
1261 /// }
1262 /// });
1263 /// ```
1264 ///
1265 /// "Dead at `now`" is DEFINED by the list below, kind by kind, and the list is what a host
1266 /// implements against. A kind it omits is a table nothing ever reclaims, which is the memory
1267 /// exhaustion path three paragraphs up, reached by a host that did everything this doc asked.
1268 ///
1269 /// "Dead at `now`" means, for each kind:
1270 ///
1271 /// - device grants with `expires_at <= now`, AND the user-code index entries of the grants
1272 /// removed. The index is a pointer rather than a record, so it is not counted in the return,
1273 /// but a store that keeps one must retire the entry with the grant: see
1274 /// the `revoke_consent` list below for what a stale entry costs, which is that user code
1275 /// permanently unusable rather than merely a leaked row. Both bundled stores make this pass.
1276 /// - authorization codes with `expires_at <= now` (in either state)
1277 /// - pushed authorization requests (present only under the `par` feature) with
1278 /// `expires_at <= now`. RFC 9126 section 4 refuses an expired `request_uri`, and a spent one
1279 /// is removed by [`Storage::take_pushed_authorization_request`], so nothing else in this
1280 /// crate ever reclaims a handle that was pushed and then abandoned. This kind was MISSING
1281 /// from this list while both bundled stores swept it, which is the worst way for a contract
1282 /// to be wrong: a host implementing the trait to the letter of the enumeration got a store
1283 /// that certified clean and never reclaimed its pushed-request table. The push endpoint is
1284 /// client authenticated (RFC 9126 section 2.1), so this is not an anonymous flood like the
1285 /// device endpoint above, but one chatty or compromised client grows the table without bound.
1286 /// - access tokens with `expires_at <= now`
1287 /// - claimed replay identifiers (`claim_replay_id`, present only under the `client-assertion`
1288 /// or `dpop` features) with `expires_at <= now`
1289 /// - refresh records with `Some(expires_at) <= now`. A record with `expires_at: None` is a
1290 /// chain with no absolute lifetime and is NOT dead; the server gives a spent record a
1291 /// retention deadline precisely so this method can reclaim it.
1292 /// - [`RevocationBarrier`]s whose `until` is at or before `now`. A barrier is not a record
1293 /// either, but
1294 /// unlike the user-code index it IS counted here, because it is a row an unswept store
1295 /// accumulates one of per revocation and nothing else ever removes. Reaping one EARLY
1296 /// reopens the resurrection window it was recorded to close, which is why the deadline comes
1297 /// from the caller rather than from a fixed retention: sweep on the deadline, never before.
1298 ///
1299 /// Nothing else is time limited, and the omissions are deliberate: a client registration and a
1300 /// consent record last until something removes them ([`Storage::delete_client`],
1301 /// [`Storage::revoke_consent`]), so a sweep that reaped either would delete a live grant a user
1302 /// still relies on. [`crate::storage_conformance`] plants a DEAD record of every kind in this
1303 /// list and checks the count this method returns, with LIVE records beside them, so a store
1304 /// that reaps too little and a store that reaps too much are each told which.
1305 ///
1306 /// It must be safe to call concurrently with request handling, and safe to call when there is
1307 /// nothing to do (answering 0). BOTH HALVES ARE CHECKED, and the first was not until 0.9.1:
1308 /// [`crate::storage_conformance`] runs `sweep_expired/empty_is_zero` for the second and
1309 /// `sweep_expired/safe_under_concurrent_writes` for the first, the latter by sweeping at an
1310 /// instant when nothing is dead while `put_token` calls land beside it and then requiring every
1311 /// write the store reported as applied to still be readable. The store that fails only that
1312 /// check is the one that reads the table, decides what to keep and writes the kept set back:
1313 /// correct in every measurement taken while nothing else is running, and losing an issued
1314 /// access token per overlap in production.
1315 fn sweep_expired(
1316 &self,
1317 now: std::time::SystemTime,
1318 ) -> impl Future<Output = Result<u64, StorageError>> + Send;
1319}
1320
1321#[derive(Default)]
1322struct MemoryInner {
1323 /// `Arc` so that [`Storage::get_client`] answers with a pointer clone rather than a deep copy
1324 /// of the registration on every authenticated request. MEASURED: 8 allocations per call before,
1325 /// one atomic increment after.
1326 clients: HashMap<String, Arc<Client>>,
1327 device_by_code: HashMap<String, DeviceGrant>,
1328 /// normalized user code -> device_code
1329 user_code_index: HashMap<String, String>,
1330 codes: HashMap<String, AuthorizationCodeRecord>,
1331 #[cfg(feature = "par")]
1332 pushed: HashMap<String, crate::par::PushedAuthorizationRequest>,
1333 /// `Arc` so that `get_token` (introspection, once per protected resource request when tokens
1334 /// are opaque) is a pointer clone. MEASURED: 7 allocations per read before, one on the write.
1335 tokens: HashMap<String, Arc<IssuedToken>>,
1336 /// `Arc` for the same reason as `tokens`; `take_refresh_token` unwraps it back to an owned
1337 /// record, which costs nothing when the store is the only holder, and clones when a reader is
1338 /// still looking at the snapshot it was handed.
1339 refresh: HashMap<String, Arc<RefreshTokenRecord>>,
1340 /// Consent records by `consent_id`. Present only under the `consent` feature, so a
1341 /// default build's store is byte for byte the store it was before.
1342 #[cfg(feature = "consent")]
1343 consents: HashMap<String, Arc<crate::consent::ConsentRecord>>,
1344 /// Claimed RFC 7523 / RFC 9449 single-use identifiers, mapped to when they may be reclaimed.
1345 /// Present only under the features that produce them, so a default build's store is byte for
1346 /// byte the store it was before.
1347 #[cfg(any(feature = "client-assertion", feature = "dpop"))]
1348 replay_ids: HashMap<String, std::time::SystemTime>,
1349 /// Recorded revocations, KEYED BY THE IDENTIFIER THEY NAME. Consulted by `put_token`,
1350 /// `put_refresh_token` AND `put_pushed_authorization_request`, each under the SAME guard that
1351 /// the revocation recorded them under, which is what makes the check-and-write one atomic step
1352 /// here.
1353 ///
1354 /// # Why this is a map, and why ONE map
1355 ///
1356 /// This was a `Vec` scanned linearly until the 0.9.1 audit, on the argument that a token is
1357 /// checked against three scopes at once so there is no single key to look up. The argument was
1358 /// wrong about the cost of being right: the scan runs on the ISSUANCE path, and the number of
1359 /// standing barriers is the number of revocations within one barrier lifetime, which at the
1360 /// shipped defaults is `refresh_reuse_window` and thirty days of them. So every token minted
1361 /// was priced by every revocation the deployment had recorded and not yet swept, and recording
1362 /// one was itself a scan, which makes filling the collection quadratic.
1363 ///
1364 /// That is the same operation this crate already refused to ship against PostgreSQL.
1365 /// `oauth-as-postgres/migrations/0005_revocation_barriers.sql` indexes the equivalent table
1366 /// because "without them every token issued costs a sequential scan of every revocation the
1367 /// deployment has ever recorded and not yet swept", with a measurement beside it. That is a
1368 /// statement about the operation rather than about a database, and this store is `pub` and is
1369 /// what a host reads first. `tests/storage_contract.rs`'s
1370 /// `a_standing_barrier_does_not_price_every_later_issuance` holds the bound.
1371 ///
1372 /// ONE map keyed by the identifier STRING, rather than one per scope or one keyed by
1373 /// [`RevocationBarrier`] itself, and both halves of that are deliberate:
1374 ///
1375 /// - Keying by `RevocationBarrier` is what this was before the `Vec`, and it cost a whole
1376 /// hashbrown instantiation for a compound enum key (MEASURED with `scripts/size-report.sh`:
1377 /// 2,204 bytes on the DEFAULT feature set, which every consumer pays). It also cannot be
1378 /// PROBED without building the key, so `is_revoked` would allocate on the issuance path,
1379 /// which `tests/allocation.rs` gates.
1380 /// - One map keyed by the identifier is probed with a `&str` the caller already holds, so
1381 /// `is_revoked` still allocates nothing, and a `client_id` lookup answers the `Client` scope
1382 /// and the `Consent` scopes for that client TOGETHER: two probes cover all three scopes.
1383 ///
1384 /// A client id and a family id can be the same string, and that is not a collision: they are
1385 /// separate FIELDS of the value, so a barrier recorded for one is never read as the other.
1386 ///
1387 /// WHAT THE KEYED LOOKUP COSTS, since the `Vec` was chosen on a measurement and replacing it
1388 /// has to answer the same question. MEASURED with `scripts/size-report.sh` on
1389 /// aarch64-apple-darwin: the DEFAULT row went from 228,485 to 233,721 bytes, so this shape
1390 /// costs 5,236 bytes over the scan, which is the two hashbrown instantiations it takes to key
1391 /// three scopes without allocating to probe them. The row stays inside its recorded budget.
1392 /// That is the trade this file is willing to make and the earlier one was not: the bytes are
1393 /// paid once per binary, and the scan was paid once per token.
1394 ///
1395 /// Each recorded scope carries TWO instants and they answer different questions. `recorded_at`
1396 /// is when the revocation happened, and it is what a write is compared against: a grant
1397 /// established after it is a NEW decision and must not be refused. `until` is when the barrier
1398 /// may be reaped, and it is only ever read by `sweep_expired`.
1399 barriers: HashMap<String, ScopeBarriers>,
1400}
1401
1402/// Every barrier recorded against ONE identifier string. See [`MemoryInner::barriers`].
1403///
1404/// `consents` is a map rather than a list because a mass logout revokes one consent per user of
1405/// one client, so the number of subjects standing against a single popular `client_id` is exactly
1406/// as unbounded as the whole collection was.
1407#[derive(Default)]
1408struct ScopeBarriers {
1409 /// Recorded by `delete_client` for this `client_id`.
1410 client: Option<BarrierTimes>,
1411 /// Recorded by `revoke_token_family` for this `family_id`.
1412 family: Option<BarrierTimes>,
1413 /// Recorded by `revoke_consent`, keyed by the subject who withdrew, for this `client_id`.
1414 consents: HashMap<String, BarrierTimes>,
1415}
1416
1417impl ScopeBarriers {
1418 /// Whether anything is still recorded here. An entry that answers `false` is a key
1419 /// `sweep_expired` removes, so the map does not keep a row per identity forever.
1420 fn is_empty(&self) -> bool {
1421 self.client.is_none() && self.family.is_none() && self.consents.is_empty()
1422 }
1423}
1424
1425/// One recorded revocation's two instants. See [`MemoryInner::barriers`] for why both are kept.
1426#[derive(Clone, Copy)]
1427struct BarrierTimes {
1428 recorded_at: std::time::SystemTime,
1429 until: std::time::SystemTime,
1430}
1431
1432impl BarrierTimes {
1433 fn new(recorded_at: std::time::SystemTime, until: std::time::SystemTime) -> Self {
1434 BarrierTimes { recorded_at, until }
1435 }
1436
1437 /// Fold a repeat revocation of the SAME scope into the one already recorded, keeping the later
1438 /// deadline. A second revocation of the same scope must never SHORTEN the first one's
1439 /// protection.
1440 ///
1441 /// `recorded_at` moves FORWARD on a repeat revocation, and that is not the same choice as the
1442 /// deadline's. The deadline takes the later of the two because protection must not shrink;
1443 /// `recorded_at` takes the later because it names the most recent revocation, and a grant
1444 /// established between the two revocations is one the second revocation was entitled to kill.
1445 /// Keeping the earlier instant would let that grant through.
1446 fn merge(&mut self, recorded_at: std::time::SystemTime, until: std::time::SystemTime) {
1447 if until > self.until {
1448 self.until = until;
1449 }
1450 if recorded_at > self.recorded_at {
1451 self.recorded_at = recorded_at;
1452 }
1453 }
1454
1455 /// [`BarrierTimes::merge`] into an empty-or-occupied slot.
1456 fn merge_into(
1457 slot: &mut Option<BarrierTimes>,
1458 recorded_at: std::time::SystemTime,
1459 until: std::time::SystemTime,
1460 ) {
1461 match slot {
1462 Some(existing) => existing.merge(recorded_at, until),
1463 None => *slot = Some(BarrierTimes::new(recorded_at, until)),
1464 }
1465 }
1466
1467 /// Whether a grant established at `established` is covered by a barrier that compares. Ties
1468 /// refuse: see [`MemoryInner::is_revoked`].
1469 fn covers(&self, established: std::time::SystemTime) -> bool {
1470 established <= self.recorded_at
1471 }
1472}
1473
1474impl MemoryInner {
1475 /// THE RESURRECTION PREDICATE. One function, consulted by every write that needs it.
1476 ///
1477 /// Written once and called from all three rather than inlined at each call site, deliberately:
1478 /// the barrier-consulting writes are `put_token`, `put_refresh_token` and
1479 /// `put_pushed_authorization_request`, and this doc said "both" until the 0.9.1 audit — which
1480 /// mattered, because a host reading the reference store for the list of writes that must
1481 /// consult a barrier would have taken the pushed request for an exemption. The last
1482 /// time this crate expressed one operation at three seams (`CompactJws::claim_time`, hand
1483 /// rolled inside `par.rs` instead of called) the hand-rolled copy was the one that failed
1484 /// open, and it shipped in 0.9.0. Same operation, one seam.
1485 ///
1486 /// The scopes are derived from the RECORD's own fields rather than passed in, so a caller
1487 /// cannot forget one and cannot name the wrong one. All three are checked: a token belongs to
1488 /// a client, to a family when it has one, and to a (client, subject) relationship when it has
1489 /// a subject, and any of the three being revoked is enough to refuse it.
1490 ///
1491 /// A barrier's DEADLINE is not read here at all; it is read only by `sweep_expired`. So a
1492 /// barrier past its deadline STILL REFUSES until the sweep reclaims it, which is the safe
1493 /// direction: the deadline is
1494 /// the point past which nothing in flight can still be holding a pre-revocation record, so
1495 /// refusing slightly longer costs a client one re-authentication and refusing too briefly
1496 /// costs the revocation itself.
1497 ///
1498 /// `grant_established_at` IS THE INSTANT THE GRANT BEHIND THIS WRITE WAS AUTHORIZED — the
1499 /// code's mint, the device approval, or the instant carried forward through every rotation of
1500 /// a refresh chain. It is NOT the instant the token is being written, and the difference is
1501 /// the whole point: a rotation and a code redemption both write at `now`, so `now` would make
1502 /// every barrier either useless or permanent.
1503 ///
1504 /// Two of the three scopes compare against it, and one deliberately does not:
1505 ///
1506 /// - `Client` and `Consent` name an identity that can legitimately be established AGAIN. A
1507 /// user who withdraws an application and approves it again has made a new decision, and a
1508 /// host may re-provision a `client_id` it deleted. A grant established after the revocation
1509 /// is that new decision and must be allowed through, or the revocation becomes a lockout
1510 /// lasting as long as the longest token this server mints.
1511 /// - `TokenFamily` is UNCONDITIONAL. A family is dead forever once it is revoked: rotation
1512 /// legitimately mints fresh records within an EXISTING family, so a comparison here would
1513 /// let exactly the write RFC 9700 s4.14.2 exists to stop — the rotation that completes after
1514 /// the cascade — put the family back. A new grant gets a new `family_id`, so nothing
1515 /// legitimate is refused by refusing this one always.
1516 ///
1517 /// Ties refuse. If a grant was established in the same instant the revocation was recorded,
1518 /// the ordering is genuinely unknown and refusing is the safe direction, exactly as it is for
1519 /// the deadline above.
1520 ///
1521 /// Allocation: NONE on the accepting path, and no scan either. The comparisons borrow, because
1522 /// `put_token` runs on every issuance and `tests/allocation.rs` counts what happens there; the
1523 /// lookups are keyed, because the same method runs on every issuance and
1524 /// `tests/storage_contract.rs` counts THAT. Two probes cover all three scopes: the `client_id`
1525 /// entry holds both the `Client` barrier and every `Consent` barrier recorded for that client.
1526 fn is_revoked(
1527 &self,
1528 client_id: &ClientId,
1529 family_id: Option<&str>,
1530 subject: Option<&str>,
1531 grant_established_at: std::time::SystemTime,
1532 ) -> bool {
1533 if let Some(scopes) = self.barriers.get(client_id.as_str()) {
1534 // A client barrier refuses on EITHER of two conditions. The window
1535 // (`t.covers(grant_established_at)`) refuses a grant established at or before the
1536 // revocation, and admits a later one so a re-provisioned `client_id` is served. The
1537 // second, `!self.clients.contains_key`, refuses whenever the client the barrier names
1538 // is GONE: `delete_client` records the barrier and removes the client row as one act,
1539 // so a client barrier standing over an ABSENT client is a deletion no re-provisioning
1540 // followed, and every grant for it is dead however its instant compares. A client the
1541 // host put back is present, so only the window applies -- re-provisioning is still
1542 // served, which is the property `admits_a_later_grant` holds. Without the absence arm a
1543 // write that raced the deletion, its `grant_established_at` a few microseconds after
1544 // `recorded_at`, is waved through as if it were a re-provisioned grant; the cross-store
1545 // race `a_pushed_request_cannot_land_behind_a_client_deletion` is where that surfaced.
1546 if scopes.client.is_some_and(|t| {
1547 t.covers(grant_established_at) || !self.clients.contains_key(client_id.as_str())
1548 }) {
1549 return true;
1550 }
1551 if let Some(subject) = subject {
1552 if scopes
1553 .consents
1554 .get(subject)
1555 .is_some_and(|t| t.covers(grant_established_at))
1556 {
1557 return true;
1558 }
1559 }
1560 }
1561 // The family scope is UNCONDITIONAL, so its presence alone refuses; see the doc above. It
1562 // is looked up under the FAMILY id rather than the client id, which is why one map with
1563 // two fields per key is the shape rather than one map per scope.
1564 family_id.is_some_and(|f| {
1565 self.barriers
1566 .get(f)
1567 .is_some_and(|scopes| scopes.family.is_some())
1568 })
1569 }
1570
1571 /// Record a barrier under the identifier it names, replacing any earlier one for the same
1572 /// scope with the later deadline. The merge rules, and why the two instants move differently,
1573 /// are [`BarrierTimes::merge_into`]'s.
1574 fn record_barrier(
1575 &mut self,
1576 barrier: RevocationBarrier,
1577 recorded_at: std::time::SystemTime,
1578 until: std::time::SystemTime,
1579 ) {
1580 // The key string is allocated HERE, on the revocation path, rather than being built to
1581 // probe with on the issuance path: a revocation happens once per logout, an issuance
1582 // happens once per token, and `is_revoked` is the one that must not allocate.
1583 match barrier {
1584 RevocationBarrier::Client(client_id) => {
1585 let entry = self
1586 .barriers
1587 .entry(client_id.as_str().to_string())
1588 .or_default();
1589 BarrierTimes::merge_into(&mut entry.client, recorded_at, until);
1590 }
1591 RevocationBarrier::TokenFamily(family_id) => {
1592 let entry = self.barriers.entry(family_id.into_string()).or_default();
1593 BarrierTimes::merge_into(&mut entry.family, recorded_at, until);
1594 }
1595 RevocationBarrier::Consent { client_id, subject } => {
1596 let entry = self
1597 .barriers
1598 .entry(client_id.as_str().to_string())
1599 .or_default();
1600 // Inserted and then merged, rather than merged into an `Option`, so that the
1601 // merge rules stay in ONE place: the insert wins only when nothing was recorded
1602 // for this subject, in which case the merge that follows it changes nothing.
1603 entry
1604 .consents
1605 .entry(subject.into_string())
1606 .or_insert(BarrierTimes::new(recorded_at, until))
1607 .merge(recorded_at, until);
1608 }
1609 }
1610 }
1611}
1612
1613/// The in-memory [`Storage`]: a mutexed set of maps. Reference implementation for the trait's
1614/// contract (its `take_*` are atomic by construction) and the store this crate's own tests run on.
1615/// Allocates nothing beyond its empty maps until used.
1616#[derive(Default)]
1617pub struct MemoryStorage {
1618 inner: Mutex<MemoryInner>,
1619}
1620
1621impl MemoryStorage {
1622 /// An empty store.
1623 pub fn new() -> Self {
1624 Self::default()
1625 }
1626
1627 fn lock(&self) -> std::sync::MutexGuard<'_, MemoryInner> {
1628 // A poisoned mutex means a panic mid-update; the maps hold owned values that are written
1629 // whole, so continuing with the recovered guard is sound.
1630 self.inner.lock().unwrap_or_else(|e| e.into_inner())
1631 }
1632}
1633
1634impl Storage for MemoryStorage {
1635 async fn get_client(&self, client_id: &ClientId) -> Result<Option<Arc<Client>>, StorageError> {
1636 // `Arc::clone` through `Option::cloned`: one atomic increment, no deep copy of the
1637 // registration. This is the hot read the module docs' measurement is about.
1638 Ok(self.lock().clients.get(client_id.as_str()).cloned())
1639 }
1640
1641 async fn put_client(&self, client: Client) -> Result<(), StorageError> {
1642 // The one allocation the `Arc` costs is paid HERE, on registration, which happens once per
1643 // client, rather than on `get_client`, which happens once per authenticated request.
1644 self.lock()
1645 .clients
1646 .insert(client.client_id.as_str().to_string(), Arc::new(client));
1647 Ok(())
1648 }
1649
1650 /// The whole operation happens under ONE guard, which is what makes the comparison and the
1651 /// write a single atomic step here. `Ok(false)` and no write when the record is absent or has
1652 /// moved on; absence is the [`Storage::delete_client`] case and it must stay absent.
1653 async fn compare_and_swap_client(
1654 &self,
1655 expected: &Client,
1656 updated: Client,
1657 ) -> Result<bool, StorageError> {
1658 let mut g = self.lock();
1659 match g.clients.get(updated.client_id.as_str()) {
1660 Some(current) if **current == *expected => {}
1661 // Absent (deleted) or changed (a concurrent update landed first). Both refuse, and
1662 // absent is the one that matters: an upsert here would put a deleted registration
1663 // back, with its old credential and its old registration access token hash.
1664 _ => return Ok(false),
1665 }
1666 g.clients
1667 .insert(updated.client_id.as_str().to_string(), Arc::new(updated));
1668 Ok(true)
1669 }
1670
1671 async fn delete_client(
1672 &self,
1673 client_id: &ClientId,
1674 window: RevocationWindow,
1675 ) -> Result<bool, StorageError> {
1676 // Before anything is removed, so a refusal leaves the store untouched rather than
1677 // half-cascaded. See `reject_empty_scope`.
1678 reject_empty_scope("client_id", client_id.as_str())?;
1679 let mut g = self.lock();
1680 let existed = g.clients.remove(client_id.as_str()).is_some();
1681 // Recorded under the SAME guard as the cascade below, so there is no instant at which the
1682 // records are gone and a concurrent issuance could still write more of them. The barrier
1683 // is recorded even when the registration was already gone: a client deleted twice is a
1684 // client whose in-flight issuances still need refusing.
1685 g.record_barrier(
1686 RevocationBarrier::Client(client_id.clone()),
1687 window.recorded_at,
1688 window.until,
1689 );
1690 // Every credential the registration holds goes with it (see the trait doc). Under the one
1691 // mutex, so no request can observe a half-deleted client.
1692 g.tokens.retain(|_, t| &t.client_id != client_id);
1693 g.refresh.retain(|_, r| &r.client_id != client_id);
1694 g.codes.retain(|_, c| &c.client_id != client_id);
1695 // RFC 9126 s2.2 binds a request_uri to the client that pushed it, so a deleted client's
1696 // outstanding handles are handles nobody may ever redeem.
1697 #[cfg(feature = "par")]
1698 g.pushed.retain(|_, p| &p.client_id != client_id);
1699 g.device_by_code.retain(|_, d| &d.client_id != client_id);
1700 // A consent names a client that no longer exists; leaving it would show a user an
1701 // application they cannot revoke, on a registration nothing can reach. The same
1702 // "everything the registration holds goes with it" rule as the four lines above.
1703 #[cfg(feature = "consent")]
1704 g.consents.retain(|_, c| &c.client_id != client_id);
1705 // The index is a pointer to a grant, not a record of its own; a dangling entry would make
1706 // a reaped user code resolve to nothing. Same pass `sweep_expired` makes.
1707 let live = &g.device_by_code;
1708 let stale: Vec<String> = g
1709 .user_code_index
1710 .iter()
1711 .filter(|(_, dc)| !live.contains_key(*dc))
1712 .map(|(uc, _)| uc.clone())
1713 .collect();
1714 for uc in stale {
1715 g.user_code_index.remove(&uc);
1716 }
1717 Ok(existed)
1718 }
1719
1720 async fn put_device_grant(&self, grant: DeviceGrant) -> Result<(), StorageError> {
1721 let mut g = self.lock();
1722 let normalized = crate::device::normalize_user_code(&grant.user_code);
1723
1724 // (1) The code must not already belong to a different device. Checked BEFORE any write, so
1725 // a refusal leaves the store exactly as it was.
1726 if let Some(owner) = g.user_code_index.get(&normalized) {
1727 if owner != &grant.device_code {
1728 return Err(StorageError::new(
1729 "user code is already indexed for a different device_code",
1730 ));
1731 }
1732 }
1733
1734 // (2) A put that changes this grant's user code retires the old entry, or the superseded
1735 // code goes on resolving here.
1736 if let Some(previous) = g.device_by_code.get(&grant.device_code) {
1737 let previous_normalized = crate::device::normalize_user_code(&previous.user_code);
1738 if previous_normalized != normalized {
1739 g.user_code_index.remove(&previous_normalized);
1740 }
1741 }
1742
1743 g.user_code_index
1744 .insert(normalized, grant.device_code.clone());
1745 g.device_by_code.insert(grant.device_code.clone(), grant);
1746 Ok(())
1747 }
1748
1749 async fn get_device_grant(
1750 &self,
1751 device_code: &str,
1752 ) -> Result<Option<DeviceGrant>, StorageError> {
1753 Ok(self.lock().device_by_code.get(device_code).cloned())
1754 }
1755
1756 async fn find_device_grant_by_user_code(
1757 &self,
1758 normalized_user_code: &str,
1759 ) -> Result<Option<DeviceGrant>, StorageError> {
1760 let g = self.lock();
1761 Ok(g.user_code_index
1762 .get(normalized_user_code)
1763 .and_then(|dc| g.device_by_code.get(dc))
1764 .cloned())
1765 }
1766
1767 async fn take_device_grant(
1768 &self,
1769 device_code: &str,
1770 ) -> Result<Option<DeviceGrant>, StorageError> {
1771 let mut g = self.lock();
1772 let grant = g.device_by_code.remove(device_code);
1773 if let Some(grant) = &grant {
1774 let normalized = crate::device::normalize_user_code(&grant.user_code);
1775 g.user_code_index.remove(&normalized);
1776 }
1777 Ok(grant)
1778 }
1779
1780 /// The whole operation happens under ONE guard, which is what makes it a compare-and-swap
1781 /// rather than a read followed by a hopeful write, and what a single-process host is entitled
1782 /// to expect from the reference implementation.
1783 ///
1784 /// Both halves of the user-code index contract documented on [`Storage::put_device_grant`] are
1785 /// enforced here TOO, restated rather than delegated, because a `&mut` guard is already held
1786 /// and calling the other method would deadlock. That duplication is a hazard worth naming: an
1787 /// earlier version of this doc claimed the index maintenance was delegated and therefore could
1788 /// not drift, and it had already drifted, because requirement (1), refusing a user code that is
1789 /// live for a DIFFERENT device code, was simply absent. A swap could hand one user code to two
1790 /// grants where a put would have refused. If either method changes, change both.
1791 async fn compare_and_swap_device_grant(
1792 &self,
1793 expected: &DeviceGrantState,
1794 updated: DeviceGrant,
1795 ) -> Result<bool, StorageError> {
1796 let mut g = self.lock();
1797 match g.device_by_code.get(&updated.device_code) {
1798 Some(current) if current.state == *expected => {}
1799 // Absent, or moved on. Absent is the redeemed-or-swept case and must stay absent: a
1800 // swap that reinstated a consumed grant would make a single-use device code
1801 // redeemable twice.
1802 _ => return Ok(false),
1803 }
1804 let normalized = crate::device::normalize_user_code(&updated.user_code);
1805 // Requirement (1), as `put_device_grant` applies it: RFC 8628 s6.1 makes the user code the
1806 // credential a human types, so two live grants answering to one code is two devices
1807 // sharing an identity. A REFUSAL rather than `Ok(false)`, because `Ok(false)` means "the
1808 // state moved on", which the caller answers by giving up quietly; this is a store-level
1809 // conflict the caller must hear about.
1810 if let Some(owner) = g.user_code_index.get(&normalized) {
1811 if owner != &updated.device_code {
1812 return Err(StorageError::new(
1813 "user code is already indexed for a different device_code",
1814 ));
1815 }
1816 }
1817 if let Some(previous) = g.device_by_code.get(&updated.device_code) {
1818 let previous_normalized = crate::device::normalize_user_code(&previous.user_code);
1819 if previous_normalized != normalized {
1820 g.user_code_index.remove(&previous_normalized);
1821 }
1822 }
1823 g.user_code_index
1824 .insert(normalized, updated.device_code.clone());
1825 g.device_by_code
1826 .insert(updated.device_code.clone(), updated);
1827 Ok(true)
1828 }
1829
1830 async fn put_authorization_code(
1831 &self,
1832 record: AuthorizationCodeRecord,
1833 ) -> Result<(), StorageError> {
1834 self.lock().codes.insert(record.code.clone(), record);
1835 Ok(())
1836 }
1837
1838 /// One guard, so the comparison and the write cannot be separated. Absence refuses, which is
1839 /// what keeps a swept or cascaded code from being reinstated.
1840 async fn compare_and_swap_authorization_code(
1841 &self,
1842 expected: &crate::authorization::AuthorizationCodeState,
1843 updated: AuthorizationCodeRecord,
1844 ) -> Result<bool, StorageError> {
1845 let mut g = self.lock();
1846 match g.codes.get(&updated.code) {
1847 Some(current) if current.state == *expected => {}
1848 _ => return Ok(false),
1849 }
1850 g.codes.insert(updated.code.clone(), updated);
1851 Ok(true)
1852 }
1853
1854 async fn take_authorization_code(
1855 &self,
1856 code: &str,
1857 ) -> Result<Option<AuthorizationCodeRecord>, StorageError> {
1858 Ok(self.lock().codes.remove(code))
1859 }
1860
1861 #[cfg(feature = "par")]
1862 async fn put_pushed_authorization_request(
1863 &self,
1864 record: crate::par::PushedAuthorizationRequest,
1865 ) -> Result<WriteOutcome, StorageError> {
1866 let mut g = self.lock();
1867 // Same one guard and the same one predicate as the two token writes. A pushed request
1868 // carries no family and no subject, so only the client scope can cover it.
1869 if g.is_revoked(&record.client_id, None, None, record.pushed_at) {
1870 return Ok(WriteOutcome::RefusedRevoked);
1871 }
1872 g.pushed.insert(record.request_uri.clone(), record);
1873 Ok(WriteOutcome::Applied)
1874 }
1875
1876 #[cfg(feature = "par")]
1877 async fn take_pushed_authorization_request(
1878 &self,
1879 request_uri: &str,
1880 ) -> Result<Option<crate::par::PushedAuthorizationRequest>, StorageError> {
1881 // Atomic by construction, like every other `take_*` here: one mutex, one `remove`.
1882 Ok(self.lock().pushed.remove(request_uri))
1883 }
1884
1885 async fn put_token(&self, token: IssuedToken) -> Result<WriteOutcome, StorageError> {
1886 let mut g = self.lock();
1887 // The check and the write under ONE guard: that is what makes this atomic rather than a
1888 // look followed by a hopeful insert. A revocation cannot land between them.
1889 if g.is_revoked(
1890 &token.client_id,
1891 token.family_id.as_deref(),
1892 token.subject.as_deref(),
1893 token.grant_established_at,
1894 ) {
1895 return Ok(WriteOutcome::RefusedRevoked);
1896 }
1897 // The `Arc` costs ONE allocation here, on issuance, and saves seven on every introspection
1898 // of the token afterwards. A token is issued once and introspected once per protected
1899 // request it is presented with, so the trade is measured in the direction that pays.
1900 g.tokens.insert(token.access_token.clone(), Arc::new(token));
1901 Ok(WriteOutcome::Applied)
1902 }
1903
1904 async fn get_token(
1905 &self,
1906 access_token: &str,
1907 ) -> Result<Option<Arc<IssuedToken>>, StorageError> {
1908 Ok(self.lock().tokens.get(access_token).cloned())
1909 }
1910
1911 async fn delete_token(&self, access_token: &str) -> Result<(), StorageError> {
1912 self.lock().tokens.remove(access_token);
1913 Ok(())
1914 }
1915
1916 async fn put_refresh_token(
1917 &self,
1918 record: RefreshTokenRecord,
1919 ) -> Result<WriteOutcome, StorageError> {
1920 let mut g = self.lock();
1921 // Same one guard, same one predicate. This is the write every refusal path of a rotation
1922 // makes, and the record it is putting back was removed by the caller's own take, so
1923 // absence proves nothing and the barrier is the only evidence available.
1924 if g.is_revoked(
1925 &record.client_id,
1926 Some(&record.family_id),
1927 record.subject.as_deref(),
1928 record.grant_established_at,
1929 ) {
1930 return Ok(WriteOutcome::RefusedRevoked);
1931 }
1932 g.refresh
1933 .insert(record.refresh_token.clone(), Arc::new(record));
1934 Ok(WriteOutcome::Applied)
1935 }
1936
1937 async fn get_refresh_token(
1938 &self,
1939 refresh_token: &str,
1940 ) -> Result<Option<Arc<RefreshTokenRecord>>, StorageError> {
1941 Ok(self.lock().refresh.get(refresh_token).cloned())
1942 }
1943
1944 async fn take_refresh_token(
1945 &self,
1946 refresh_token: &str,
1947 ) -> Result<Option<RefreshTokenRecord>, StorageError> {
1948 // Owned, because this is the rotation primitive: the record is GONE from the store and
1949 // "exactly one caller got it" has to be what the type says. `try_unwrap` reclaims the
1950 // record in place when nothing else is holding the snapshot, which is the ordinary case,
1951 // and falls back to a clone when a concurrent reader is still looking at it.
1952 Ok(self
1953 .lock()
1954 .refresh
1955 .remove(refresh_token)
1956 .map(|a| Arc::try_unwrap(a).unwrap_or_else(|a| (*a).clone())))
1957 }
1958
1959 async fn revoke_token_family(
1960 &self,
1961 family_id: &str,
1962 window: RevocationWindow,
1963 ) -> Result<u64, StorageError> {
1964 reject_empty_scope("family_id", family_id)?;
1965 // A scan is honest for a map with no secondary index, and this runs once per detected
1966 // compromise rather than per request. A host with a real database indexes `family_id`.
1967 let mut g = self.lock();
1968 // BEFORE the removals, under the same guard: the ordering is invisible here because the
1969 // guard makes the whole method one step, but it is the order a transactional store should
1970 // use too, so that a partial failure leaves the barrier rather than leaves the gap.
1971 g.record_barrier(
1972 RevocationBarrier::TokenFamily(family_id.into()),
1973 window.recorded_at,
1974 window.until,
1975 );
1976 let before = g.tokens.len() + g.refresh.len();
1977 g.tokens
1978 .retain(|_, t| t.family_id.as_deref() != Some(family_id));
1979 g.refresh.retain(|_, r| r.family_id != family_id);
1980 Ok((before - (g.tokens.len() + g.refresh.len())) as u64)
1981 }
1982
1983 #[cfg(feature = "consent")]
1984 async fn put_consent(&self, record: crate::consent::ConsentRecord) -> Result<(), StorageError> {
1985 self.lock()
1986 .consents
1987 .insert(record.consent_id.to_string(), Arc::new(record));
1988 Ok(())
1989 }
1990
1991 /// One guard again, and the comparison is against what `find_consent` would answer for the
1992 /// PAIR rather than against the `consent_id`: a withdrawal removes the record the caller read
1993 /// and any replacement has a different id, so comparing ids would miss the interleaving.
1994 #[cfg(feature = "consent")]
1995 async fn compare_and_swap_consent(
1996 &self,
1997 expected: Option<&crate::consent::ConsentRecord>,
1998 updated: crate::consent::ConsentRecord,
1999 ) -> Result<bool, StorageError> {
2000 let mut g = self.lock();
2001 let current = g
2002 .consents
2003 .values()
2004 .find(|c| c.client_id == updated.client_id && c.subject == updated.subject)
2005 .cloned();
2006 match (current.as_deref(), expected) {
2007 // Widening what we read, and it is still there unchanged.
2008 (Some(live), Some(expected)) if live == expected => {}
2009 // Creating, and the pair genuinely still has nothing. This is the half that closes the
2010 // duplicate-record race: the loser of two concurrent first approvals sees the winner's
2011 // record here and is refused.
2012 (None, None) => {}
2013 // Withdrawn, replaced, or created underneath us. Refuse and write nothing.
2014 _ => return Ok(false),
2015 }
2016 // A widen keeps the record's own id (see `record_consent`), so this replaces in place
2017 // rather than accumulating; a create inserts a fresh one.
2018 g.consents
2019 .insert(updated.consent_id.to_string(), Arc::new(updated));
2020 Ok(true)
2021 }
2022
2023 #[cfg(feature = "consent")]
2024 async fn get_consent(
2025 &self,
2026 consent_id: &str,
2027 ) -> Result<Option<Arc<crate::consent::ConsentRecord>>, StorageError> {
2028 Ok(self.lock().consents.get(consent_id).cloned())
2029 }
2030
2031 #[cfg(feature = "consent")]
2032 async fn find_consent(
2033 &self,
2034 client_id: &ClientId,
2035 subject: &str,
2036 ) -> Result<Option<Arc<crate::consent::ConsentRecord>>, StorageError> {
2037 // A scan, honestly, for a map with no secondary index; a host with a real database indexes
2038 // the pair, and the trait doc says so because this one IS on the authorization path.
2039 Ok(self
2040 .lock()
2041 .consents
2042 .values()
2043 .find(|c| &c.client_id == client_id && c.subject.as_ref() == subject)
2044 .cloned())
2045 }
2046
2047 #[cfg(feature = "consent")]
2048 async fn consents_for_subject(
2049 &self,
2050 subject: &str,
2051 ) -> Result<Vec<Arc<crate::consent::ConsentRecord>>, StorageError> {
2052 Ok(self
2053 .lock()
2054 .consents
2055 .values()
2056 .filter(|c| c.subject.as_ref() == subject)
2057 .cloned()
2058 .collect())
2059 }
2060
2061 #[cfg(feature = "consent")]
2062 async fn revoke_consent(
2063 &self,
2064 consent_id: &str,
2065 window: RevocationWindow,
2066 ) -> Result<u64, StorageError> {
2067 // The whole cascade under the ONE mutex, which is this store's version of the single
2068 // transaction the trait doc asks a real database for: no request can observe a
2069 // half-withdrawn consent, and nothing can be issued between the lookup and the sweep.
2070 let mut g = self.lock();
2071 // PEEKED rather than removed, so the scope check below can run before anything is
2072 // mutated: a refusal must leave the consent standing rather than withdraw it and then
2073 // decline to record the barrier that withdrawal depends on.
2074 let Some(peek) = g.consents.get(consent_id) else {
2075 // Already withdrawn, or never existed. Both are success; see the trait doc. No barrier
2076 // either, because there is no (client, subject) pair to name one for.
2077 return Ok(0);
2078 };
2079 reject_empty_scope("client_id", peek.client_id.as_str())?;
2080 reject_empty_scope("subject", peek.subject.as_ref())?;
2081 let consent = g
2082 .consents
2083 .remove(consent_id)
2084 .expect("the peek above holds the same guard");
2085 let client_id = &consent.client_id;
2086 let subject: &str = consent.subject.as_ref();
2087 // The cascade below can only reach what is in the store NOW. The barrier is what reaches
2088 // the issuance that is mid-flight for this pair and has not written yet.
2089 g.record_barrier(
2090 RevocationBarrier::Consent {
2091 client_id: client_id.clone(),
2092 subject: subject.into(),
2093 },
2094 window.recorded_at,
2095 window.until,
2096 );
2097 let before = g.tokens.len() + g.refresh.len() + g.codes.len() + g.device_by_code.len();
2098 g.tokens
2099 .retain(|_, t| !(&t.client_id == client_id && t.subject.as_deref() == Some(subject)));
2100 g.refresh
2101 .retain(|_, r| !(&r.client_id == client_id && r.subject.as_deref() == Some(subject)));
2102 // An unredeemed code is a grant in flight. Leaving it would let the client mint a token
2103 // seconds after the user withdrew, which is the withdrawal failing in the way nobody
2104 // notices until it matters.
2105 g.codes
2106 .retain(|_, c| !(&c.client_id == client_id && c.subject == subject));
2107 // Same for a device grant this user has already approved but the device has not polled for
2108 // yet. A PENDING one is left alone: nobody has consented to it, and killing it would end a
2109 // login the user may be in the middle of.
2110 g.device_by_code.retain(|_, d| {
2111 !(&d.client_id == client_id
2112 && matches!(&d.state, DeviceGrantState::Approved { subject: s } if s == subject))
2113 });
2114 // The user-code index points at grants rather than being a record of its own, so a dangling
2115 // entry would make a reaped code resolve to nothing. The same pass `sweep_expired` makes.
2116 let live = &g.device_by_code;
2117 let stale: Vec<String> = g
2118 .user_code_index
2119 .iter()
2120 .filter(|(_, dc)| !live.contains_key(*dc))
2121 .map(|(uc, _)| uc.clone())
2122 .collect();
2123 for uc in stale {
2124 g.user_code_index.remove(&uc);
2125 }
2126 let after = g.tokens.len() + g.refresh.len() + g.codes.len() + g.device_by_code.len();
2127 Ok((before - after) as u64)
2128 }
2129
2130 #[cfg(any(feature = "client-assertion", feature = "dpop"))]
2131 async fn claim_replay_id(
2132 &self,
2133 id: &str,
2134 expires_at: std::time::SystemTime,
2135 ) -> Result<bool, StorageError> {
2136 // Atomic by construction: the whole claim happens under the one mutex, so two concurrent
2137 // presentations of the same identifier cannot both observe it absent. The `id` is only
2138 // allocated when the claim is actually taken, which keeps a replayed request from costing
2139 // an allocation as well as a lookup.
2140 let mut g = self.lock();
2141 if g.replay_ids.contains_key(id) {
2142 return Ok(false);
2143 }
2144 g.replay_ids.insert(id.to_string(), expires_at);
2145 Ok(true)
2146 }
2147
2148 async fn sweep_expired(&self, now: std::time::SystemTime) -> Result<u64, StorageError> {
2149 let mut g = self.lock();
2150 let mut removed = 0u64;
2151
2152 // Device grants first, so the index pass below sees the survivors.
2153 let before = g.device_by_code.len();
2154 g.device_by_code.retain(|_, grant| now < grant.expires_at);
2155 removed += (before - g.device_by_code.len()) as u64;
2156 // The index is not counted separately: it is not a record, it is a pointer to one, and a
2157 // dangling pointer here would make a reaped user code resolve to nothing.
2158 let live = &g.device_by_code;
2159 let stale: Vec<String> = g
2160 .user_code_index
2161 .iter()
2162 .filter(|(_, dc)| !live.contains_key(*dc))
2163 .map(|(uc, _)| uc.clone())
2164 .collect();
2165 for uc in stale {
2166 g.user_code_index.remove(&uc);
2167 }
2168
2169 let before = g.codes.len();
2170 g.codes.retain(|_, c| now < c.expires_at);
2171 removed += (before - g.codes.len()) as u64;
2172
2173 // RFC 9126 s4: an expired request_uri MUST be rejected, and once it is expired there is
2174 // nothing left to recognise it for, so it is swept like anything else. A swept handle and
2175 // a used one are the same answer at the authorization endpoint, deliberately.
2176 #[cfg(feature = "par")]
2177 {
2178 let before = g.pushed.len();
2179 g.pushed.retain(|_, p| now < p.expires_at);
2180 removed += (before - g.pushed.len()) as u64;
2181 }
2182
2183 let before = g.tokens.len();
2184 g.tokens.retain(|_, t| now < t.expires_at);
2185 removed += (before - g.tokens.len()) as u64;
2186
2187 // `None` means the chain has no absolute lifetime, so it is not dead. A SPENT record from
2188 // such a chain was stamped with a retention deadline at rotation, which is what lets this
2189 // reclaim it (see `RefreshTokenRecord::expires_at`).
2190 let before = g.refresh.len();
2191 g.refresh.retain(|_, r| match r.expires_at {
2192 Some(exp) => now < exp,
2193 None => true,
2194 });
2195 removed += (before - g.refresh.len()) as u64;
2196
2197 // The replay set is the one collection here that an unauthenticated caller can grow: every
2198 // refused-but-well-formed assertion or proof adds an entry. It is bounded by the artifact
2199 // lifetime caps in `client_assertion.rs` and `dpop.rs`, but only a sweep actually reclaims
2200 // it, exactly as for everything else in this store.
2201 #[cfg(any(feature = "client-assertion", feature = "dpop"))]
2202 {
2203 let before = g.replay_ids.len();
2204 g.replay_ids.retain(|_, exp| now < *exp);
2205 removed += (before - g.replay_ids.len()) as u64;
2206 }
2207
2208 // Barriers last, and COUNTED: unlike the user-code index this is a row nothing else ever
2209 // removes, one per revocation, so a store that never reclaimed them would grow with every
2210 // logout. `now < until` REAPS a barrier whose deadline is exactly now — the retain keeps
2211 // only what is strictly ahead of the sweep — which is the same "dead at `now`" boundary
2212 // every other kind above uses, the boundary the trait states, and the one
2213 // `oauth-as-postgres` implements as `expires_at_ns <= $1`. The comment here said the
2214 // opposite until the 0.9.1 audit, describing a store that keeps it; a host implementing
2215 // the sweep from this reference would have had the two bundled stores disagreeing about
2216 // the instant a barrier stops standing.
2217 //
2218 // COUNTED PER SCOPE, not per map key: one key can hold a client barrier, a family barrier
2219 // and any number of consent barriers (see `ScopeBarriers`), and what the trait doc says is
2220 // counted is barriers. A key whose last scope was reaped is then dropped, so the map does
2221 // not keep a row per identity that was ever revoked.
2222 g.barriers.retain(|_, scopes| {
2223 if scopes.client.is_some_and(|t| now >= t.until) {
2224 scopes.client = None;
2225 removed += 1;
2226 }
2227 if scopes.family.is_some_and(|t| now >= t.until) {
2228 scopes.family = None;
2229 removed += 1;
2230 }
2231 let before = scopes.consents.len();
2232 scopes.consents.retain(|_, t| now < t.until);
2233 removed += (before - scopes.consents.len()) as u64;
2234 !scopes.is_empty()
2235 });
2236
2237 Ok(removed)
2238 }
2239}