Skip to main content

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}