Skip to main content

oauth_as/
delegate.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! [`delegate_storage!`](crate::delegate_storage), the answer to "I want Postgres for clients and
5//! memory for codes".
6//!
7//! [`crate::store::Storage`] has no default method bodies, and its own docs argue at length for
8//! why: there is no method here whose obvious default is safe, and a defaulted one fails in
9//! production in the direction that loses credentials. The cost of that decision falls on the host
10//! who wants to specialise ONE method, because they then have to hand-write the other twenty-eight
11//! as forwarding calls.
12//!
13//! That forwarding is mechanical, so this macro writes it. It is the answer the trait's docs
14//! promise, and it lands after the 0.9.1 `Storage` break so that it forwards the FINAL method set
15//! rather than a set about to change.
16//!
17//! # Why the feature gating goes through `__oauth_as_if_*` and not through `#[cfg]`
18//!
19//! `macro_rules!` has hygiene for identifiers and NONE for `cfg`. An attribute a macro EMITS is
20//! evaluated where the macro EXPANDS, so a `#[cfg(feature = "par")]` written in this file and
21//! expanded into a host crate asks whether THE HOST has a feature called `par`. Almost no host
22//! does. rustc says this in as many words when it sees it: "using a cfg inside a macro will use the
23//! cfgs from the destination crate and not the ones from the defining crate".
24//!
25//! THIS SHIPPED WRONG AND IS WORTH RECORDING. Through 0.9.1 the nine feature-gated arms carried
26//! their `#[cfg]` inline, so in every real host crate they expanded to NOTHING, whatever features
27//! `oauth-as` itself had been built with. A host that turned on `consent` and `par` and listed all
28//! twenty-nine names got E0046 naming `put_pushed_authorization_request`,
29//! `take_pushed_authorization_request`, the six consent methods and `claim_replay_id`, and was
30//! pushed straight back to hand-writing the forwarders this macro exists to spare them.
31//!
32//! The fix moves the DECISION to a place `oauth-as` compiles: a pair of `#[macro_export]` macros per
33//! gate, selected by `#[cfg]` HERE, one passing its input through and one swallowing it. The gated
34//! arms then call `$crate::__oauth_as_if_par! { ... }`, which is an ordinary macro path and carries
35//! this crate's answer with it. Nothing is emitted into the host that the host has to be able to
36//! evaluate.
37//!
38//! Nothing else works. `cfg_attr` has the same problem for the same reason; `cfg!()` is an
39//! expression and cannot delete an item; a proc macro could read `CARGO_FEATURE_*` but that is a
40//! `syn` dependency in the ONE crate whose whole argument is that it has almost none.
41//!
42//! `crates/oauth-as-delegate-fixture` is the standing gate, and it has to be a separate crate: the
43//! doctest below CANNOT catch this, because rustdoc compiles doctests with the declaring crate's
44//! cfg flags, so the doctest and the bug agreed with each other for as long as both existed.
45
46// THE THREE GATES, resolved HERE and carried into the host as macro paths. See the module docs for
47// why this shape rather than an emitted `#[cfg]`. Each pair is exhaustive and mutually exclusive, so
48// exactly one definition of each name exists in any build, and `#[macro_export]` puts it at this
49// crate's root where `$crate::` can reach it.
50//
51// `#[doc(hidden)]`: these are an implementation detail of `delegate_storage!`, not API. They are
52// nonetheless PUBLIC macros with a stable-looking name, which is unavoidable (`macro_rules!` has no
53// `pub(crate)` for exported macros), hence the `__oauth_as_` prefix. Do not call them.
54
55/// Emits its input when `oauth-as` was built with `par`, and nothing otherwise.
56#[doc(hidden)]
57#[macro_export]
58#[cfg(feature = "par")]
59macro_rules! __oauth_as_if_par {
60    ($($item:tt)*) => { $($item)* };
61}
62
63/// Emits its input when `oauth-as` was built with `par`, and nothing otherwise.
64#[doc(hidden)]
65#[macro_export]
66#[cfg(not(feature = "par"))]
67macro_rules! __oauth_as_if_par {
68    ($($item:tt)*) => {};
69}
70
71/// Emits its input when `oauth-as` was built with `consent`, and nothing otherwise.
72#[doc(hidden)]
73#[macro_export]
74#[cfg(feature = "consent")]
75macro_rules! __oauth_as_if_consent {
76    ($($item:tt)*) => { $($item)* };
77}
78
79/// Emits its input when `oauth-as` was built with `consent`, and nothing otherwise.
80#[doc(hidden)]
81#[macro_export]
82#[cfg(not(feature = "consent"))]
83macro_rules! __oauth_as_if_consent {
84    ($($item:tt)*) => {};
85}
86
87/// Emits its input when `oauth-as` was built with `client-assertion` or `dpop`, and nothing
88/// otherwise. The two features share one gate because they share one method: RFC 7523 assertion
89/// replay and RFC 9449 DPoP proof replay are the same claim-if-absent operation.
90#[doc(hidden)]
91#[macro_export]
92#[cfg(any(feature = "client-assertion", feature = "dpop"))]
93macro_rules! __oauth_as_if_replay {
94    ($($item:tt)*) => { $($item)* };
95}
96
97/// Emits its input when `oauth-as` was built with `client-assertion` or `dpop`, and nothing
98/// otherwise.
99#[doc(hidden)]
100#[macro_export]
101#[cfg(not(any(feature = "client-assertion", feature = "dpop")))]
102macro_rules! __oauth_as_if_replay {
103    ($($item:tt)*) => {};
104}
105
106/// Forward the named [`crate::store::Storage`] methods to an inner store.
107///
108/// # Usage
109///
110/// Name the FIELD holding the inner store, then the methods to forward. Anything you do NOT list, you write
111/// yourself in the same `impl` block. Listing is EXPLICIT rather than "everything except", and
112/// that is deliberate: `macro_rules!` cannot subtract a set, and more to the point a host reading
113/// this call should be able to see which methods are their own without cross-referencing the
114/// trait. If you forget one, the compiler names it, which is exactly the signal the trait's
115/// no-defaults rule exists to produce.
116///
117/// ```
118/// use oauth_as::store::{MemoryStorage, Storage, StorageError, WriteOutcome};
119/// use oauth_as::{ClientId, IssuedToken};
120///
121/// /// Counts issuance, and is otherwise an ordinary in-memory store.
122/// struct CountingStore {
123///     inner: MemoryStorage,
124///     issued: std::sync::atomic::AtomicU64,
125/// }
126///
127/// impl Storage for CountingStore {
128///     // The one method this store is actually for.
129///     async fn put_token(&self, token: IssuedToken) -> Result<WriteOutcome, StorageError> {
130///         let outcome = self.inner.put_token(token).await?;
131///         if outcome.is_applied() {
132///             self.issued.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
133///         }
134///         Ok(outcome)
135///     }
136///
137///     // Everything else, including the feature-gated methods: each gated one is generated only
138///     // when `oauth-as` itself has the feature, so naming one you have not enabled produces
139///     // nothing rather than an error. Your OWN crate's features are not consulted and do not
140///     // need to exist.
141///     oauth_as::delegate_storage! {
142///         to inner;
143///         get_client, put_client, compare_and_swap_client, delete_client,
144///         put_device_grant, get_device_grant, find_device_grant_by_user_code,
145///         take_device_grant, compare_and_swap_device_grant,
146///         put_authorization_code, compare_and_swap_authorization_code,
147///         take_authorization_code,
148///         put_pushed_authorization_request, take_pushed_authorization_request,
149///         get_token, delete_token,
150///         put_refresh_token, get_refresh_token, take_refresh_token, revoke_token_family,
151///         put_consent, compare_and_swap_consent, get_consent, find_consent,
152///         consents_for_subject, revoke_consent,
153///         claim_replay_id,
154///         sweep_expired,
155///     }
156/// }
157/// ```
158///
159/// # What it does not do
160///
161/// It forwards. It does not make two stores atomic with respect to each other, and no macro
162/// could: if clients live in Postgres and codes live in memory, then
163/// [`crate::store::Storage::delete_client`]'s cascade spans both, and the trait requires that
164/// cascade to be ONE event. A host splitting a store across backends owns that problem, and
165/// [`crate::storage_conformance`] is how they find out whether they have solved it.
166///
167/// The feature-gated methods are forwarded only when the feature is on IN `oauth-as`, so a name
168/// listed under a feature you have not enabled is simply not generated. Listing one you do not have
169/// is not an error; it produces nothing.
170///
171/// "in `oauth-as`" is the load-bearing part and it has to be said, because the obvious reading is
172/// wrong and was ALSO what the macro did until this was fixed: the gate is NOT your crate's feature
173/// set. Your crate does not need a feature called `par` and almost certainly does not have one.
174/// Note also that this means cargo FEATURE UNIFICATION can turn a forwarder on: an unrelated
175/// dependency enabling `consent` enables it for your build of `oauth-as` too, and the macro will
176/// then generate `put_consent` and the rest. That is the correct outcome, since the trait grew the
177/// methods at the same moment, and it is the reason this macro is worth having.
178///
179/// # Why it takes a FIELD and not an expression
180///
181/// `to inner`, not `to self.inner`, and that is macro hygiene rather than taste. A `self` written
182/// at the call site is a different `self` from the one in the generated method, so an expression
183/// containing it does not resolve and the error (`expected value, found module self`) points at
184/// the macro rather than at anything a host can act on. Taking the field name lets the macro build
185/// `self.$f` itself, out of its own `self`. A tuple field works too: `to 0`.
186#[macro_export]
187macro_rules! delegate_storage {
188    (to $field:tt; $($method:ident),* $(,)?) => {
189        $($crate::delegate_storage!(@one $field, $method);)*
190    };
191
192    // ------------------------------------------------------------------------------- clients
193    (@one $f:tt, get_client) => {
194        async fn get_client(
195            &self,
196            client_id: &$crate::ClientId,
197        ) -> ::core::result::Result<
198            ::core::option::Option<::std::sync::Arc<$crate::Client>>,
199            $crate::store::StorageError,
200        > {
201            $crate::store::Storage::get_client(&self.$f, client_id).await
202        }
203    };
204    (@one $f:tt, put_client) => {
205        async fn put_client(
206            &self,
207            client: $crate::Client,
208        ) -> ::core::result::Result<(), $crate::store::StorageError> {
209            $crate::store::Storage::put_client(&self.$f, client).await
210        }
211    };
212    (@one $f:tt, compare_and_swap_client) => {
213        async fn compare_and_swap_client(
214            &self,
215            expected: &$crate::Client,
216            updated: $crate::Client,
217        ) -> ::core::result::Result<bool, $crate::store::StorageError> {
218            $crate::store::Storage::compare_and_swap_client(&self.$f, expected, updated).await
219        }
220    };
221    (@one $f:tt, delete_client) => {
222        async fn delete_client(
223            &self,
224            client_id: &$crate::ClientId,
225            window: $crate::store::RevocationWindow,
226        ) -> ::core::result::Result<bool, $crate::store::StorageError> {
227            $crate::store::Storage::delete_client(&self.$f, client_id, window).await
228        }
229    };
230
231    // -------------------------------------------------------------------------- device grants
232    (@one $f:tt, put_device_grant) => {
233        async fn put_device_grant(
234            &self,
235            grant: $crate::DeviceGrant,
236        ) -> ::core::result::Result<(), $crate::store::StorageError> {
237            $crate::store::Storage::put_device_grant(&self.$f, grant).await
238        }
239    };
240    (@one $f:tt, get_device_grant) => {
241        async fn get_device_grant(
242            &self,
243            device_code: &str,
244        ) -> ::core::result::Result<
245            ::core::option::Option<$crate::DeviceGrant>,
246            $crate::store::StorageError,
247        > {
248            $crate::store::Storage::get_device_grant(&self.$f, device_code).await
249        }
250    };
251    (@one $f:tt, find_device_grant_by_user_code) => {
252        async fn find_device_grant_by_user_code(
253            &self,
254            normalized_user_code: &str,
255        ) -> ::core::result::Result<
256            ::core::option::Option<$crate::DeviceGrant>,
257            $crate::store::StorageError,
258        > {
259            $crate::store::Storage::find_device_grant_by_user_code(&self.$f, normalized_user_code).await
260        }
261    };
262    (@one $f:tt, take_device_grant) => {
263        async fn take_device_grant(
264            &self,
265            device_code: &str,
266        ) -> ::core::result::Result<
267            ::core::option::Option<$crate::DeviceGrant>,
268            $crate::store::StorageError,
269        > {
270            $crate::store::Storage::take_device_grant(&self.$f, device_code).await
271        }
272    };
273    (@one $f:tt, compare_and_swap_device_grant) => {
274        async fn compare_and_swap_device_grant(
275            &self,
276            expected: &$crate::DeviceGrantState,
277            updated: $crate::DeviceGrant,
278        ) -> ::core::result::Result<bool, $crate::store::StorageError> {
279            $crate::store::Storage::compare_and_swap_device_grant(&self.$f, expected, updated).await
280        }
281    };
282
283    // --------------------------------------------------------------------- authorization codes
284    (@one $f:tt, put_authorization_code) => {
285        async fn put_authorization_code(
286            &self,
287            record: $crate::AuthorizationCodeRecord,
288        ) -> ::core::result::Result<(), $crate::store::StorageError> {
289            $crate::store::Storage::put_authorization_code(&self.$f, record).await
290        }
291    };
292    (@one $f:tt, compare_and_swap_authorization_code) => {
293        async fn compare_and_swap_authorization_code(
294            &self,
295            expected: &$crate::AuthorizationCodeState,
296            updated: $crate::AuthorizationCodeRecord,
297        ) -> ::core::result::Result<bool, $crate::store::StorageError> {
298            $crate::store::Storage::compare_and_swap_authorization_code(&self.$f, expected, updated)
299                .await
300        }
301    };
302    (@one $f:tt, take_authorization_code) => {
303        async fn take_authorization_code(
304            &self,
305            code: &str,
306        ) -> ::core::result::Result<
307            ::core::option::Option<$crate::AuthorizationCodeRecord>,
308            $crate::store::StorageError,
309        > {
310            $crate::store::Storage::take_authorization_code(&self.$f, code).await
311        }
312    };
313
314    // ----------------------------------------------------------------------------------- PAR
315    (@one $f:tt, put_pushed_authorization_request) => {
316        $crate::__oauth_as_if_par! {
317            async fn put_pushed_authorization_request(
318                &self,
319                record: $crate::par::PushedAuthorizationRequest,
320            ) -> ::core::result::Result<$crate::store::WriteOutcome, $crate::store::StorageError> {
321                $crate::store::Storage::put_pushed_authorization_request(&self.$f, record).await
322            }
323        }
324    };
325    (@one $f:tt, take_pushed_authorization_request) => {
326        $crate::__oauth_as_if_par! {
327            async fn take_pushed_authorization_request(
328                &self,
329                request_uri: &str,
330            ) -> ::core::result::Result<
331                ::core::option::Option<$crate::par::PushedAuthorizationRequest>,
332                $crate::store::StorageError,
333            > {
334                $crate::store::Storage::take_pushed_authorization_request(&self.$f, request_uri)
335                    .await
336            }
337        }
338    };
339
340    // -------------------------------------------------------------------------------- tokens
341    (@one $f:tt, put_token) => {
342        async fn put_token(
343            &self,
344            token: $crate::IssuedToken,
345        ) -> ::core::result::Result<$crate::store::WriteOutcome, $crate::store::StorageError> {
346            $crate::store::Storage::put_token(&self.$f, token).await
347        }
348    };
349    (@one $f:tt, get_token) => {
350        async fn get_token(
351            &self,
352            access_token: &str,
353        ) -> ::core::result::Result<
354            ::core::option::Option<::std::sync::Arc<$crate::IssuedToken>>,
355            $crate::store::StorageError,
356        > {
357            $crate::store::Storage::get_token(&self.$f, access_token).await
358        }
359    };
360    (@one $f:tt, delete_token) => {
361        async fn delete_token(
362            &self,
363            access_token: &str,
364        ) -> ::core::result::Result<(), $crate::store::StorageError> {
365            $crate::store::Storage::delete_token(&self.$f, access_token).await
366        }
367    };
368    (@one $f:tt, put_refresh_token) => {
369        async fn put_refresh_token(
370            &self,
371            record: $crate::RefreshTokenRecord,
372        ) -> ::core::result::Result<$crate::store::WriteOutcome, $crate::store::StorageError> {
373            $crate::store::Storage::put_refresh_token(&self.$f, record).await
374        }
375    };
376    (@one $f:tt, get_refresh_token) => {
377        async fn get_refresh_token(
378            &self,
379            refresh_token: &str,
380        ) -> ::core::result::Result<
381            ::core::option::Option<::std::sync::Arc<$crate::RefreshTokenRecord>>,
382            $crate::store::StorageError,
383        > {
384            $crate::store::Storage::get_refresh_token(&self.$f, refresh_token).await
385        }
386    };
387    (@one $f:tt, take_refresh_token) => {
388        async fn take_refresh_token(
389            &self,
390            refresh_token: &str,
391        ) -> ::core::result::Result<
392            ::core::option::Option<$crate::RefreshTokenRecord>,
393            $crate::store::StorageError,
394        > {
395            $crate::store::Storage::take_refresh_token(&self.$f, refresh_token).await
396        }
397    };
398    (@one $f:tt, revoke_token_family) => {
399        async fn revoke_token_family(
400            &self,
401            family_id: &str,
402            window: $crate::store::RevocationWindow,
403        ) -> ::core::result::Result<u64, $crate::store::StorageError> {
404            $crate::store::Storage::revoke_token_family(&self.$f, family_id, window).await
405        }
406    };
407
408    // ------------------------------------------------------------------------------- consent
409    (@one $f:tt, put_consent) => {
410        $crate::__oauth_as_if_consent! {
411            async fn put_consent(
412                &self,
413                record: $crate::ConsentRecord,
414            ) -> ::core::result::Result<(), $crate::store::StorageError> {
415                $crate::store::Storage::put_consent(&self.$f, record).await
416            }
417        }
418    };
419    (@one $f:tt, compare_and_swap_consent) => {
420        $crate::__oauth_as_if_consent! {
421            async fn compare_and_swap_consent(
422                &self,
423                expected: ::core::option::Option<&$crate::ConsentRecord>,
424                updated: $crate::ConsentRecord,
425            ) -> ::core::result::Result<bool, $crate::store::StorageError> {
426                $crate::store::Storage::compare_and_swap_consent(&self.$f, expected, updated).await
427            }
428        }
429    };
430    (@one $f:tt, get_consent) => {
431        $crate::__oauth_as_if_consent! {
432            async fn get_consent(
433                &self,
434                consent_id: &str,
435            ) -> ::core::result::Result<
436                ::core::option::Option<::std::sync::Arc<$crate::ConsentRecord>>,
437                $crate::store::StorageError,
438            > {
439                $crate::store::Storage::get_consent(&self.$f, consent_id).await
440            }
441        }
442    };
443    (@one $f:tt, find_consent) => {
444        $crate::__oauth_as_if_consent! {
445            async fn find_consent(
446                &self,
447                client_id: &$crate::ClientId,
448                subject: &str,
449            ) -> ::core::result::Result<
450                ::core::option::Option<::std::sync::Arc<$crate::ConsentRecord>>,
451                $crate::store::StorageError,
452            > {
453                $crate::store::Storage::find_consent(&self.$f, client_id, subject).await
454            }
455        }
456    };
457    (@one $f:tt, consents_for_subject) => {
458        $crate::__oauth_as_if_consent! {
459            async fn consents_for_subject(
460                &self,
461                subject: &str,
462            ) -> ::core::result::Result<
463                ::std::vec::Vec<::std::sync::Arc<$crate::ConsentRecord>>,
464                $crate::store::StorageError,
465            > {
466                $crate::store::Storage::consents_for_subject(&self.$f, subject).await
467            }
468        }
469    };
470    (@one $f:tt, revoke_consent) => {
471        $crate::__oauth_as_if_consent! {
472            async fn revoke_consent(
473                &self,
474                consent_id: &str,
475                window: $crate::store::RevocationWindow,
476            ) -> ::core::result::Result<u64, $crate::store::StorageError> {
477                $crate::store::Storage::revoke_consent(&self.$f, consent_id, window).await
478            }
479        }
480    };
481
482    // --------------------------------------------------------------------------------- replay
483    (@one $f:tt, claim_replay_id) => {
484        $crate::__oauth_as_if_replay! {
485            async fn claim_replay_id(
486                &self,
487                id: &str,
488                expires_at: ::std::time::SystemTime,
489            ) -> ::core::result::Result<bool, $crate::store::StorageError> {
490                $crate::store::Storage::claim_replay_id(&self.$f, id, expires_at).await
491            }
492        }
493    };
494
495    // ---------------------------------------------------------------------------------- sweep
496    (@one $f:tt, sweep_expired) => {
497        async fn sweep_expired(
498            &self,
499            now: ::std::time::SystemTime,
500        ) -> ::core::result::Result<u64, $crate::store::StorageError> {
501            $crate::store::Storage::sweep_expired(&self.$f, now).await
502        }
503    };
504}