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;