Skip to main content

oauth_as/
client.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! Registered OAuth clients, mirrored from RFC 6749 section 2 with the OAuth 2.1 public /
5//! confidential split.
6
7use std::fmt;
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::grant::GrantType;
13// Aliased to the name this module used when it carried its own copy of the encoder, so the call
14// sites below (and `src/tests/client.rs`, which reaches the private helper) read unchanged. There
15// is one FUNCTION; `crate::hex` owns it, and `tests/hex_single_definition.rs` keeps it at one.
16use crate::hex::encode as hex_lower;
17use crate::scope::ScopeSet;
18
19/// A client identifier (RFC 6749 section 2.2): opaque to this crate, unique per registration.
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
21pub struct ClientId(String);
22
23impl ClientId {
24    /// Wrap an identifier.
25    pub fn new(id: impl Into<String>) -> Self {
26        ClientId(id.into())
27    }
28
29    /// The identifier text.
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl fmt::Display for ClientId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.write_str(&self.0)
38    }
39}
40
41/// A STORED VERIFIER for a client secret: enough to check a presented secret, never enough to
42/// present one.
43///
44/// This is what [`ClientAuth::ConfidentialSecretHash`] holds, and it is the shape a host should
45/// persist. RFC 6749 section 2.3.1 says the client secret is a password; a password at rest
46/// belongs in a one-way form, so that a dump of the client table is not a set of working
47/// credentials.
48///
49/// Two kinds of scheme:
50///
51/// - [`SecretHash::SHA256_HEX`], built by [`SecretHash::sha256`] and verified by this crate with
52///   no host code and no new dependency (`sha2` is already here for RFC 7636 PKCE). Plain SHA-256
53///   is the RIGHT primitive for this particular job and the wrong one for a user password: a
54///   client secret is high-entropy and host-generated, so there is no dictionary to run against
55///   it, and the offline-guessing threat that makes a slow KDF necessary for human-chosen
56///   passwords does not exist here. The comparison is constant time regardless, for the reason
57///   given on [`ClientAuth::verify_with`].
58/// - Anything else, built by [`SecretHash::custom`] and verified by a host-supplied
59///   [`SecretVerifier`]. A host whose policy names argon2id, scrypt or bcrypt, or whose
60///   verification happens in an HSM, keeps that dependency in its own tree where it belongs. A
61///   custom scheme with NO verifier installed never authenticates: failing closed is the only
62///   safe reading of "the server cannot check this credential".
63#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SecretHash {
65    scheme: String,
66    encoded: String,
67}
68
69/// Hand-written for the same reason as [`ClientAuth`]'s: a stored verifier is not a credential a
70/// client can present, but it IS the input to an offline attack, so it must not turn up in a
71/// host's logs through `{:?}`. The SCHEME stays visible, because that is the field an operator
72/// needs when auditing which registrations still use a weak or retired one.
73impl fmt::Debug for SecretHash {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        f.debug_struct("SecretHash")
76            .field("scheme", &self.scheme)
77            .field("encoded", &"[redacted]")
78            .finish()
79    }
80}
81
82impl SecretHash {
83    /// The scheme identifier for the built-in hash: lower-case hex of the SHA-256 digest of the
84    /// secret's UTF-8 bytes. Named on the wire-visible model of a `$scheme$` prefix so a host can
85    /// migrate registrations one at a time and tell which is which.
86    pub const SHA256_HEX: &'static str = "sha256-hex";
87
88    /// Hash `secret` with the built-in scheme. The result is what the host stores; the secret
89    /// itself is handed to the client once and never persisted here.
90    pub fn sha256(secret: &str) -> Self {
91        SecretHash {
92            scheme: SecretHash::SHA256_HEX.to_string(),
93            encoded: hex_lower(&Sha256::digest(secret.as_bytes())),
94        }
95    }
96
97    /// A stored verifier in a scheme this crate does not implement, to be checked by the host's
98    /// [`SecretVerifier`]. `encoded` is opaque here: a PHC string, a KMS key handle, whatever the
99    /// host's verifier understands.
100    pub fn custom(scheme: impl Into<String>, encoded: impl Into<String>) -> Self {
101        SecretHash {
102            scheme: scheme.into(),
103            encoded: encoded.into(),
104        }
105    }
106
107    /// The scheme identifier.
108    pub fn scheme(&self) -> &str {
109        &self.scheme
110    }
111
112    /// The stored verifier text, in whatever encoding the scheme defines.
113    pub fn encoded(&self) -> &str {
114        &self.encoded
115    }
116
117    /// Verify against the BUILT-IN scheme only; any other scheme is `false` here and is the host
118    /// verifier's business. Constant time via [`constant_time_eq`].
119    fn verify_builtin(&self, presented: &str) -> bool {
120        if self.scheme != SecretHash::SHA256_HEX {
121            return false;
122        }
123        let computed = hex_lower(&Sha256::digest(presented.as_bytes()));
124        constant_time_eq(computed.as_bytes(), self.encoded.as_bytes())
125    }
126
127    /// Whether `presented` is the secret behind this stored verifier, consulting `verifier` for a
128    /// scheme this crate does not implement.
129    ///
130    /// The same ORDER OF PREFERENCE, and the same fail-closed rule, as
131    /// [`ClientAuth::verify_with`], which delegates here: the crate's own scheme is decided by the
132    /// crate, an unrecognised one is decided by the host, and an unrecognised one with no host
133    /// verifier installed never verifies.
134    ///
135    /// Public because a [`SecretHash`] is no longer only a client secret. RFC 7592 section 2 makes
136    /// the registration access token a bearer credential the server has to check on every
137    /// management request, and it is stored the same one-way way for the same reason (see
138    /// [`crate::registration`]), so it needs the same comparison rather than a second copy of it.
139    pub fn verify(&self, presented: &str, verifier: Option<&dyn SecretVerifier>) -> bool {
140        if self.scheme == SecretHash::SHA256_HEX {
141            self.verify_builtin(presented)
142        } else {
143            // Fails closed with no verifier: the server cannot check this credential, and "cannot
144            // check" must never read as "checked out".
145            match verifier {
146                Some(v) => v.verify(self, presented),
147                None => false,
148            }
149        }
150    }
151}
152
153/// The host's client-secret verifier, for [`SecretHash`] schemes this crate does not implement.
154///
155/// Installed on the server (`AuthorizationServer::with_secret_verifier`) and consulted by
156/// [`ClientAuth::verify_with`]. It is an ADDITION, never an override: a registration in the
157/// built-in scheme is always verified by this crate, so a permissive or buggy host verifier cannot
158/// weaken one.
159///
160/// Implementations MUST compare in constant time with respect to the presented secret: a
161/// comparison that returns early on the first differing byte leaks, through its own timing, how
162/// much of a guess was right, which turns an offline search into an online one an attacker can
163/// run a byte at a time. Every serious password-hashing crate already does this.
164///
165/// # MUST NOT PANIC
166///
167/// [`SecretVerifier::verify`] MUST answer `false` for every input it cannot make sense of: a
168/// stored encoding it does not recognise, a truncated hash, a parameter block with a length it did
169/// not expect, a presented secret that is empty or enormous. `false` is the fail-closed answer and
170/// it is always available; this trait has no error channel precisely because there is nothing a
171/// verifier could report that is not "this does not verify".
172///
173/// This crate catches no unwind anywhere on a request path, so a panic here is not turned into
174/// `invalid_client`. It unwinds out of `AuthorizationServer::authenticate_client` and out of the
175/// token request the host is driving. NAMING THE CONSEQUENCE: this seam is reachable by a caller
176/// with NO valid credential at all. [`SecretVerifier::dummy_hash`] is consulted on the
177/// UNKNOWN-CLIENT path, deliberately, so an unauthenticated request with an invented `client_id`
178/// runs this verifier over host-controlled bytes. A verifier that panics on a malformed encoding
179/// is therefore a remotely reachable panic on the token endpoint, and on a host that treats a
180/// panicking task as fatal it is a remotely reachable process abort.
181///
182/// # `verify` runs on the CALLER'S EXECUTOR THREAD, and it is not async
183///
184/// This method is synchronous and is called inline inside an `async fn`, so the KDF runs on
185/// whichever executor thread is polling the token request. Nothing here yields, and a host cannot
186/// interpose `spawn_blocking` from outside: there is no async variant of this seam in 0.9.
187///
188/// The cost is not hypothetical, and this trait's own docs price it: argon2id at ordinary
189/// parameters is roughly 200 ms (see [`SecretVerifier::dummy_hash`]). On a CURRENT-THREAD runtime
190/// that is 200 ms during which the reactor polls nothing else, per token request, and the
191/// unknown-client path pays it too. What a host should do about it:
192///
193/// - budget for it as request LATENCY, not as background work,
194/// - run the server on a multi-threaded runtime, so one stalled worker is not the whole reactor,
195/// - keep the [`crate::events::RateLimiter`] installed. It runs BEFORE this seam, so a host can
196///   bound how many of these an unauthenticated caller can start,
197/// - or hand off internally: a verifier may keep its own blocking pool and block on the result,
198///   which moves the KDF off the executor thread at the cost of a hop.
199///
200/// An ASYNC variant of this method would remove the need for all four, and it is not in 0.9: it
201/// would be a breaking change to a trait hosts already implement, so it belongs to 1.0.
202pub trait SecretVerifier: Send + Sync {
203    /// Whether `presented` is the secret behind `stored`.
204    fn verify(&self, stored: &SecretHash, presented: &str) -> bool;
205
206    /// A stored verifier in THIS verifier's scheme, over a secret nobody knows, used to make an
207    /// unknown `client_id` cost the same wall time as a known one.
208    ///
209    /// # What it is for
210    ///
211    /// RFC 6749 section 5.2 has an unknown client and a wrong secret collapse into one
212    /// `invalid_client`, and this crate keeps that collapse on the wire. TIMING breaks it anyway
213    /// when verification is expensive: the token endpoint cannot verify a secret for a
214    /// registration it did not find, so it answers immediately, while a real id pays the whole
215    /// KDF. With argon2id at ordinary parameters that is roughly 200 ms against 2 ms — a
216    /// single-request oracle over the entire client registry, which per-id throttling does not
217    /// touch because the attacker sends exactly one request per id.
218    ///
219    /// So the server performs a DUMMY verification on the unknown-id path, through this seam,
220    /// against whatever this method returns. It is this method rather than a constant in this
221    /// crate because only the host knows its own scheme: a hash in a scheme the verifier does not
222    /// recognise would be rejected on inspection, in microseconds, which is the leak again.
223    ///
224    /// # What to return
225    ///
226    /// A [`SecretHash`] in the same scheme and with the same cost parameters as the registrations
227    /// this verifier actually checks, over a secret that was drawn at random and thrown away (or,
228    /// equivalently, over a value no client will ever present). It may be a constant compiled into
229    /// the host: it authenticates nothing, because no registration names it, and it is only ever
230    /// compared against.
231    ///
232    /// # The default, and why it is `None` rather than something
233    ///
234    /// A verifier that supplies nothing here leaves the crate's own fallback in place, which
235    /// equalises the built-in `sha256-hex` scheme and cannot equalise a scheme it does not
236    /// implement. That is a real residual and it is stated on
237    /// `AuthorizationServer::authenticate_client` rather than papered over: this crate cannot
238    /// invent a well-formed argon2id encoding, and a required method here would break every
239    /// existing implementation of this trait to fix a leak most of them do not have.
240    fn dummy_hash(&self) -> Option<SecretHash> {
241        None
242    }
243}
244
245/// How the client authenticates to the token endpoint (RFC 6749 section 2.3).
246///
247/// `Debug` is hand-written rather than derived (see below) so that `ConfidentialSecret`'s secret
248/// never appears in a debug format. `Client` derives `Debug` and holds a `ClientAuth`, so this
249/// also keeps `{:?}` on a whole `Client` safe, without needing a hand-written `Debug` there too.
250#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
251/// `#[non_exhaustive]`: `client-assertion` and `mtls` each add a variant, independently, so this
252/// enum has four possible variant sets. A host matches this to render "how does this client
253/// authenticate" in an admin UI, or to decide what its own registration endpoint will accept, and
254/// neither of those should stop compiling because an unrelated crate in the graph wanted mutual
255/// TLS. Registering a client is unaffected: naming a variant is still just naming it.
256#[non_exhaustive]
257pub enum ClientAuth {
258    /// A public client (native app, browser app, device): no secret exists, so possession of the
259    /// `client_id` proves nothing and the flows compensate (PKCE, device-code user interaction).
260    Public,
261    /// A confidential client whose SECRET ITSELF is stored here.
262    ///
263    /// PREFER [`ClientAuth::ConfidentialSecretHash`]. This variant means the plaintext credential
264    /// lives wherever the host persists a [`Client`], so a leak of that store is a leak of every
265    /// client's working credential, and the host cannot honestly tell a customer that their secret
266    /// is not recoverable. It stays supported because it is legitimately right for two cases: a
267    /// host that resolves secrets from a vault or KMS at request time and never writes them down,
268    /// and a host migrating registrations gradually. This crate only ever compares it, in constant
269    /// time, and never logs it.
270    ConfidentialSecret {
271        /// The shared secret the client presents.
272        secret: String,
273    },
274    /// A confidential client stored as a one-way VERIFIER rather than as its secret. This is the
275    /// variant to reach for: see [`SecretHash`].
276    ConfidentialSecretHash {
277        /// The stored verifier.
278        hash: SecretHash,
279    },
280    /// A confidential client that authenticates with an RFC 7523 signed assertion rather than by
281    /// presenting a secret: `private_key_jwt` or `client_secret_jwt`.
282    ///
283    /// The keys are held INLINE rather than behind a `Box`, unlike `Client::registration`. The
284    /// question is what this costs a deployment that does not use it, and the answer is nothing:
285    /// the widest existing variant is `ConfidentialSecretHash` (two `String`s), and
286    /// [`crate::client_assertion::AssertionKeys`] is narrower than that, so `ClientAuth` does not
287    /// grow by a byte. Boxing would have ADDED an allocation at registration time to save a struct
288    /// size that was already paid for.
289    #[cfg(feature = "client-assertion")]
290    ConfidentialAssertion {
291        /// What the registration expects the assertion to be signed with. This, and never the
292        /// token's own header, is what decides the algorithm: see
293        /// [`crate::client_assertion::verify_assertion`].
294        keys: crate::client_assertion::AssertionKeys,
295    },
296    /// RFC 8705: a confidential client that authenticates with a mutual-TLS CERTIFICATE and
297    /// holds no shared secret at all. This is the variant a deployment whose policy forbids
298    /// shared secrets registers, and the only one where the credential never travels: the
299    /// client proves possession of a private key to the host's TLS layer, and this crate is
300    /// handed the resulting certificate as an established fact.
301    ///
302    /// Carried INLINE rather than boxed, on the same measurement as the assertion variant
303    /// above: the widest shape [`crate::mtls::MtlsClientRegistration`] can take is one
304    /// `String` plus a discriminant, against `ConfidentialSecretHash`'s two `String`s, so
305    /// this variant does not make `ClientAuth`, or the [`Client`] every host store holds one
306    /// of per registration, any bigger than it already was.
307    #[cfg(feature = "mtls")]
308    Mtls {
309        /// Which RFC 8705 method, and what it expects to see.
310        registration: crate::mtls::MtlsClientRegistration,
311    },
312}
313
314/// Hand-written so `ConfidentialSecret { secret }` never prints the secret. An AS library that
315/// logs nothing itself should still not make `tracing::debug!(?client)` on a host's part into a
316/// plaintext credential leak; deriving `Debug` here would do exactly that. Every non-secret
317/// variant and field stays visible so the type is still useful to debug-print.
318impl fmt::Debug for ClientAuth {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        match self {
321            ClientAuth::Public => f.write_str("Public"),
322            ClientAuth::ConfidentialSecret { secret: _ } => f
323                .debug_struct("ConfidentialSecret")
324                .field("secret", &"[redacted]")
325                .finish(),
326            // `SecretHash`'s own Debug is already redacted; going through it keeps the scheme
327            // visible, which is the part an operator can act on.
328            ClientAuth::ConfidentialSecretHash { hash } => f
329                .debug_struct("ConfidentialSecretHash")
330                .field("hash", hash)
331                .finish(),
332            // `AssertionKeys`'s own Debug redacts a `client_secret_jwt` secret and prints the
333            // PUBLIC keys of a `private_key_jwt` registration, which are public.
334            #[cfg(feature = "client-assertion")]
335            ClientAuth::ConfidentialAssertion { keys } => f
336                .debug_struct("ConfidentialAssertion")
337                .field("keys", keys)
338                .finish(),
339            // NOT redacted: a registration that names an expected subject DN or a certificate
340            // thumbprint holds no secret. Both are public facts about a public document, and
341            // an operator debugging a refused mutual-TLS client needs to see exactly which
342            // value the server expected.
343            #[cfg(feature = "mtls")]
344            ClientAuth::Mtls { registration } => f
345                .debug_struct("Mtls")
346                .field("registration", registration)
347                .finish(),
348        }
349    }
350}
351
352impl ClientAuth {
353    /// Whether this registration is CONFIDENTIAL, meaning the client can prove possession of
354    /// something. RFC 6749 section 4.4 (client credentials), RFC 7662 section 2.1 (introspection)
355    /// and RFC 7009 section 2.1 (revocation) all require that, and the answer must not be "is this
356    /// variant `ConfidentialSecret`", because a new storage form for the same credential would
357    /// then silently read as public.
358    pub fn is_confidential(&self) -> bool {
359        !matches!(self, ClientAuth::Public)
360    }
361
362    /// Verify a presented secret with no host verifier installed. See [`ClientAuth::verify_with`],
363    /// which this delegates to; a [`ClientAuth::ConfidentialSecretHash`] in a scheme this crate
364    /// does not implement therefore never authenticates through this entry point.
365    pub fn verify(&self, presented: Option<&str>) -> bool {
366        self.verify_with(presented, None)
367    }
368
369    /// Verify a presented secret, consulting the host's [`SecretVerifier`] for hash schemes this
370    /// crate does not implement.
371    ///
372    /// Public clients accept `None` and reject any presented secret (presenting a secret for a
373    /// secretless registration is a client mixup worth failing loud on). Confidential clients
374    /// require the exact secret; the comparison is constant time regardless of the length of
375    /// either the registered or the presented secret: an early-exit comparison would report,
376    /// through its own timing, how many leading bytes of a guess were right.
377    ///
378    /// ORDER OF PREFERENCE for a hashed registration: the crate's own scheme is checked by the
379    /// crate, and only an unrecognised scheme is passed to `verifier`. That way installing a
380    /// verifier can only ADD registrations that authenticate, never change the answer for one the
381    /// crate could already decide.
382    ///
383    /// What this does NOT cover, and who does: if the caller returned early for an unknown
384    /// `client_id` without calling this at all, an unknown client and a known client with a wrong
385    /// secret would be distinguishable by timing even though this function leaks nothing. That is
386    /// the caller's responsibility rather than this function's, and
387    /// `AuthorizationServer::authenticate_client` discharges it by running a DUMMY verification
388    /// through this same function on the unknown-id path. See [`SecretVerifier::dummy_hash`] for
389    /// the part of it only the host can supply.
390    pub fn verify_with(
391        &self,
392        presented: Option<&str>,
393        verifier: Option<&dyn SecretVerifier>,
394    ) -> bool {
395        match self {
396            ClientAuth::Public => presented.is_none(),
397            ClientAuth::ConfidentialSecret { secret } => match presented {
398                Some(p) => constant_time_eq(secret.as_bytes(), p.as_bytes()),
399                None => false,
400            },
401            ClientAuth::ConfidentialSecretHash { hash } => match presented {
402                Some(p) => hash.verify(p, verifier),
403                None => false,
404            },
405            // NEVER, and not because it is unimplemented. This registration's credential is a
406            // SIGNATURE over a claim set (RFC 7523 section 3), and there is no presented string
407            // that is the right answer here. Returning `true` for any input would let a client
408            // registered for `private_key_jwt` be authenticated by the `client_secret_post` path
409            // instead, which is the downgrade the registration exists to forbid; the assertion path
410            // is `AuthorizationServer::authenticate_client` and it does not come through here.
411            #[cfg(feature = "client-assertion")]
412            ClientAuth::ConfidentialAssertion { .. } => false,
413            // NEVER, and for the same reason as the assertion arm above. A mutual-TLS
414            // registration has no secret to compare against, so there is no presented string
415            // that could be the right one, and `None` is not the right answer either: unlike
416            // `Public`, this client is confidential and something must be proven. The
417            // certificate is checked by `crate::mtls::verify_certificate`, which is reached
418            // only from `AuthorizationServer::authenticate_client`; every OTHER caller of
419            // this function, now or later, therefore fails closed on a mutual-TLS client
420            // rather than accidentally authenticating one with no evidence at all.
421            #[cfg(feature = "mtls")]
422            ClientAuth::Mtls { .. } => false,
423        }
424    }
425}
426
427/// Constant-time equality, by comparing SHA-256 digests over a fixed 32 bytes rather than the raw
428/// inputs.
429///
430/// Hashing first is what makes this constant time, on two axes that a raw byte-by-byte compare
431/// cannot deliver at once:
432///
433/// 1. Value: the accumulator below visits all 32 digest bytes regardless of where (or whether) the
434///    inputs first differ, so no early difference shows up as an early exit.
435/// 2. Length: SHA-256 always produces exactly 32 bytes no matter how long `a` or `b` are, so the
436///    loop always runs exactly 32 iterations. Comparing the raw inputs directly, even with a
437///    "run for max(a.len(), b.len())" loop, makes wall time grow with the presented secret's
438///    length once it exceeds the registered one, which lets a network attacker binary-search the
439///    registered secret's length by timing the token endpoint. Hashing first removes the input
440///    length from the loop bound entirely.
441///
442/// This also happens to make the function actually correct: two digests are equal only when the
443/// two inputs were equal (SHA-256 collision resistance), so there is no longer a length-encoding
444/// edge case where sufficiently padded unequal inputs compare equal.
445///
446/// This is NOT password hashing, and SHA-256 is not being used as a KDF here. `secret` is a
447/// high-entropy, host-generated and host-managed credential, not a human-chosen password, so
448/// there is no offline-guessing threat this needs to be slow against. SHA-256's only job in this
449/// function is to be a fixed-width length equaliser ahead of a constant-time compare; nobody
450/// should read this as a template for verifying user passwords.
451fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
452    let da = Sha256::digest(a);
453    let db = Sha256::digest(b);
454    let mut acc: u8 = 0;
455    for i in 0..32 {
456        acc |= da[i] ^ db[i];
457    }
458    acc == 0
459}
460
461/// A registered client: identity, authentication, and what it is allowed to do.
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct Client {
464    /// The unique client identifier.
465    pub client_id: ClientId,
466    /// How the client authenticates.
467    pub auth: ClientAuth,
468    /// The grant types this registration may use; anything else is `unauthorized_client`.
469    pub grant_types: Vec<GrantType>,
470    /// Registered redirect URIs (authorization-code grant; exact-match per OAuth 2.1).
471    pub redirect_uris: Vec<String>,
472    /// The scopes this client may ever be granted; a request outside this set is `invalid_scope`.
473    pub allowed_scopes: ScopeSet,
474    /// The scopes granted when a request names none (RFC 6749 section 3.3 server default).
475    pub default_scopes: ScopeSet,
476    /// Human-readable name for consent and admin surfaces.
477    pub name: Option<String>,
478    /// Present exactly when this registration was created by RFC 7591 dynamic client
479    /// registration, and absent for one the host provisioned itself.
480    ///
481    /// BOXED, and this is not a style choice, though the reason is no longer the one it was
482    /// written for. The original argument was that a `Client` is deep cloned out of the store on
483    /// every token-plane request, so every byte here is paid per request;
484    /// [`crate::store::Storage::get_client`] hands back an `Arc<Client>` now, so a read is a
485    /// pointer clone and the struct's SIZE is not on that path at all.
486    ///
487    /// Re-examined on that basis, the box is MORE clearly right than when it was chosen, because
488    /// the optimisation removed its only cost. What it buys is now a memory argument rather than a
489    /// per-request one. MEASURED: `Option<Box<DynamicRegistration>>` is 8 bytes against 104 for
490    /// the record inline, which is the difference between a `Client` of 200 bytes and one of 296,
491    /// paid by every registration in every store whether or not RFC 7591 is enabled. What it USED
492    /// to cost was an extra allocation on every clone of a client that does have one; with `Arc`
493    /// there are no such clones, so that allocation is now paid exactly once, when the
494    /// registration is created.
495    ///
496    /// It lives on the client rather than in a table of its own because it IS the client: RFC
497    /// 7592 section 2 manages a registration through the same identifier the token endpoint
498    /// authenticates, and splitting the two across two stores would make deletion a
499    /// two-phase problem the host has to get right.
500    pub registration: Option<Box<DynamicRegistration>>,
501}
502
503/// What a dynamically registered client carries beyond an ordinary registration: the RFC 7592
504/// section 2 management credential, and the RFC 7591 section 3.2.1 members that are not
505/// recoverable from the rest of the [`Client`].
506///
507/// The registration access token is held as a one-way [`SecretHash`], never as itself. It is a
508/// bearer credential that reads, rewrites and DELETES a registration, so it is at least as
509/// sensitive as the client secret next to it, and it is stored the same way for the same reason:
510/// a dump of the client table must not be a set of working credentials.
511#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
512pub struct DynamicRegistration {
513    /// The stored verifier for the RFC 7592 section 2 registration access token.
514    pub registration_access_token_hash: SecretHash,
515    /// RFC 7591 section 3.2.1 `client_id_issued_at`: seconds since the Unix epoch.
516    pub client_id_issued_at: Option<u64>,
517    /// RFC 7591 section 3.2.1 `client_secret_expires_at`: seconds since the Unix epoch, or `0`
518    /// for a secret that never expires. `None` when no secret was issued.
519    pub client_secret_expires_at: Option<u64>,
520    /// RFC 7591 section 2 `token_endpoint_auth_method`, as registered. Kept verbatim because
521    /// [`ClientAuth`] deliberately does not distinguish `client_secret_basic` from
522    /// `client_secret_post` (RFC 6749 section 2.3.1 lets a confidential client use either), so the
523    /// value the client registered cannot be recovered from it.
524    pub token_endpoint_auth_method: String,
525}
526
527impl Client {
528    /// Whether the registration permits `grant_type`.
529    pub fn allows_grant(&self, grant_type: GrantType) -> bool {
530        self.grant_types.contains(&grant_type)
531    }
532}
533
534#[cfg(test)]
535#[path = "tests/client.rs"]
536mod tests;