Skip to main content

oauth_as/
client_assertion.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 7523 JWT client authentication: `private_key_jwt` and `client_secret_jwt`.
5//!
6//! # What this buys
7//!
8//! A deployment whose security policy forbids transmitting a shared secret cannot use
9//! `client_secret_basic` or `client_secret_post` at all, because both put the secret on the wire on
10//! every single request. RFC 7523 lets a client prove possession of a key instead, and with
11//! `private_key_jwt` this server never holds anything that could authenticate AS the client, only
12//! the public half. It is also required by FAPI 2.0 and expected by most enterprise deployments.
13//!
14//! # What the host owns
15//!
16//! The registration, and with it the algorithm. A client authenticates this way exactly when its
17//! [`crate::client::ClientAuth`] is [`crate::client::ClientAuth::ConfidentialAssertion`], and the
18//! [`AssertionKeys`] inside decide BOTH which `alg` is accepted and which key verifies. Nothing
19//! about either comes off the wire. See [`verify_assertion`] for why that is the whole defence
20//! against JWS algorithm confusion rather than one check among many.
21//!
22//! # Single use is the point
23//!
24//! RFC 7523 section 3 requires the `jti` to be single use within the assertion's own validity
25//! window, and an implementation that verifies the signature and skips that has built a credential
26//! that anyone who observed one request can send again. Verification here is PURE: it returns the
27//! `jti` and the deadline, and CLAIMING it is [`crate::store::Storage::claim_replay_id`], which
28//! `AuthorizationServer::authenticate_client` calls on the same request. Neither half is worth
29//! anything without the other, which is why [`VerifiedAssertion`] carries the deadline rather than
30//! leaving a caller to invent one.
31
32use std::fmt;
33use std::time::{Duration, SystemTime, UNIX_EPOCH};
34
35use serde::{Deserialize, Serialize};
36
37use crate::jwt::{verify_hs256, CompactJws, Es256Verifier, PublicJwk};
38
39/// RFC 7521 section 4.2: the `client_assertion_type` a JWT bearer assertion must carry.
40pub const CLIENT_ASSERTION_TYPE: &str = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
41
42/// The RFC 8414 `token_endpoint_auth_methods_supported` value for a MAC-signed assertion.
43pub const CLIENT_SECRET_JWT: &str = "client_secret_jwt";
44
45/// The RFC 8414 `token_endpoint_auth_methods_supported` value for a public-key-signed assertion.
46pub const PRIVATE_KEY_JWT: &str = "private_key_jwt";
47
48/// The shortest `client_secret_jwt` key this crate will register: 22 characters.
49///
50/// NOT tuning, and the same reasoning as [`crate::server::MIN_USER_CODE_LENGTH`]: a parameter this
51/// weak is not a slower version of the feature, it is the feature not working. RFC 6749 section
52/// 10.10 requires a credential of this kind to carry at least 128 bits of entropy, and base64url,
53/// which is what a generated secret is nearly always spelled in, carries 6 bits per character, so
54/// 128 bits is `ceil(128 / 6)` = 22 characters.
55///
56/// The reason it matters MORE here than for `client_secret_basic` is that a `client_secret_jwt`
57/// assertion is an HMAC over public inputs: an attacker who observes ONE assertion (from a log, a
58/// proxy, a captured request) can grind candidate keys against it offline, at whatever rate their
59/// hardware allows, without ever touching this server again. There is no rate limit that reaches
60/// that, so the key length is the entire defence.
61///
62/// This is a LENGTH check and length only bounds entropy from above: 22 copies of the letter `a`
63/// clears it and carries none. A library cannot measure the entropy of a string it did not
64/// generate, and refusing the obviously-too-short case is the part that can be checked. Clamping,
65/// the answer [`crate::server::ServerConfig::user_code_length`] gives, is not available here: this
66/// crate cannot lengthen a secret the client already holds.
67pub const MIN_CLIENT_SECRET_JWT_KEY_LENGTH: usize = 22;
68
69/// A registered `client_secret_jwt` HMAC key that has cleared
70/// [`MIN_CLIENT_SECRET_JWT_KEY_LENGTH`].
71///
72/// A newtype with a private field rather than a bare `String` on the variant, because a private
73/// field is the only spelling Rust has for "this cannot be reached by a struct literal", and a
74/// floor a caller can skip by writing `AssertionKeys::ClientSecret { secret: "abc".into() }` is not
75/// a floor. Deserialization is routed through the same check for the same reason
76/// [`crate::jwt::PublicJwk`]'s is: a registration read back out of the host's store must be held to
77/// what the constructor holds a fresh one to, or the store becomes the way around it.
78#[derive(Clone, PartialEq, Eq, Serialize)]
79#[serde(transparent)]
80pub struct ClientSecretKey(String);
81
82impl ClientSecretKey {
83    /// Register a secret, refusing one below the floor.
84    pub fn new(secret: impl Into<String>) -> Result<Self, WeakClientSecret> {
85        let secret = secret.into();
86        // Characters, not bytes: the floor is an argument about how many symbols of a generated
87        // alphabet the secret carries, and a multi-byte character contributes one of those.
88        if secret.chars().count() < MIN_CLIENT_SECRET_JWT_KEY_LENGTH {
89            return Err(WeakClientSecret);
90        }
91        Ok(ClientSecretKey(secret))
92    }
93
94    /// The key material, for the HMAC.
95    pub fn as_bytes(&self) -> &[u8] {
96        self.0.as_bytes()
97    }
98}
99
100/// Hand-written, for the reason [`AssertionKeys`]'s own is: this type exists to sit inside a
101/// registration that gets logged, and a derived one would print the key.
102impl fmt::Debug for ClientSecretKey {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str("\"[redacted]\"")
105    }
106}
107
108impl<'de> Deserialize<'de> for ClientSecretKey {
109    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
110        ClientSecretKey::new(String::deserialize(d)?).map_err(serde::de::Error::custom)
111    }
112}
113
114/// A `client_secret_jwt` key that does not clear [`MIN_CLIENT_SECRET_JWT_KEY_LENGTH`].
115///
116/// Carries NO payload: the whole of it is "too short", and a rejection that echoed the offending
117/// secret would write the credential into whatever log caught the error.
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub struct WeakClientSecret;
120
121impl fmt::Display for WeakClientSecret {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(
124            f,
125            "a client_secret_jwt key must be at least {MIN_CLIENT_SECRET_JWT_KEY_LENGTH} \
126             characters (RFC 6749 s10.10, 128 bits at 6 bits per base64url character)"
127        )
128    }
129}
130
131impl std::error::Error for WeakClientSecret {}
132
133/// The `token_endpoint_auth_signing_alg_values_supported` this server advertises (RFC 8414
134/// section 2), which is exactly what [`AssertionKeys::signing_alg`] can return.
135pub const ASSERTION_SIGNING_ALGS: &[&str] = &["HS256", "ES256"];
136
137/// The longest `exp - now` this server will accept on an assertion.
138///
139/// Ten minutes. This is not a guess at what clients do, it is a bound on what an attacker can make
140/// this server REMEMBER: the replay defence has to hold a `jti` until the assertion's own `exp`, so
141/// an unbounded `exp` is an unbounded storage commitment chosen by the party presenting the
142/// credential. Ten minutes is comfortably longer than any legitimate client's assertion lifetime
143/// and short enough that the retained set is bounded by request rate rather than by a number an
144/// attacker wrote in a claim.
145pub const MAX_ASSERTION_LIFETIME: Duration = Duration::from_secs(600);
146
147/// How far a client's clock may be AHEAD of this server's before `iat` and `nbf` are refused.
148///
149/// Granted in that direction only. Leeway that lets an assertion be accepted slightly early can
150/// only ever refuse a request that was going to be fine anyway; leeway on `exp` would keep a dead
151/// credential alive, so [`verify_assertion`] does not grant any there.
152///
153/// The SAME constant [`crate::dpop`] publishes for RFC 9449 proofs, re-exported rather than
154/// duplicated: see `src/skew.rs` for why one definition rather than two equal ones.
155pub use crate::skew::CLOCK_SKEW_LEEWAY;
156
157/// The largest client assertion [`verify_assertion`] and [`unverified_subject`] will look at, in
158/// bytes, checked BEFORE either parses anything.
159///
160/// THE SAME ARGUMENT [`crate::dpop::MAX_PROOF_BYTES`] MAKES, word for word, and it applies here for
161/// the same reasons. Both functions are PUBLIC, so a host may hand either one a string from
162/// anywhere; both hand it straight to [`crate::jwt::CompactJws::parse`], which base64-decodes it and
163/// runs two JSON parses over the result; and both run before anything about the caller has been
164/// established — [`unverified_subject`] runs before the registration has even been LOOKED UP, which
165/// is the whole of its purpose. This crate's `MAX_BODY_BYTES` is in the optional `http` module,
166/// which `client-assertion` does not depend on, so a host that assembles its own request parsing
167/// (the arrangement this library is built for) has never had a bound on this string.
168///
169/// WHY 4 KiB, from what an assertion actually contains. RFC 7523 section 3 fixes the claim set:
170/// `iss`, `sub`, `aud`, `exp`, `nbf`, `iat`, `jti`, over a header carrying `alg` and at most `typ`
171/// and `kid`, with either a 32-byte HMAC or a 64-byte ECDSA signature. Base64url encoded that is a
172/// few hundred bytes in practice, and 4096 leaves generous room for a long issuer URL, a verbose
173/// `kid` and claims a deployment adds, while refusing a megabyte of `client_assertion` parameter
174/// before any of it is decoded. An assertion this cap refuses is not one any conforming client
175/// sends.
176pub const MAX_ASSERTION_BYTES: usize = 4096;
177
178/// What a registration expects a client assertion to be signed with.
179///
180/// The variants are the two RFC 7523 methods, and the choice between them is the choice of
181/// algorithm: there is deliberately no way to spell "this client uses `private_key_jwt` and also
182/// HS256". See [`verify_assertion`].
183#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub enum AssertionKeys {
185    /// `client_secret_jwt` (RFC 7523 section 2.2 with a MAC): HMAC-SHA-256 under the registered
186    /// client secret.
187    ///
188    /// Weaker than [`AssertionKeys::PublicKeys`] and supported because deployments have it: the
189    /// server still holds a secret that could authenticate as the client, so a dump of the client
190    /// table is still a set of working credentials. What it does buy over `client_secret_basic` is
191    /// that the secret never crosses the network, so it cannot be captured in transit or logged by
192    /// an intermediary.
193    ClientSecret {
194        /// The shared secret. Held in the clear because HMAC verification needs the key itself; a
195        /// one-way [`crate::client::SecretHash`] cannot be used here, and pretending otherwise
196        /// would be the kind of storage that looks safe and is not.
197        ///
198        /// A [`ClientSecretKey`] rather than a `String` so that the entropy floor cannot be walked
199        /// past by writing the variant out by hand.
200        secret: ClientSecretKey,
201    },
202    /// `private_key_jwt` (RFC 7523 section 2.2 with a digital signature): ECDSA P-256 under a key
203    /// only the client holds. This is the variant to reach for.
204    PublicKeys {
205        /// The registered public keys. Several are allowed so a client can rotate: it publishes the
206        /// new key alongside the old, signs with either during the overlap, and retires the old one
207        /// when it is done. A server that accepted only one key would make rotation an outage.
208        keys: Vec<PublicJwk>,
209    },
210}
211
212/// Hand-written for the same reason as [`crate::client::ClientAuth`]'s: `ClientAuth` derives
213/// nothing that would print a secret, and this type sits inside it. The PUBLIC keys stay visible,
214/// because they are public and because "which keys does this registration actually hold" is the
215/// first question anyone debugging a `private_key_jwt` failure asks.
216impl fmt::Debug for AssertionKeys {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match self {
219            AssertionKeys::ClientSecret { .. } => f
220                .debug_struct("ClientSecret")
221                .field("secret", &"[redacted]")
222                .finish(),
223            AssertionKeys::PublicKeys { keys } => {
224                f.debug_struct("PublicKeys").field("keys", keys).finish()
225            }
226        }
227    }
228}
229
230impl AssertionKeys {
231    /// The RFC 8414 method name this registration authenticates with.
232    pub fn token_endpoint_auth_method(&self) -> &'static str {
233        match self {
234            AssertionKeys::ClientSecret { .. } => CLIENT_SECRET_JWT,
235            AssertionKeys::PublicKeys { .. } => PRIVATE_KEY_JWT,
236        }
237    }
238
239    /// The ONE `alg` this registration's assertions may carry.
240    ///
241    /// Singular on purpose. A registration that accepted a SET of algorithms would be one where an
242    /// attacker gets to pick from that set, and the interesting attacks are all about picking the
243    /// element the deployment did not think about.
244    pub fn signing_alg(&self) -> &'static str {
245        match self {
246            AssertionKeys::ClientSecret { .. } => "HS256",
247            AssertionKeys::PublicKeys { .. } => "ES256",
248        }
249    }
250}
251
252/// Why an assertion was refused.
253///
254/// Every one of these becomes the same `invalid_client` on the wire (RFC 6749 section 5.2), for the
255/// same reason `authenticate_client` collapses "unknown client" and "wrong secret": telling a
256/// caller WHICH check it failed is telling an attacker how to get closer. The distinction exists
257/// for the host's audit channel, where the reader is not the attacker.
258// `Hash` because `crate::events::ClientAuthFailure` derives it and now carries one of these.
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
260#[non_exhaustive]
261pub enum AssertionFailure {
262    /// Not a compact JWS, or longer than [`MAX_ASSERTION_BYTES`] and so refused on size before
263    /// anything is decoded, or its `typ` says it is some other kind of JWT.
264    Malformed,
265    /// The header's `alg` is not the one this registration signs with.
266    AlgorithmMismatch,
267    /// The signature did not verify under any registered key.
268    BadSignature,
269    /// `iss` or `sub` is absent, or is not the client this request claims to be.
270    WrongPrincipal,
271    /// `aud` names neither this server's token endpoint nor its issuer.
272    WrongAudience,
273    /// `exp` is absent, has passed, or is further out than this server will track a `jti` for.
274    Expired,
275    /// `nbf` or `iat` is in the future by more than [`CLOCK_SKEW_LEEWAY`].
276    NotYetValid,
277    /// `jti` is absent or empty, so single use cannot be enforced.
278    MissingJti,
279    /// This `jti` has been seen before within the assertion's own validity window.
280    Replayed,
281    /// The single-use claim could not be RECORDED, so this assertion was refused without ever
282    /// being judged: the storage seam behind [`crate::store::Storage::claim_replay_id`] failed.
283    ///
284    /// The wire answer is the same `invalid_client` every other variant collapses to, and the
285    /// refusal is deliberate — a claim that could not be recorded is a claim that did not happen,
286    /// and treating a storage outage as "probably fine" would make every assertion replayable for
287    /// the duration of it. What this variant exists for is the AUDIT channel: through 0.9.0 the
288    /// outage was reported as [`AssertionFailure::Replayed`], which
289    /// [`crate::events::ClientAuthFailure::AssertionInvalid`] documents as "somebody who has
290    /// captured a client's traffic, which is a different incident and a much worse one". An outage
291    /// fails every `private_key_jwt` client at once, so that mislabel turned a store alarm into a
292    /// burst of this crate's worst-incident signal.
293    ReplayCheckUnavailable,
294}
295
296impl fmt::Display for AssertionFailure {
297    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298        f.write_str(match self {
299            AssertionFailure::Malformed => "the client assertion is not a well formed JWT",
300            AssertionFailure::AlgorithmMismatch => {
301                "the client assertion alg is not the one this registration signs with"
302            }
303            AssertionFailure::BadSignature => "the client assertion signature did not verify",
304            AssertionFailure::WrongPrincipal => "the client assertion iss/sub is not this client",
305            AssertionFailure::WrongAudience => "the client assertion aud is not this server",
306            AssertionFailure::Expired => "the client assertion is expired or too long lived",
307            AssertionFailure::NotYetValid => "the client assertion is not yet valid",
308            AssertionFailure::MissingJti => "the client assertion carries no jti",
309            AssertionFailure::Replayed => "the client assertion jti has already been used",
310            AssertionFailure::ReplayCheckUnavailable => {
311                "the client assertion jti could not be recorded as spent"
312            }
313        })
314    }
315}
316
317impl std::error::Error for AssertionFailure {}
318
319/// What a verified assertion leaves the caller holding.
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct VerifiedAssertion {
322    /// The `jti` the caller MUST claim as single use before treating the client as authenticated.
323    pub jti: String,
324    /// The assertion's own `exp`: how long the `jti` has to be remembered, and no longer.
325    pub expires_at: SystemTime,
326}
327
328/// The `typ` header values this server will read as a client assertion.
329///
330/// RFC 7523 does not fix `typ`, so an absent one is legal and common. What this list is really for
331/// is REFUSING the values that mean something else: RFC 9449 section 4.2 fixes a DPoP proof at
332/// `dpop+jwt` and RFC 9068 section 2.1 fixes an access token at `at+jwt`, and both are JWTs the
333/// same party signs with the same key and sends to this same endpoint. With no `typ` check they are
334/// interchangeable with an authentication credential, so a proof captured from one request could be
335/// presented as the client's password on the next. `client-authentication+jwt` is the value
336/// draft-ietf-oauth-rfc7523bis introduces for exactly this reason, and is accepted so that a client
337/// which already sets it is not punished for being early.
338const ACCEPTED_TYP: &[&str] = &["JWT", "jwt", "client-authentication+jwt"];
339
340/// Verify one RFC 7523 section 3 client assertion.
341///
342/// `audiences` is what section 3 (3) will accept as naming this server: the caller passes the token
343/// endpoint URL and the issuer identifier. `now` is the server's clock.
344///
345/// The ORDER of the checks below is the security property of this function:
346///
347/// 1. `alg` comes from `keys`, which is the REGISTRATION, and never from the token header. This is
348///    the whole of the defence against JWS algorithm confusion, and it is structural rather than a
349///    check that could be forgotten: [`AssertionKeys`] holds either a secret or public keys, so
350///    there is no value an attacker can put in the header that routes an HMAC verification at a
351///    public key it already knows.
352/// 2. The SIGNATURE is verified before any claim is read for anything but its own sake. Acting on
353///    an unauthenticated claim, even only to produce a better error message, is how a verifier ends
354///    up telling an attacker which client ids exist.
355/// 3. Everything after that is the section 3 claim set, in the section's own order.
356///
357/// `verifier` is the ES256 backend, the host's to choose after 0.9.0: enable `jwt-p256` for
358/// [`crate::jwt::P256Verifier`], or pass your own.
359///
360/// It is an `Option`, and unlike [`crate::dpop::verify_proof`]'s it HAS to be, because only ONE of
361/// the two RFC 7523 methods involves a curve at all. `private_key_jwt` is ES256 and `None` refuses
362/// it, for that function's reason: a caller holding no verifier must refuse the credential rather
363/// than verify it leniently. `client_secret_jwt` is HS256 over the registered secret (RFC 7518
364/// section 3.2), and there is no elliptic curve on that path, no key for a verifier to check and
365/// nothing for a backend to contribute. Taking a verifier by value here made a build with
366/// `client-assertion` and no ES256 backend refuse a perfectly valid HMAC, which is a refusal no
367/// RFC asks for.
368///
369/// PUBLIC because [`VerifiedAssertion`] and [`AssertionFailure`] are, and a type no consumer can
370/// obtain is a type that should not have been exported. It is also the other half of
371/// [`unverified_subject`], which has always been public: exposing the "believe nothing" lookup
372/// while hiding the verification it exists to feed left the safe path out of reach.
373pub fn verify_assertion(
374    verifier: Option<&dyn Es256Verifier>,
375    keys: &AssertionKeys,
376    assertion: &str,
377    client_id: &str,
378    audiences: &[&str],
379    now: SystemTime,
380) -> Result<VerifiedAssertion, AssertionFailure> {
381    // (0) SIZE, before the parse and therefore before any base64 decoding or JSON parsing happens.
382    // See [`MAX_ASSERTION_BYTES`]: nothing about this caller has been established yet, so whatever
383    // work this function does on a hostile string, it does for whoever sent it.
384    if assertion.len() > MAX_ASSERTION_BYTES {
385        return Err(AssertionFailure::Malformed);
386    }
387
388    let jws = CompactJws::parse(assertion).map_err(|_| AssertionFailure::Malformed)?;
389
390    if let Some(typ) = jws.header_str("typ") {
391        if !ACCEPTED_TYP.contains(&typ) {
392            return Err(AssertionFailure::Malformed);
393        }
394    }
395
396    // (1) The registration decides the algorithm. An ABSENT `alg` fails here too rather than
397    // defaulting to anything: RFC 7515 section 4.1.1 makes it REQUIRED, and a missing one is not
398    // something this server has to guess about.
399    if jws.header_str("alg") != Some(keys.signing_alg()) {
400        return Err(AssertionFailure::AlgorithmMismatch);
401    }
402
403    // (1b) RFC 7515 s4.1.11 `crit`, on the same parser every other JWS verifier here uses. See
404    // `CompactJws::reject_unknown_crit` for why this is not per-call-site.
405    if jws.reject_unknown_crit().is_err() {
406        return Err(AssertionFailure::Malformed);
407    }
408
409    // (2) The signature, over the bytes that arrived.
410    let signed = match keys {
411        // The entropy floor is enforced at registration and again on the way out of the host's
412        // store, so nothing here has to re-check it; see `ClientSecretKey`.
413        AssertionKeys::ClientSecret { secret } => verify_hs256(
414            secret.as_bytes(),
415            jws.signing_input.as_bytes(),
416            &jws.signature,
417        ),
418        // Any ONE registered key is enough: see `AssertionKeys::PublicKeys` on rotation.
419        //
420        // No verifier means NO key matches, which lands on the same `BadSignature` an ES256
421        // assertion under a foreign key gets. Deliberately the same answer: the caller maps every
422        // failure to one bare `invalid_client` anyway (see `authenticate_by_assertion`), and a
423        // distinct "this deployment has no backend" outcome would be a fact about the server's
424        // configuration that an unauthenticated caller could read off the wire.
425        AssertionKeys::PublicKeys { keys } => match verifier {
426            Some(verifier) => keys
427                .iter()
428                .any(|key| verifier.verify(key, jws.signing_input.as_bytes(), &jws.signature)),
429            None => false,
430        },
431    };
432    if !signed {
433        return Err(AssertionFailure::BadSignature);
434    }
435
436    // RFC 7523 section 3 (1) and (2): for CLIENT AUTHENTICATION the issuer and the subject are both
437    // the client itself. Checked against the `client_id` this request presented, so an assertion
438    // minted for one registration cannot authenticate another even where two registrations have
439    // been given the same secret, which is a deployment mistake but a common one.
440    if jws.claim_str("iss") != Some(client_id) || jws.claim_str("sub") != Some(client_id) {
441        return Err(AssertionFailure::WrongPrincipal);
442    }
443
444    // RFC 7523 section 3 (3): the assertion must name THIS server. Without it, every other
445    // authorization server the client also authenticates to can take the assertion it was handed
446    // and present it here as that client. This is the only check standing between a multi-AS client
447    // and a credential its other servers can spend.
448    if !audience_matches(&jws, audiences) {
449        return Err(AssertionFailure::WrongAudience);
450    }
451
452    // RFC 7523 section 3 (4): `exp` is REQUIRED and must not have passed. NO skew leeway here: see
453    // `CLOCK_SKEW_LEEWAY`.
454    //
455    // The addition is CHECKED. `exp` is a `u64` out of JSON that nobody has authenticated yet (this
456    // assertion IS the authentication), and `UNIX_EPOCH + Duration::from_secs(u64::MAX)` PANICS
457    // rather than wrapping. In a library that panic unwinds into the host's request handler, and it
458    // happened BEFORE either bound below was compared. `Expired` is the honest answer for a value
459    // that cannot be represented: it is far past the `MAX_ASSERTION_LIFETIME` ceiling the next check
460    // imposes, so the refusal is the same one an `exp` of merely a year hence already gets.
461    let exp = jws.claim_time("exp").ok_or(AssertionFailure::Expired)?;
462    let expires_at = UNIX_EPOCH
463        .checked_add(Duration::from_secs(exp))
464        .ok_or(AssertionFailure::Expired)?;
465    if now >= expires_at {
466        return Err(AssertionFailure::Expired);
467    }
468    let ceiling = now
469        .checked_add(MAX_ASSERTION_LIFETIME)
470        .ok_or(AssertionFailure::Expired)?;
471    if expires_at > ceiling {
472        return Err(AssertionFailure::Expired);
473    }
474
475    // RFC 7523 section 3 (5) and (6): `nbf` and `iat` are OPTIONAL and are checked when present.
476    // Both get the skew leeway, because both can only ever refuse an assertion that is otherwise
477    // fine.
478    //
479    // Checked for the same reason `exp` above is, and mapped to `NotYetValid` for the same kind of
480    // reason: a `nbf` or `iat` too large to represent is a time in the future, which is exactly what
481    // this loop refuses. A value that overflows must never be the one value that skips the check.
482    let horizon = now
483        .checked_add(CLOCK_SKEW_LEEWAY)
484        .ok_or(AssertionFailure::NotYetValid)?;
485    for claim in ["nbf", "iat"] {
486        if let Some(value) = jws.claim_time(claim) {
487            match UNIX_EPOCH.checked_add(Duration::from_secs(value)) {
488                Some(instant) if instant <= horizon => {}
489                _ => return Err(AssertionFailure::NotYetValid),
490            }
491        }
492    }
493
494    // RFC 7523 section 3 (7): the `jti` is what single use is enforced ON. An assertion without one
495    // cannot be tracked, and an untrackable bearer credential is one that anybody who saw the
496    // request can send again, so it is refused outright rather than accepted with the replay check
497    // quietly skipped. "We could not check this" must never read as "checked out".
498    let jti = jws.claim_str("jti").unwrap_or_default();
499    if jti.is_empty() {
500        return Err(AssertionFailure::MissingJti);
501    }
502
503    Ok(VerifiedAssertion {
504        jti: jti.to_string(),
505        expires_at,
506    })
507}
508
509/// The `sub` an assertion claims, WITHOUT verifying anything about it.
510///
511/// RFC 7521 section 4.2 makes `client_id` optional on a request that carries an assertion, because
512/// the assertion already names the client. Something still has to LOCATE the registration before
513/// the registration can decide the key, and this is that something.
514///
515/// It is safe only because of what the caller does next, and the caller is the only reason this is
516/// public: the value returned here is used to look up a client, and then
517/// [`verify_assertion`] re-checks `iss` and `sub` against that client's own id under the
518/// registration's key. Nothing is believed on the strength of this read. A caller that used the
519/// result for anything else, an audit record naming the client, a rate limit bucket, an
520/// authorization decision, would be trusting an unsigned string an attacker wrote.
521pub fn unverified_subject(assertion: &str) -> Option<String> {
522    // (0) SIZE, on the same terms and for the same reason as in [`verify_assertion`], and if
523    // anything more urgently: this is the EARLIER of the two, called to find the registration that
524    // will decide the key, so it runs on a string nothing at all is yet known about. A bound
525    // enforced only by the verifier would leave the lookup that precedes it unbounded.
526    if assertion.len() > MAX_ASSERTION_BYTES {
527        return None;
528    }
529
530    let jws = CompactJws::parse(assertion).ok()?;
531    jws.claim_str("sub").map(str::to_string)
532}
533
534/// Whether the assertion names one of `audiences`, in either of the two shapes RFC 7519 section
535/// 4.1.3 allows for the claim.
536fn audience_matches(jws: &CompactJws<'_>, audiences: &[&str]) -> bool {
537    match jws.payload.get("aud") {
538        Some(serde_json::Value::String(one)) => audiences.iter().any(|a| a == one),
539        Some(serde_json::Value::Array(many)) => many
540            .iter()
541            .filter_map(|v| v.as_str())
542            .any(|one| audiences.contains(&one)),
543        _ => false,
544    }
545}
546
547// The unit tests need a key that can SIGN, so they need `jwt-p256`, the built-in ES256 backend.
548// `jwt` alone carries the `Es256Signer`/`Es256Verifier` seam and no curve arithmetic at all, and a
549// test that cannot produce a signature cannot test a verifier.
550#[cfg(all(test, feature = "jwt-p256"))]
551#[path = "tests/client_assertion.rs"]
552mod tests;