Skip to main content

oauth_as/
events.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! The host seams this library cannot fill for itself: an AUDIT EVENT channel and a RATE LIMITING
5//! decision point, plus the one slot ([`Hooks`]) the server carries for both of them and for the
6//! client secret verifier ([`crate::client::SecretVerifier`]).
7//!
8//! # Why these live here and not in the host's own code
9//!
10//! Both answer questions only the library can ask and only the host can answer.
11//!
12//! - OBSERVATION. This crate revokes a whole token family when it detects authorization code
13//!   replay (RFC 9700 section 4.1.1) or refresh token reuse (OAuth 2.1 draft section 6.1, RFC 9700
14//!   section 4.14.2). Those are the crate's most serious security behaviours and their entire
15//!   value is that somebody NOTICES: a revocation that appears in no log is an incident nobody
16//!   investigates. Nothing outside the library can see either event, because the evidence (a
17//!   consumed code presented twice, a spent refresh record presented again) exists only inside the
18//!   grant machinery.
19//! - THROTTLING. RFC 8628 section 5.1 makes the device user code's entropy adequate only IN
20//!   COMBINATION WITH rate limiting of code entry. The library knows an attempt happened and
21//!   whether it failed; it does NOT know the caller, the IP, the session or the user, because it
22//!   never sees a request. So the library asks and reports, and the host counts and decides.
23//!
24//!   That reasoning explains why this is a SEAM. It never justified shipping the seam EMPTY, and
25//!   as of 0.9.0 the crate does not: [`crate::rate_limit::FixedWindowRateLimiter`] is a counter a
26//!   host installs in one line, with defaults derived from the section 5.1 arithmetic. It is a
27//!   floor rather than a ceiling (it is per process, so on a multi-node deployment the effective
28//!   limit is multiplied by the node count); its module docs say plainly what it cannot do.
29//!
30//! # Zero cost until enabled
31//!
32//! The crate doc promises a host that never turns something on pays nothing for it, and this
33//! module is built to keep that promise structurally rather than by intention:
34//!
35//! - [`Hooks`] is ONE pointer wide. The seams behind it are four in a default build (an
36//!   [`EventSink`], a [`RateLimiter`], a [`crate::client::SecretVerifier`] and a
37//!   [`crate::registration::RegistrationPolicy`]) and six with `jar` and `jwt` (a
38//!   `RequestObjectKeys` and an `Es256Verifier`). Held as separate `Option<Box<dyn _>>` fields they
39//!   would be 16 bytes each on every [`crate::server::AuthorizationServer`] value, so 64 bytes paid
40//!   by every host and 96 by one enabling both features; instead they live inside a boxed struct
41//!   that is not allocated at all until something is installed.
42//!
43//!   The registration policy is worth naming rather than counting, because it is the seam whose
44//!   ABSENCE is the security behaviour: with none installed, every RFC 7591 registration is REFUSED
45//!   (see [`Hooks::registration_policy`]), which is the opposite default to the rate limiter's.
46//! - [`Hooks::emit`] takes a CLOSURE, not an [`Event`]. With no sink installed the closure is
47//!   never called, so the event is never built: no allocation, no formatting, no vtable dispatch,
48//!   just one null check on a pointer that is already in cache. `tests/events.rs` measures exactly
49//!   that with a counting allocator and with a closure that panics if it is ever run.
50//! - Events are delivered SYNCHRONOUSLY, on the calling task. This crate has no background task by
51//!   design (see the crate docs) and does not gain one here; a host that wants buffering owns a
52//!   channel and writes three lines of [`EventSink`].
53//!
54//! # What events may carry, and what they may never carry
55//!
56//! Security finding C13 hand-wrote `Debug` on every type in this crate that holds a credential, so
57//! that a host's `tracing::debug!(?request)` could not become a plaintext credential leak. An event
58//! channel is a second way out of the process for the same values, and it goes to the same logs,
59//! so it is held to the same rule: NO access token, NO refresh token, NO authorization code, NO
60//! device code, NO user code, NO client secret, NO PKCE verifier. `tests/events.rs` scans the
61//! [`Event`] declaration and fails if a field named after one of those appears.
62//!
63//! What events DO carry is what an incident response needs to act:
64//!
65//! - `client_id`, which RFC 6749 section 2.2 states is not a secret.
66//! - the `subject`, where there is one. This is the host's own user identifier, which the host
67//!   already put into this crate; it is not a credential (holding it authenticates nobody), though
68//!   a host in a privacy-regulated setting may want to treat it as personal data in its logs.
69//! - the `family_id` of a refresh chain. This one is worth justifying, because it is the only
70//!   opaque server-minted string in the whole set. It is SAFE to log because it is not a
71//!   credential in any sense the protocol recognises: it is accepted at no endpoint, it appears in
72//!   no request and no response, it is never given to a client, and possessing it lets nobody
73//!   obtain, refresh or introspect a token. Its only power is to NAME a set of records in the
74//!   host's own store, which is precisely the correlation an operator needs to answer "what else
75//!   did this compromised grant issue" and to call [`crate::store::Storage::revoke_token_family`]
76//!   by hand. The alternative, logging the tokens themselves, is the leak this rule exists to
77//!   prevent; the alternative of logging nothing makes the revocation untraceable.
78
79use crate::client::SecretVerifier;
80#[cfg(feature = "client-assertion")]
81use crate::client_assertion::AssertionFailure;
82#[cfg(feature = "dpop")]
83use crate::dpop::DpopFailure;
84use crate::error::ErrorCode;
85use crate::grant::GrantType;
86use crate::registration::RegistrationPolicy;
87use crate::scope::ScopeSet;
88use crate::token::TokenTypeHint;
89
90/// Why a client failed to authenticate (RFC 6749 section 5.2 `invalid_client`).
91///
92/// The WIRE collapses all of these into one `invalid_client`, deliberately, so an attacker cannot
93/// probe which client ids exist. The AUDIT channel separates them just as deliberately: the host
94/// is not the attacker, and "a thousand unknown client ids" and "a thousand wrong secrets for one
95/// real client" are different incidents with different responses.
96///
97/// Shared by BOTH planes: it is the reason carried by
98/// [`Event::ClientAuthenticationFailed`] (the token plane) and by
99/// [`Event::ClientRegistrationAuthenticationFailed`] (the RFC 7592 management plane). One
100/// vocabulary rather than two, because a host counting credential guesses wants to count the same
101/// shapes wherever they happen; WHICH plane an attempt arrived on is the event variant, not this
102/// enum, so a sink can separate them without having to learn a second set of names.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
104#[non_exhaustive]
105pub enum ClientAuthFailure {
106    /// No registration exists for the presented `client_id`.
107    UnknownClient,
108    /// The registration exists and the presented credential did not verify (or none was presented
109    /// for a confidential client, or one was presented for a public one).
110    SecretMismatch,
111    /// The host's own [`RateLimiter`] refused the attempt before it was evaluated.
112    RateLimited,
113    /// The registration is dynamic (RFC 7591) and its `client_secret_expires_at` has passed, so the
114    /// secret is no longer a credential however correct it is.
115    ///
116    /// Separated from [`ClientAuthFailure::SecretMismatch`] because the response differs and the
117    /// urgency differs. This is not an attack: it is a client that missed a rotation window this
118    /// server announced when it registered them, and the fix is to re-register rather than to
119    /// investigate. A run of these after a rotation deadline is expected; a run of them before one
120    /// means the deployment's clock or its issued lifetimes are wrong.
121    SecretExpired,
122    /// The registration is PUBLIC and the endpoint the caller reached admits confidential clients
123    /// only: RFC 7662 section 2.1 introspection, and the RFC 6749 section 4.4 client credentials
124    /// grant.
125    ///
126    /// Not a wrong credential — no credential was ever in play. A public registration has no
127    /// secret, so "authenticated as a public client" is a sentence true of every caller on the
128    /// internet, and naming a client id is not authentication.
129    ///
130    /// IT EXISTS SO THAT THE WIRE DOES NOT HAVE TO SAY IT. Through 0.9.1 both endpoints answered
131    /// this case with an `invalid_client` CARRYING A DESCRIPTION ("introspection requires a
132    /// confidential client"), while an unknown client id and a confidential client with the wrong
133    /// secret both got a BARE `invalid_client` — so the description sorted "this id is registered,
134    /// and it is public" from everything else, which is the enumeration
135    /// [`ClientAuthFailure::UnknownClient`] and [`ClientAuthFailure::SecretMismatch`] are collapsed
136    /// on the wire to prevent. The description is gone; the fact is here instead, in the channel
137    /// where the reader is not the attacker. It is also the sentence an operator actually needs,
138    /// because the usual cause is a resource server registered with the wrong
139    /// `token_endpoint_auth_method` rather than an attack.
140    NotConfidential,
141    /// MANAGEMENT PLANE ONLY. The `client_id` names a client the HOST provisioned itself, which
142    /// carries no RFC 7591 registration record and therefore no registration access token that
143    /// could ever verify.
144    ///
145    /// Separated from [`ClientAuthFailure::UnknownClient`] because it says something that one does
146    /// not: the client id was REAL. A run of these is somebody walking a deployment's static client
147    /// ids looking for one that happens to be dynamically registered and therefore rewritable
148    /// through RFC 7592 section 2.2; a run of `UnknownClient` is somebody who has not found a live
149    /// id yet. The wire tells the caller neither (both are the same `401`).
150    NoDynamicRegistration,
151    /// The registration authenticates with RFC 8705 mutual TLS and NO certificate reached
152    /// this crate. Worth separating from a mismatch: in practice it usually means the TLS
153    /// terminator is not configured to request, verify or forward a client certificate, which
154    /// is an operational fault affecting every mutual-TLS client at once rather than an
155    /// attack on one of them.
156    #[cfg(feature = "mtls")]
157    NoCertificatePresented,
158    /// A certificate was presented and did not match the registration (RFC 8705 section 2.1
159    /// subject values, or section 2.2 thumbprints). This one IS the attack shape: a caller
160    /// holding some valid certificate trying to be a client it is not.
161    #[cfg(feature = "mtls")]
162    CertificateMismatch,
163    /// The registration exists and an RFC 7523 client assertion was presented that did not verify:
164    /// a bad signature, an `alg` the registration does not use, an audience naming another server,
165    /// an expired assertion, or a `jti` that had already been spent.
166    ///
167    /// Separated from [`ClientAuthFailure::SecretMismatch`] because the responses differ. A run of
168    /// wrong secrets is credential stuffing; a run of REPLAYED assertions is somebody who has
169    /// captured a client's traffic, which is a different incident and a much worse one.
170    ///
171    /// CARRIES THE REASON, because collapsing the nine into one told the operator nothing they
172    /// could act on. `AssertionFailure` documents itself as existing "for the host's audit channel,
173    /// where the reader is not the attacker", and until 0.9.1 the server discarded it here, so a
174    /// burst of these was indistinguishable between clock skew on the client
175    /// (`Expired`/`NotYetValid`, fix NTP), a key rotation the registration did not follow
176    /// (`BadSignature`, fix the registration), and assertions captured at another authorization
177    /// server and replayed here (`WrongAudience`, an incident). The mutual-TLS arm beside it
178    /// already forwarded its failure verbatim, which is how the omission was found.
179    #[cfg(feature = "client-assertion")]
180    AssertionInvalid {
181        /// Which of the RFC 7523 section 3 checks the assertion failed. Never reaches the wire:
182        /// every one of them is the same `invalid_client` there.
183        reason: AssertionFailure,
184    },
185}
186
187/// The OPERATOR's sentence, never the client's. Everything here is what the wire deliberately
188/// refuses to distinguish (see the type's docs), so these strings must not reach a response body.
189impl std::fmt::Display for ClientAuthFailure {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.write_str(match self {
192            ClientAuthFailure::UnknownClient => "no registration for that client_id",
193            ClientAuthFailure::SecretMismatch => "the presented client credential did not verify",
194            ClientAuthFailure::RateLimited => "the host's rate limiter refused the attempt",
195            ClientAuthFailure::SecretExpired => {
196                "the registration's client_secret_expires_at has passed"
197            }
198            ClientAuthFailure::NotConfidential => {
199                "that client id is registered as a public client, and this endpoint admits \
200                 confidential clients only"
201            }
202            ClientAuthFailure::NoDynamicRegistration => {
203                "that client id exists but was provisioned by the host, so it has no registration \
204                 access token"
205            }
206            #[cfg(feature = "mtls")]
207            ClientAuthFailure::NoCertificatePresented => {
208                "the registration authenticates with mutual TLS and no certificate was presented"
209            }
210            #[cfg(feature = "mtls")]
211            ClientAuthFailure::CertificateMismatch => {
212                "the presented certificate is not one this registration authenticates with"
213            }
214            #[cfg(feature = "client-assertion")]
215            // The REASON is appended rather than dropped: `AssertionFailure` already writes one
216            // sentence per check, and this is the channel those sentences were written for.
217            ClientAuthFailure::AssertionInvalid { reason } => {
218                return write!(f, "the client assertion did not verify: {reason}")
219            }
220        })
221    }
222}
223
224/// It is the `Err` payload of `crate::mtls::authenticate_via_mtls`, so a host handling that with
225/// `?` or collecting it into a `Box<dyn Error>` needs this, exactly as `DpopFailure` and
226/// `AssertionFailure` do for theirs. (Plain text rather than intra-doc links: those two types are
227/// behind features this one is not, so a link would dangle in a default build.)
228impl std::error::Error for ClientAuthFailure {}
229
230/// Something the authorization server did, or refused to do, worth recording.
231///
232/// Every field borrows: an event costs no allocation to build, which is what lets a host with a
233/// sink installed pay only for what its sink chooses to keep. See the module docs for the rule on
234/// what may and may not appear here.
235///
236/// `#[non_exhaustive]`: later releases will add events, and adding one must not be a breaking change
237/// for a host that matched on this. The RFC 7591 and 7592 registration events below arrived exactly
238/// that way and are here now, as did [`Event::DpopProofRefused`], which this paragraph named as
239/// the next candidate until 0.9.1 added it.
240#[derive(Debug, Clone, PartialEq, Eq)]
241#[non_exhaustive]
242pub enum Event<'a> {
243    /// A client failed to authenticate at a token-plane endpoint.
244    ClientAuthenticationFailed {
245        /// The `client_id` presented, which may not name any registration.
246        client_id: &'a str,
247        /// Which of the two indistinguishable-on-the-wire failures this actually was.
248        failure: ClientAuthFailure,
249    },
250    /// An access token was issued (and possibly a refresh token with it).
251    TokenIssued {
252        /// The client the grant was issued to.
253        client_id: &'a str,
254        /// Which grant produced it.
255        grant_type: GrantType,
256        /// The resource owner, absent for `client_credentials` (RFC 6749 section 4.4 has none).
257        subject: Option<&'a str>,
258        /// The granted scope, borrowed rather than rendered: a sink that does not want it pays
259        /// nothing, and one that does can format it itself.
260        scope: &'a ScopeSet,
261        /// The refresh chain this issuance belongs to, when it has one. See the module docs for
262        /// why this identifier is safe to log.
263        family_id: Option<&'a str>,
264        /// Whether a refresh token was issued alongside the access token.
265        refresh_issued: bool,
266    },
267    /// A grant request was refused, with the RFC 6749 section 5.2 code the client was told.
268    GrantRefused {
269        /// The client that asked.
270        client_id: &'a str,
271        /// The grant it asked for.
272        grant_type: GrantType,
273        /// The error code that went back on the wire.
274        error: ErrorCode,
275    },
276    /// An RFC 9449 DPoP proof was refused, and WHY.
277    ///
278    /// No `client_id`, and its absence is the honest shape rather than an omission: the proof is
279    /// checked BEFORE anything authenticates (see `AuthorizationServer::verify_dpop`), because it
280    /// binds to the REQUEST rather than to the grant, so at the moment this fires there is no
281    /// established identity to name. A host that wants to correlate has its own request context,
282    /// exactly as [`Attempt::DeviceUserCodeEntry`] requires.
283    ///
284    /// Every one of these is the same `invalid_dpop_proof` on the wire. The distinction is the
285    /// operator's, and `DpopFailure` documents itself as existing for exactly that; through 0.9.0
286    /// the server discarded it and emitted NO event at all, so a deployment could not tell a
287    /// client with a skewed clock from one whose proofs were being captured and replayed.
288    #[cfg(feature = "dpop")]
289    DpopProofRefused {
290        /// Which of the RFC 9449 section 4.3 checks the proof failed.
291        failure: DpopFailure,
292    },
293    /// A user approved a device grant at the host's verification UI (RFC 8628 section 3.3).
294    DeviceGrantApproved {
295        /// The device's client.
296        client_id: &'a str,
297        /// The user who approved it.
298        subject: &'a str,
299    },
300    /// A user refused a device grant, which the device will next see as `access_denied`.
301    DeviceGrantDenied {
302        /// The device's client.
303        client_id: &'a str,
304    },
305    /// EVIDENCE OF COMPROMISE. An authorization code was presented after it had already been
306    /// redeemed (RFC 6749 section 4.1.2, RFC 9700 section 4.1.1). The server has refused the
307    /// replay and revoked what the code minted; a code is a value that leaks into logs, `Referer`
308    /// headers and browser history, so a replay means either a leak or an attack in progress.
309    AuthorizationCodeReplayDetected {
310        /// The client the code was issued to.
311        client_id: &'a str,
312        /// The refresh family that was revoked, when the code's chain was still reachable.
313        family_id: Option<&'a str>,
314        /// Whether the refresh family was actually revoked (a chain already swept leaves nothing to
315        /// kill, and a store that refuses the revocation kills nothing either).
316        tokens_revoked: bool,
317        /// Whether ANY step of the compromise response failed to persist: the family revocation,
318        /// the fallback deletion of the access token the code minted, or the write that puts the
319        /// consumed code record back so the NEXT replay is still detectable.
320        ///
321        /// This exists because the wire cannot carry the news. A replayed code is answered
322        /// `invalid_grant` however badly the store is behaving, since the party being answered is
323        /// whoever holds the leaked code, so this event is the ONLY signal a deployment gets that a
324        /// code was replayed and the only place the truth about what was done can be told. An event
325        /// that overstates the containment is worse than no event: it is what an operator reads
326        /// while deciding not to investigate.
327        ///
328        /// `true` means credentials the server intended to destroy may still be live. Treat it as
329        /// an incident that needs a human, not as a storage warning.
330        containment_failed: bool,
331    },
332    /// EVIDENCE OF COMPROMISE, and the most serious event here. A superseded refresh token was
333    /// presented, which means two parties hold it (OAuth 2.1 draft section 6.1, RFC 9700 section
334    /// 4.14.2). The whole family has been revoked, which also logs out the legitimate client, so
335    /// this is an event a host will be asked about.
336    RefreshTokenReuseDetected {
337        /// The client that presented the spent token.
338        client_id: &'a str,
339        /// The revoked family. See the module docs for why this is safe to log.
340        family_id: &'a str,
341        /// How many records the family revocation removed. `0` when the revocation itself failed,
342        /// which `containment_failed` is what tells apart from a family that was already swept.
343        records_revoked: u64,
344        /// Whether the family revocation failed to persist, exactly as on
345        /// [`Event::AuthorizationCodeReplayDetected`].
346        ///
347        /// The reuse is real and is reported either way: the token was consumed before this was
348        /// judged, so the server cannot un-know that two parties hold it, and the presenter is
349        /// answered `invalid_grant` whatever the store did. This event is therefore the ONLY place
350        /// the truth about what was actually done can be told, and an event that overstates the
351        /// containment is worse than no event — it is what an operator reads while deciding not to
352        /// investigate.
353        ///
354        /// `true` means the compromised grant's tokens ARE STILL LIVE, up to their own expiry: the
355        /// access tokens the family issued still authorize, and the refresh chain the thief rotated
356        /// away can still be rotated again. The presented token's `Spent` record is put back so a
357        /// further presentation is still detected as reuse, but nothing was revoked. Treat it as an
358        /// incident that needs a human, not as a storage warning.
359        containment_failed: bool,
360    },
361    /// A token was revoked through the RFC 7009 endpoint.
362    TokenRevoked {
363        /// The client that revoked it (section 2.1 requires it to be the owner).
364        client_id: &'a str,
365        /// Which kind was removed.
366        token_type: TokenTypeHint,
367        /// The presented token IS revoked; this says whether the RFC 7009 section 2.1 SHOULD that
368        /// follows it succeeded. Revoking a refresh token also invalidates the access tokens of the
369        /// same grant, and that cascade is deliberately non-fatal: turning a completed revocation
370        /// into an error would tell an honest client nothing happened when the token it named is
371        /// already gone. Non-fatal is not the same as unreported, and `true` here means the
372        /// client's access tokens from that grant are still live for up to one access token TTL.
373        ///
374        /// Always `false` for an access token: there is no grant-wide cascade to attempt.
375        cascade_failed: bool,
376    },
377    /// A resource owner WITHDREW a consent, and everything issued under it was revoked.
378    ///
379    /// The one event in this set a USER causes rather than a client, and it is worth recording for
380    /// the same reason the two compromise events are: the cascade logs a client out of an account
381    /// it was in the middle of working with, so a host will be asked what happened.
382    /// `records_revoked` is how many tokens, codes and approved device grants the withdrawal
383    /// actually removed, which is what distinguishes "the user ended a live session" from "the
384    /// user revoked something that had already expired".
385    #[cfg(feature = "consent")]
386    ConsentWithdrawn {
387        /// The client that may no longer act for this user.
388        client_id: &'a str,
389        /// The user who withdrew it.
390        subject: &'a str,
391        /// How many records the cascade removed.
392        records_revoked: u64,
393    },
394    /// A client registered itself through RFC 7591 dynamic client registration.
395    ///
396    /// Worth watching even in a deployment that meant to enable this: RFC 7591 section 5 is
397    /// explicit that an open registration endpoint lets anyone create clients, so the RATE of this
398    /// event is the signal that an abuse policy is not holding. Carries no credential, so neither
399    /// the issued client secret nor the registration access token reaches the host's logs.
400    ClientRegistered {
401        /// The identifier the server minted.
402        client_id: &'a str,
403    },
404    /// A caller failed to authenticate at the RFC 7592 registration MANAGEMENT endpoint.
405    ///
406    /// The sibling of [`Event::ClientAuthenticationFailed`], and separate from it on purpose: the
407    /// two planes are guessed at for different reasons, and a sink that could not tell them apart
408    /// would report a management-plane brute force as token-endpoint noise. What is being guessed
409    /// here is the registration access token, and RFC 7592 section 2.2 lets its holder replace the
410    /// whole metadata document INCLUDING `redirect_uris`, which is where this client's
411    /// authorization codes are delivered. A landed guess is therefore not "an attacker can act as
412    /// this client"; it is "an attacker can have this client's codes sent to a URI they chose".
413    ///
414    /// Emitted for all four refusals of a management request, which the wire deliberately cannot
415    /// tell apart (they are one `401`, because distinguishing them is an enumeration oracle over
416    /// the client table): an attempt this host's [`RateLimiter`] denied before the store was
417    /// touched ([`ClientAuthFailure::RateLimited`]), an unknown `client_id`
418    /// ([`ClientAuthFailure::UnknownClient`]), a host-provisioned client that has no registration
419    /// at all ([`ClientAuthFailure::NoDynamicRegistration`]), and a registration access token that
420    /// did not verify ([`ClientAuthFailure::SecretMismatch`]).
421    ///
422    /// The limited one is emitted but NOT recorded as an attempt: the attempt never happened, so
423    /// there is no outcome to report and charging it would bill the caller twice for one try.
424    ///
425    /// Carries no credential: not the presented token, not a prefix of it, not its length. See the
426    /// module docs for the rule.
427    ClientRegistrationAuthenticationFailed {
428        /// The `client_id` the request named, which may name no registration. Not a secret
429        /// (RFC 6749 section 2.2).
430        client_id: &'a str,
431        /// Which of the four indistinguishable-on-the-wire refusals this actually was.
432        failure: ClientAuthFailure,
433    },
434    /// A registration was rewritten through RFC 7592 section 2.2.
435    ///
436    /// An update can change the redirect URIs, which is the whole of where this client's
437    /// authorization codes may be delivered, so it deserves the same attention as the original
438    /// registration.
439    ClientRegistrationUpdated {
440        /// The registration that changed.
441        client_id: &'a str,
442    },
443    /// A registration was deleted through RFC 7592 section 2.3, taking with it every token,
444    /// refresh chain and outstanding authorization code it held.
445    ClientRegistrationDeleted {
446        /// The registration that is now gone.
447        client_id: &'a str,
448    },
449    /// The host's own [`crate::registration::RegistrationPolicy`] refused a registration or an
450    /// update.
451    ///
452    /// Distinct from [`Event::ClientRegistrationAuthenticationFailed`], and the distinction is the
453    /// point: that one is somebody who could not prove who they are, this one is somebody who
454    /// DID and was then told no. On the management plane the caller has already presented a valid
455    /// registration access token, so a stream of these is a client with a working credential
456    /// repeatedly attempting something the deployment's policy forbids, which is a different
457    /// investigation from a brute force and was previously invisible in both directions.
458    ///
459    /// It is also the only signal a host gets that its own policy is doing anything at all. The
460    /// wire answer is a bare `401` chosen so that a policy refusing on CONTENT does not confirm
461    /// what content it dislikes (see `register_dynamic_client`), which means the operator learns
462    /// nothing from it either.
463    ///
464    /// Carries no credential: not the initial access token, not the registration access token, and
465    /// not the rejected metadata document, which is attacker-supplied text of the host's own
466    /// choosing to log or not.
467    ClientRegistrationRefusedByPolicy {
468        /// The registration being updated, for an RFC 7592 section 2.2 management request. `None`
469        /// for an initial RFC 7591 registration, which has no `client_id` yet: the refusal happens
470        /// before one is minted, deliberately, so that a refused attempt allocates nothing.
471        client_id: Option<&'a str>,
472    },
473}
474
475/// Where events go. The host implements this; the library never logs anything itself.
476///
477/// `on_event` is called SYNCHRONOUSLY, inside the request the host is already driving, and it takes
478/// `&self` so the sink is shared. Two consequences a host should design for: a slow sink slows the
479/// token endpoint, and a panicking sink panics the request. A host doing anything expensive should
480/// push onto a channel here and do the work elsewhere; this crate will not own that thread.
481pub trait EventSink: Send + Sync {
482    /// Record one event. Must not panic and should not block.
483    fn on_event(&self, event: Event<'_>);
484}
485
486/// Something a caller is attempting that a host may want to throttle.
487///
488/// Deliberately carries no credential: not the user code being tried (RFC 8628 section 6.1 makes
489/// it the credential a human types) and not the secret. It also cannot carry an IP or a session,
490/// because this library never sees a request; a host correlates using its own request context,
491/// which it still holds at the moment it calls into the server.
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
493#[non_exhaustive]
494pub enum Attempt<'a> {
495    /// A client is authenticating at a token-plane endpoint, or at the RFC 7592 registration
496    /// management plane. The `client_id` is included because RFC 6749 section 2.2 makes it
497    /// explicitly not a secret, so a limiter may key on it.
498    ///
499    /// ONE BUDGET FOR A CLIENT'S TWO BEARER CREDENTIALS, deliberately. The client secret and the
500    /// registration access token answer the same question — may this caller keep presenting
501    /// credentials as this `client_id` — and separate budgets would let an attacker stuffing the
502    /// management plane stay under a limiter watching the token endpoint, while the credential
503    /// they are guessing at is the more powerful of the two (RFC 7592 section 2.3 deletes the
504    /// registration and everything it was issued).
505    ///
506    /// IT ALSO CARRIES A RESOURCE SERVER'S INTROSPECTION TRAFFIC, and a limiter keying on this
507    /// needs to know that, because it is the one caller whose volume is not a function of anything
508    /// this server issued. A registration named in
509    /// [`crate::server::ServerConfig::resource_servers`] authenticates here once per RFC 7662
510    /// introspection, which is once per call at the protected resource it guards. There is
511    /// deliberately no introspection-specific variant: adding one to this `#[non_exhaustive]` enum
512    /// would land in the wildcard arm of every limiter already written, and a wildcard that
513    /// answers [`RateLimitDecision::Allow`] would silently stop throttling the endpoint on
514    /// upgrade. See "Resource servers introspect once per API call" in [`crate::rate_limit`] for
515    /// how to give one `client_id` a capacity of its own instead.
516    ClientAuthentication {
517        /// The presented identifier, which may name no registration.
518        client_id: &'a str,
519    },
520    /// A user code was entered at the host's verification UI
521    /// ([`crate::server::AuthorizationServer::approve_device`] /
522    /// [`crate::server::AuthorizationServer::deny_device`]). This is the attempt RFC 8628 section
523    /// 5.1 requires a deployment to rate limit.
524    DeviceUserCodeEntry,
525    /// A request at the AUTHORIZATION endpoint
526    /// ([`crate::server::AuthorizationServer::validate_authorization_request`], and again where an
527    /// approved request is turned into a code).
528    ///
529    /// The endpoint that takes NO CREDENTIAL at all, which is why it wants a variant of its own
530    /// rather than sharing [`Attempt::ClientAuthentication`]'s: there is nothing here for a limiter
531    /// counting credential guesses to count, and the `client_id` is the presented one (RFC 6749
532    /// section 2.2 makes it public), so a caller may name any registration they like. What a
533    /// deployment is bounding is work and STORAGE: every request costs a `get_client`, and an
534    /// approved one WRITES an authorization code record that nothing but
535    /// [`crate::store::Storage::sweep_expired`] reclaims.
536    ///
537    /// Checked twice on a completed flow, deliberately, because the two points cost different
538    /// things: the validation is a read, and the issuance is the write. A limiter that charged the
539    /// second to the first would let a caller who validates once issue without further charge.
540    AuthorizationRequest {
541        /// The `client_id` the request named, which may name no registration.
542        client_id: &'a str,
543    },
544    /// An RFC 7591 dynamic client registration
545    /// ([`crate::server::AuthorizationServer::register_dynamic_client`]).
546    ///
547    /// THE ONE UNBOUNDED WRITE IN THE CRATE, and the reason is that a [`crate::client::Client`] has
548    /// no expiry: no deadline, no sweep, nothing that ever reclaims one. Every other attacker-driven
549    /// record this server writes (a code, a token, a device grant, a replay `jti`) carries a
550    /// deadline and is reclaimed by [`crate::store::Storage::sweep_expired`], so an unthrottled
551    /// endpoint there is a burst. Here it is permanent growth, one row per request, forever.
552    ///
553    /// NO `client_id`, and its absence is deliberate rather than an oversight: at the moment this
554    /// is checked no identifier has been minted, and none is minted for a refusal, so a refused
555    /// registration allocates nothing at all. A limiter that wants to key on the caller has to use
556    /// its own request context, exactly as [`Attempt::DeviceUserCodeEntry`] requires.
557    ClientRegistration,
558}
559
560/// What a [`RateLimiter`] decided.
561#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
562pub enum RateLimitDecision {
563    /// Proceed.
564    Allow,
565    /// Refuse without evaluating the credential.
566    Deny,
567}
568
569/// How an attempt turned out, reported back so a limiter can count FAILURES rather than traffic.
570#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
571pub enum AttemptOutcome {
572    /// The credential verified / the code matched a live grant.
573    Succeeded,
574    /// It did not. This is the signal a guessing attack produces.
575    Failed,
576}
577
578/// The host's throttle. THIS LIBRARY DOES NOT RATE LIMIT ANYTHING, and cannot: it never sees a
579/// request, so it has no caller, no IP, no session and no user to count against.
580///
581/// RFC 8628 section 5.1 is explicit that the device user code's entropy is sufficient only in
582/// combination with rate limiting of user code entry, so for any deployment offering the device
583/// grant this is not optional in practice, only optional in the type system.
584///
585/// A HOST DOES NOT HAVE TO WRITE ONE. [`crate::rate_limit::FixedWindowRateLimiter`] is an
586/// implementation this crate ships, in memory, with no new dependency and with defaults derived
587/// from the section 5.1 arithmetic. Implement this trait yourself when you have something the
588/// library does not: a request IP, a session, a user, or a store shared across nodes.
589///
590/// # MUST NOT PANIC, and should not block
591///
592/// Both methods MUST answer for every input. There is no error channel and none is needed:
593/// [`RateLimitDecision::Allow`] and [`RateLimitDecision::Deny`] are both always available, and a
594/// limiter that cannot reach its shared counter should decide which of the two its deployment
595/// prefers rather than unwinding. This crate catches no unwind anywhere on a request path.
596///
597/// NAMING THE CONSEQUENCE, once per method, because the two sit at different points of a request:
598///
599/// - [`RateLimiter::check`] runs BEFORE any credential is evaluated, which is the whole point of
600///   it, and that puts it on the paths an UNAUTHENTICATED caller reaches: the authorization
601///   request, RFC 8628 section 5.1 user-code entry, and client authentication at the token
602///   endpoint. A panic here is remotely reachable by anyone who can open a socket, and it takes
603///   the request down before the throttle that would have limited how often they could try it.
604/// - [`RateLimiter::record`] runs AFTER the request has done its work, including after the store
605///   writes it drove. A panic here unwinds a request whose records are already written and whose
606///   response never reaches the client: the credential was spent, the client was told nothing, and
607///   the only place the two could have been reconciled was the response that was lost.
608///
609/// Blocking is the same argument one notch quieter. `check` is called inline on the caller's
610/// executor thread, so a limiter that waits on a network round trip to a shared counter adds that
611/// wait to every request, and on a current-thread runtime it adds it to every OTHER request too.
612pub trait RateLimiter: Send + Sync {
613    /// Decide whether `attempt` may proceed. Called BEFORE any credential is evaluated, so a
614    /// `Deny` costs the attacker a lookup and tells them nothing.
615    fn check(&self, attempt: Attempt<'_>) -> RateLimitDecision;
616
617    /// Report how an allowed attempt turned out. Defaults to doing nothing, so a host that only
618    /// wants a hard ceiling implements one method.
619    fn record(&self, attempt: Attempt<'_>, outcome: AttemptOutcome) {
620        let _ = (attempt, outcome);
621    }
622}
623
624/// The installed seams. Boxed as a unit (see [`Hooks`]) so that installing none of them allocates
625/// anything at all.
626#[derive(Default)]
627struct Installed {
628    events: Option<Box<dyn EventSink>>,
629    rate_limiter: Option<Box<dyn RateLimiter>>,
630    secret_verifier: Option<Box<dyn SecretVerifier>>,
631    registration_policy: Option<Box<dyn RegistrationPolicy>>,
632    #[cfg(feature = "jar")]
633    request_object_keys: Option<Box<dyn crate::par::RequestObjectKeys>>,
634    /// The host's ES256 backend for VERIFICATION (RFC 9449 DPoP proofs, RFC 9101 request objects,
635    /// RFC 7523 client assertions). `Arc` rather than `Box` because it is also what a host hands
636    /// to [`crate::signer_conformance`] and may share with its own resource-server half.
637    #[cfg(feature = "jwt")]
638    es256_verifier: Option<std::sync::Arc<dyn crate::jwt::Es256Verifier>>,
639}
640
641/// The server's slot for the host seams: exactly one pointer wide, and null until the host
642/// installs something.
643///
644/// This shape is the design decision the module docs argue for. Holding the seams as separate
645/// `Option<Box<dyn _>>` fields directly on [`crate::server::AuthorizationServer`] would add 16
646/// bytes each, 64 in a default build and 96 with `jar` and `jwt`, to every server value in every
647/// deployment, including every deployment that installs nothing, and
648/// `tests/allocation.rs` holds that type to a size budget precisely so a convenience like that
649/// cannot be paid for silently.
650#[derive(Default)]
651pub struct Hooks(Option<Box<Installed>>);
652
653impl Hooks {
654    /// An empty slot: nothing installed, nothing allocated.
655    pub fn new() -> Self {
656        Hooks(None)
657    }
658
659    fn installed(&mut self) -> &mut Installed {
660        // The one allocation this module can make, and only on a host's explicit install call.
661        self.0.get_or_insert_with(Default::default)
662    }
663
664    // THE INSTALLERS ARE `pub(crate)`, and the READERS below are `pub`. That asymmetry is the
665    // whole shape of this type: `Hooks` is a LAYOUT decision (one nullable pointer, size-gated in
666    // `tests/allocation.rs`), not a second installation API. A host installs through the six
667    // `AuthorizationServer::with_*` builders, which are the one verb for the one job; it reads
668    // through `AuthorizationServer::hooks()` when it wants to emit onto the same channel or
669    // consult its own limiter. Through 0.9.0 both spellings were public and only one of them was
670    // reachable, because `AuthorizationServer::new` builds its own `Hooks` and hands out only a
671    // shared reference: every in-tree caller of the `install_*` form was a test.
672
673    /// Install the audit sink, replacing any previous one.
674    pub(crate) fn install_event_sink(&mut self, sink: Box<dyn EventSink>) {
675        self.installed().events = Some(sink);
676    }
677
678    /// Install the rate limiter, replacing any previous one.
679    pub(crate) fn install_rate_limiter(&mut self, limiter: Box<dyn RateLimiter>) {
680        self.installed().rate_limiter = Some(limiter);
681    }
682
683    /// Install the client secret verifier, replacing any previous one.
684    pub(crate) fn install_secret_verifier(&mut self, verifier: Box<dyn SecretVerifier>) {
685        self.installed().secret_verifier = Some(verifier);
686    }
687
688    /// Install the RFC 7591 registration policy, replacing any previous one.
689    pub(crate) fn install_registration_policy(&mut self, policy: Box<dyn RegistrationPolicy>) {
690        self.installed().registration_policy = Some(policy);
691    }
692
693    /// Install the RFC 9101 request object verification keys, replacing any previous source.
694    #[cfg(feature = "jar")]
695    pub(crate) fn install_request_object_keys(
696        &mut self,
697        keys: Box<dyn crate::par::RequestObjectKeys>,
698    ) {
699        self.installed().request_object_keys = Some(keys);
700    }
701
702    /// Install the ES256 backend used to VERIFY signatures, replacing any previous one.
703    #[cfg(feature = "jwt")]
704    pub(crate) fn install_es256_verifier(
705        &mut self,
706        verifier: std::sync::Arc<dyn crate::jwt::Es256Verifier>,
707    ) {
708        self.installed().es256_verifier = Some(verifier);
709    }
710
711    /// The installed ES256 verifier, or `None`.
712    ///
713    /// `None` is NOT read as "accept anything": every caller refuses instead. Callers inside this
714    /// crate reach the verifier through a private resolver on `AuthorizationServer`, which is what
715    /// applies the `jwt-p256` fallback; this method reports only what the HOST installed, so that
716    /// fallback lives in exactly one place.
717    #[cfg(feature = "jwt")]
718    pub fn es256_verifier(&self) -> Option<&std::sync::Arc<dyn crate::jwt::Es256Verifier>> {
719        match &self.0 {
720            Some(installed) => installed.es256_verifier.as_ref(),
721            None => None,
722        }
723    }
724
725    /// The installed RFC 9101 request object key source.
726    ///
727    /// `None` is NOT read as "accept anything", for the same reason as
728    /// [`Hooks::registration_policy`] and the opposite of the [`RateLimiter`] default: a server
729    /// that cannot check a signature must refuse the request, because "cannot check" must never
730    /// read as "checked out".
731    #[cfg(feature = "jar")]
732    pub fn request_object_keys(&self) -> Option<&dyn crate::par::RequestObjectKeys> {
733        match &self.0 {
734            Some(installed) => installed.request_object_keys.as_deref(),
735            None => None,
736        }
737    }
738
739    /// Whether an event sink is installed.
740    ///
741    /// Call sites use this to decide whether to CLONE a value that is about to be consumed and
742    /// would otherwise be unavailable by the time the event can honestly be emitted (a client id
743    /// moved into a grant record, say). An unobserved host takes the `false` branch and pays
744    /// nothing; an observed one pays one small clone for the record it asked for.
745    pub fn is_observed(&self) -> bool {
746        match &self.0 {
747            Some(installed) => installed.events.is_some(),
748            None => false,
749        }
750    }
751
752    /// Emit an event, building it ONLY if a sink is installed.
753    ///
754    /// The closure is the whole point: see the module docs. With no sink this compiles down to a
755    /// null check and a return.
756    pub fn emit<'a, F>(&self, event: F)
757    where
758        F: FnOnce() -> Event<'a>,
759    {
760        if let Some(installed) = &self.0 {
761            if let Some(sink) = &installed.events {
762                sink.on_event(event());
763            }
764        }
765    }
766
767    /// Ask the host's limiter whether `attempt` may proceed. With none installed the answer is
768    /// [`RateLimitDecision::Allow`]: a library with no notion of a caller has no business
769    /// inventing a throttling policy, and failing closed here would break every host that has not
770    /// yet written one.
771    ///
772    /// "Allow" is therefore the answer for a host that installed nothing, and RFC 8628 section 5.1
773    /// says that host is running an under-protected verification endpoint. Install
774    /// [`crate::rate_limit::FixedWindowRateLimiter`] if you have nothing better.
775    pub fn check(&self, attempt: Attempt<'_>) -> RateLimitDecision {
776        match &self.0 {
777            Some(installed) => match &installed.rate_limiter {
778                Some(limiter) => limiter.check(attempt),
779                None => RateLimitDecision::Allow,
780            },
781            None => RateLimitDecision::Allow,
782        }
783    }
784
785    /// Report an outcome to the host's limiter, if any.
786    pub fn record(&self, attempt: Attempt<'_>, outcome: AttemptOutcome) {
787        if let Some(installed) = &self.0 {
788            if let Some(limiter) = &installed.rate_limiter {
789                limiter.record(attempt, outcome);
790            }
791        }
792    }
793
794    /// The installed client secret verifier, for [`crate::client::ClientAuth::verify_with`].
795    pub fn secret_verifier(&self) -> Option<&dyn SecretVerifier> {
796        match &self.0 {
797            Some(installed) => installed.secret_verifier.as_deref(),
798            None => None,
799        }
800    }
801
802    /// The installed RFC 7591 registration policy.
803    ///
804    /// `None` means the host installed none, and that is NOT read as "allow": see
805    /// [`RegistrationPolicy`]. It is the opposite of the [`RateLimiter`] default above, and
806    /// deliberately so. An absent limiter means the host has not written a throttling policy yet,
807    /// and refusing every request would break a host that never asked for throttling. An absent
808    /// registration policy means the host turned on an endpoint that mints clients and said
809    /// nothing about who may use it, and RFC 7591 section 5 is explicit about what an open one
810    /// costs.
811    pub fn registration_policy(&self) -> Option<&dyn RegistrationPolicy> {
812        match &self.0 {
813            Some(installed) => installed.registration_policy.as_deref(),
814            None => None,
815        }
816    }
817}
818
819#[cfg(test)]
820#[path = "tests/events.rs"]
821mod tests;