Skip to main content

oauth_as/
token.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! Token wire and storage shapes: the RFC 6749 section 5.1 success response, plus the records the
5//! server persists through [`crate::store::Storage`].
6//!
7//! Access and refresh tokens are OPAQUE random strings by default. Under the `jwt` feature the
8//! WIRE access token becomes an RFC 9068 structured token and the opaque string becomes its `jti`;
9//! the shapes here are unchanged either way, which is the point. [`IssuedToken`] is persisted
10//! whichever form went out, keyed by whatever the client will actually present, so RFC 7662
11//! introspection and RFC 7009 revocation keep working and a revoked JWT is genuinely dead at this
12//! server rather than merely deprecated.
13
14use std::fmt;
15use std::time::SystemTime;
16
17use serde::{Deserialize, Serialize};
18
19use crate::client::ClientId;
20use crate::scope::ScopeSet;
21
22/// `token_type` values this server issues: `Bearer` (RFC 6750), and `DPoP` (RFC 9449 section 5)
23/// under the `dpop` feature when the request proved possession of a key. Both registered values
24/// are case-insensitive on the wire but conventionally spelled as the renames below pin them.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26/// `#[non_exhaustive]`: the `Dpop` variant exists only under the `dpop` feature, so a host that
27/// matches this exhaustively compiles today and stops compiling the day anything in its dependency
28/// graph turns `dpop` on. Naming either variant still works; only the match needs a wildcard arm.
29/// A `token_type` is exactly the thing a host branches on when deciding how to hand the response
30/// to its client, so this is the enum most likely to be matched and the least affordable to leave
31/// open.
32#[non_exhaustive]
33pub enum TokenType {
34    /// RFC 6750 bearer token.
35    #[serde(rename = "Bearer")]
36    Bearer,
37    /// RFC 9449 section 5 sender-constrained token, bound to the key the client proved possession
38    /// of. The spelling is `DPoP`, exactly, because RFC 9449 section 7.1 makes it the HTTP
39    /// authentication scheme name the client will present the token under.
40    #[cfg(feature = "dpop")]
41    #[serde(rename = "DPoP")]
42    Dpop,
43}
44
45/// The RFC 6749 section 5.1 successful token response.
46///
47/// `Debug` is hand-written (see below) rather than derived: `access_token` and `refresh_token` are
48/// bearer credentials (RFC 6750 section 1 for the access token; RFC 9700 section 4.14.2 for the
49/// refresh token), so a host doing the obvious `tracing::debug!(?response)` must not thereby write
50/// either to its logs.
51#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
52/// `#[non_exhaustive]`: `authorization_details` appears only under the `rar` feature, so this
53/// struct's field set moves with a flag no host controls alone. This is an OUTPUT: the crate builds
54/// it and the host serializes it, so the paths that matter are unaffected, and a host that needs to
55/// build one anyway (a proxy, a test double) still has `Deserialize`, which is derived in here and
56/// so keeps working from outside.
57#[non_exhaustive]
58pub struct TokenResponse {
59    /// The access token: an opaque random string, or an RFC 9068 JWT under the `jwt` feature.
60    pub access_token: String,
61    /// `Bearer` (RFC 6750), or `DPoP` (RFC 9449 s5) when the `dpop` feature is on and the token
62    /// request carried a proof, because a sender-constrained token MUST NOT be presented as a
63    /// bearer token.
64    pub token_type: TokenType,
65    /// Lifetime in seconds (RECOMMENDED by the RFC; this server always includes it).
66    pub expires_in: u64,
67    /// The rotating refresh token, when the grant and server config produce one.
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub refresh_token: Option<String>,
70    /// Space-delimited granted scope. This server always includes it when non-empty, which also
71    /// satisfies the section 3.3 requirement to report a scope differing from the request.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub scope: Option<String>,
74    /// The RFC 9396 authorization details as GRANTED, which section 7 makes a MUST for a response
75    /// to a request that carried them.
76    ///
77    /// It is a MUST for the same reason RFC 6749 section 3.3 has `scope` echoed when it differs
78    /// from the request: section 7.1 explicitly permits what was granted to differ from what was
79    /// asked for, because the host's consent screen may narrow or enrich it. Without this member a
80    /// client has no way to learn that what it holds is not what it requested, and would go on to
81    /// call a resource server believing it can do something it cannot.
82    ///
83    /// Omitted entirely when empty, so a deployment that never uses authorization details emits
84    /// exactly the body it emitted before this existed.
85    ///
86    /// `#[serde(default)]` is what makes that omission READABLE, and it is not optional beside a
87    /// `skip_serializing_if`. The two together are a matched pair: a member left out on the way out
88    /// has to be allowed to be absent on the way back in, or the type cannot parse the very body it
89    /// just emitted. Without it the ordinary response above is refused with `missing field
90    /// "authorization_details"`, which also falsifies the `#[non_exhaustive]` note above promising a
91    /// host that `Deserialize` "keeps working from outside": a proxy or a test double reading a
92    /// response back would break the moment `rar` appeared anywhere in its dependency graph.
93    #[cfg(feature = "rar")]
94    #[serde(
95        default,
96        skip_serializing_if = "crate::rar::AuthorizationDetails::is_empty"
97    )]
98    pub authorization_details: crate::rar::AuthorizationDetails,
99}
100
101/// Hand-written so neither `access_token` nor `refresh_token` ever prints. `refresh_token` keeps
102/// its `Some`/`None` shape (via `redact_opt`, mirrored from [`crate::server::TokenRequest`]'s
103/// hand-written `Debug`): whether a refresh token was issued at all is diagnostic, not secret, and
104/// collapsing `Some("[redacted]")` and `None` to the same output would hide that.
105impl fmt::Debug for TokenResponse {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        fn redact_opt<T>(value: &Option<T>) -> Option<&'static str> {
108            value.as_ref().map(|_| "[redacted]")
109        }
110        f.debug_struct("TokenResponse")
111            .field("access_token", &"[redacted]")
112            .field("token_type", &self.token_type)
113            .field("expires_in", &self.expires_in)
114            .field("refresh_token", &redact_opt(&self.refresh_token))
115            .field("scope", &self.scope)
116            .finish()
117    }
118}
119
120/// The RFC 7800 section 3.1 confirmation claim: HOW a token is sender constrained, meaning
121/// what a presenter has to prove in addition to holding the string.
122///
123/// This is what a resource server checks the binding against, and it is the whole reason
124/// sender constraining is worth anything at introspection time: without it the binding is
125/// known only to the authorization server, and an RS that introspects is back to trusting a
126/// bearer string.
127///
128/// EVERY MEMBER IS OPTIONAL, and that is the design rather than an accident. RFC 7800 section
129/// 3.1 defines `cnf` as a JSON OBJECT whose members are confirmation methods, and different
130/// sender-constraining mechanisms register different members OF THE SAME OBJECT: RFC 9449
131/// section 6.1 registers `jkt` for a DPoP key binding, RFC 8705 section 3.1 registers
132/// `x5t#S256` for a certificate binding. A token can legitimately carry both, so neither may
133/// be modelled as "the" confirmation and neither may overwrite the other. Adding a mechanism
134/// means adding an optional member here; it never means replacing this type.
135/// DESERIALIZED THROUGH `ConfirmationWire`, for the reason [`IntrospectionResponse`] is
136/// deserialized through `IntrospectionWire`, and the member set above is why it has to be
137/// separate from that one. `cnf` is an OBJECT of confirmation methods and each feature registers
138/// its own, so a build with `dpop` and not `mtls` carries the OUTER member and not the inner one:
139/// the guard on `IntrospectionWire::cnf` passes (the member IS present), the interior deserializes
140/// to a `Confirmation` with nothing in it, and [`Confirmation::is_empty`] answers `true`. That is
141/// the certificate-bound-token-read-as-a-bearer-token case in full, arrived at through the guard
142/// meant to stop it. The interior needs the same treatment as the exterior, and this is it.
143#[cfg(any(feature = "dpop", feature = "mtls"))]
144#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(try_from = "ConfirmationWire")]
146/// `#[non_exhaustive]`: the paragraph above says adding a sender-constraining mechanism means
147/// adding an optional member here, and `dpop` and `mtls` each add one INDEPENDENTLY, so there are
148/// four different field sets this type has depending on which pair of flags is on. The attribute is
149/// what makes that promise cost a host nothing: build with `Confirmation::default()` (every member
150/// is optional by construction) and set the members the mechanism you use registers.
151#[non_exhaustive]
152pub struct Confirmation {
153    /// RFC 9449 section 6.1 `jkt`: the RFC 7638 SHA-256 thumbprint of the client's proof
154    /// key, base64url without padding.
155    #[cfg(feature = "dpop")]
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub jkt: Option<String>,
158    /// RFC 8705 section 3.1 `x5t#S256`: the SHA-256 thumbprint of the DER encoding of the
159    /// X.509 certificate the client presented when the token was issued. A resource server
160    /// checks it with [`Confirmation::confirms_certificate`].
161    #[cfg(feature = "mtls")]
162    #[serde(rename = "x5t#S256", default, skip_serializing_if = "Option::is_none")]
163    pub x5t_s256: Option<crate::mtls::CertificateThumbprint>,
164}
165
166#[cfg(any(feature = "dpop", feature = "mtls"))]
167impl Confirmation {
168    /// Wrap a DPoP key thumbprint.
169    #[cfg(feature = "dpop")]
170    pub fn jkt(jkt: impl Into<String>) -> Self {
171        Confirmation {
172            jkt: Some(jkt.into()),
173            #[cfg(feature = "mtls")]
174            x5t_s256: None,
175        }
176    }
177
178    /// Whether this names no confirmation method at all, which is what an ordinary bearer
179    /// token has. The `cnf` member is OMITTED for such a token rather than sent as an empty
180    /// object: an empty `cnf` claims a constraint exists and then names none, which is worse
181    /// than silence.
182    pub fn is_empty(&self) -> bool {
183        #[cfg(feature = "dpop")]
184        if self.jkt.is_some() {
185            return false;
186        }
187        #[cfg(feature = "mtls")]
188        if self.x5t_s256.is_some() {
189            return false;
190        }
191        true
192    }
193}
194
195/// The deserialize-side mirror of [`Confirmation`], and the reason that type does not derive
196/// `Deserialize` directly.
197///
198/// Same construction as [`IntrospectionWire`] and for the same reason, one level further in: a
199/// confirmation METHOD this build cannot represent is parsed and refused rather than dropped. The
200/// members are the ones RFC 9449 section 6.1 and RFC 8705 section 3.1 register, and a build has
201/// each one either as itself or as an `IgnoredAny` that remembers only that it was there.
202///
203/// NOT `#[serde(deny_unknown_fields)]`, for the reason `IntrospectionWire` is not: RFC 7800
204/// section 3.1 defines `cnf` as an open set of confirmation methods and registers others this
205/// crate does not implement, so a `cnf` naming one of those is conformant. Only the two methods
206/// this crate knows the meaning of are refused when it cannot hold them.
207#[cfg(any(feature = "dpop", feature = "mtls"))]
208#[derive(Deserialize)]
209struct ConfirmationWire {
210    #[cfg(feature = "dpop")]
211    jkt: Option<String>,
212    #[cfg(feature = "mtls")]
213    #[serde(rename = "x5t#S256")]
214    x5t_s256: Option<crate::mtls::CertificateThumbprint>,
215    // The methods this build cannot represent.
216    #[cfg(not(feature = "dpop"))]
217    jkt: Option<serde::de::IgnoredAny>,
218    #[cfg(not(feature = "mtls"))]
219    #[serde(rename = "x5t#S256")]
220    x5t_s256: Option<serde::de::IgnoredAny>,
221}
222
223#[cfg(any(feature = "dpop", feature = "mtls"))]
224impl TryFrom<ConfirmationWire> for Confirmation {
225    type Error = UnrepresentableMember;
226
227    fn try_from(wire: ConfirmationWire) -> Result<Self, Self::Error> {
228        #[cfg(not(feature = "dpop"))]
229        if wire.jkt.is_some() {
230            return Err(
231                "confirmation carries `jkt` (RFC 9449 s6.1), so the token is bound to a DPoP key \
232                 and this build of oauth-as cannot represent that: rebuild with the `dpop` feature",
233            );
234        }
235        #[cfg(not(feature = "mtls"))]
236        if wire.x5t_s256.is_some() {
237            return Err(
238                "confirmation carries `x5t#S256` (RFC 8705 s3.1), so the token is bound to a \
239                 client certificate and this build of oauth-as cannot represent that: rebuild \
240                 with the `mtls` feature",
241            );
242        }
243        Ok(Confirmation {
244            #[cfg(feature = "dpop")]
245            jkt: wire.jkt,
246            #[cfg(feature = "mtls")]
247            x5t_s256: wire.x5t_s256,
248        })
249    }
250}
251
252/// The RFC 7009 section 2.1 `token_type_hint`. A hint the server disagrees with is not an error:
253/// section 2.1 requires it to keep looking, so this only chooses which lookup runs first.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum TokenTypeHint {
257    /// The caller believes this is an access token.
258    AccessToken,
259    /// The caller believes this is a refresh token.
260    RefreshToken,
261}
262
263impl std::str::FromStr for TokenTypeHint {
264    type Err = ();
265
266    fn from_str(s: &str) -> Result<Self, Self::Err> {
267        match s {
268            "access_token" => Ok(TokenTypeHint::AccessToken),
269            "refresh_token" => Ok(TokenTypeHint::RefreshToken),
270            _ => Err(()),
271        }
272    }
273}
274
275/// The RFC 7662 section 2.2 introspection response.
276///
277/// `active` is the only REQUIRED member, and for an inactive token it is the ONLY member: section
278/// 2.2 is explicit that the server should not describe a token the caller has not proven it
279/// holds, and section 4 explains why (the endpoint would otherwise answer questions about tokens
280/// an attacker merely guessed).
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282/// DESERIALIZED THROUGH a private mirror (`IntrospectionWire` in this module), which is what makes
283/// a member this build cannot represent an ERROR rather than a silent omission: `token_type` has
284/// always failed loudly for a `DPoP` token in a build without `dpop`, and the five feature-gated
285/// members beside it used to fail silently for the same class of response. What is NOT refused is
286/// an unknown member, which RFC 7662 section 2.2 explicitly permits a server to send.
287#[serde(try_from = "IntrospectionWire")]
288/// `#[non_exhaustive]`: four separate features (`consent`, `rar`, `dpop`, `mtls`) each add a member
289/// here, which is more feature-driven variation than any other wire body this crate publishes.
290/// [`IntrospectionResponse::inactive`] is the construction path and always was the sensible one:
291/// start from the one-member refusal and fill in what the token actually is, rather than writing
292/// out a literal that has to name every claim the current flag set happens to produce.
293#[non_exhaustive]
294pub struct IntrospectionResponse {
295    /// Whether the token is currently active.
296    pub active: bool,
297    /// Space-delimited granted scope.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub scope: Option<String>,
300    /// The client the token was issued to.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub client_id: Option<String>,
303    /// The resource owner the token acts for.
304    ///
305    /// ABSENT FOR A CLIENT-CREDENTIALS TOKEN, and that is a deliberate disagreement with the
306    /// signed token beside it. RFC 6749 section 4.4 has no resource owner, so RFC 7662 section
307    /// 2.2's "usually a machine-readable identifier of the resource owner" has nothing to name and
308    /// the member is omitted; RFC 9068 section 2.2 makes `sub` REQUIRED in a JWT access token and
309    /// directs the AS to put the `client_id` there instead. Both are right, and a resource server
310    /// reading one token through both channels therefore sees a subject in the JWT and no subject
311    /// from introspection. Neither says a user was involved.
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub sub: Option<String>,
314    /// The token type (RFC 6750 `Bearer`).
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub token_type: Option<TokenType>,
317    /// Expiry, as seconds since the Unix epoch.
318    #[serde(skip_serializing_if = "Option::is_none")]
319    pub exp: Option<u64>,
320    /// Issuance, as seconds since the Unix epoch.
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub iat: Option<u64>,
323    /// The issuer of the token.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub iss: Option<String>,
326    /// The resource server(s) the token is for: the RFC 8707 resource indicators the grant was
327    /// narrowed to.
328    ///
329    /// RFC 7662 section 2.2 lists `aud` as OPTIONAL and defers its shape to RFC 7519 section 4.1.3,
330    /// which admits either a single string or an array. This crate always emits the ARRAY form when
331    /// it has an audience at all, because a caller that has to handle two shapes for one claim
332    /// eventually handles only one of them; and it omits the member entirely, rather than sending
333    /// an empty array, when no resource was requested. An empty array reads as "restricted to
334    /// nothing", which is the opposite of the truth.
335    ///
336    /// UNDER `jwt`, THE SIGNED `aud` MAY BE NARROWER THAN THIS MEMBER'S ABSENCE SUGGESTS. This is
337    /// the grant's RFC 8707 resource indicators and nothing else, so a grant that named none omits
338    /// the member; the signed access token for that same grant carries the DEPLOYMENT-WIDE
339    /// [`crate::jwt::JwtConfig::audience`] instead, because RFC 9068 section 2.2 makes `aud`
340    /// required and a token has to name somebody. So "no `aud` here" means "this grant was
341    /// narrowed to no particular resource server", NOT "this token is valid everywhere": a
342    /// resource server enforcing audience restriction should enforce it from the token when it has
343    /// one, and treat this member as the grant's narrowing on top.
344    #[serde(skip_serializing_if = "Option::is_none")]
345    pub aud: Option<Vec<String>>,
346    /// RFC 9470 section 6.2: when the resource owner behind this token authenticated, as seconds
347    /// since the Unix epoch (OpenID Connect Core section 2 `auth_time`).
348    ///
349    /// This is what makes a step-up challenge answerable at all: a resource server that asked for a
350    /// `max_age` has to be able to see whether the token it now holds actually satisfies it, and
351    /// RFC 9470 section 6 names introspection (section 6.2) as one of the two places it may look,
352    /// the other being the JWT itself (section 6.1). Present exactly
353    /// when the host REPORTED an authentication for the grant (see
354    /// [`crate::consent::Authentication`]), and omitted rather than sent as `null` when it did not,
355    /// because a null there reads to a careless resource server as a freshness it has checked.
356    #[cfg(feature = "consent")]
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub auth_time: Option<u64>,
359    /// RFC 9470 section 6.2: the authentication context class the host reported for the grant
360    /// (OpenID Connect Core section 2 `acr`). Opaque to this crate; see
361    /// [`crate::consent::Authentication::acr`].
362    #[cfg(feature = "consent")]
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub acr: Option<String>,
365    /// RFC 9396 section 9.2: the authorization details this token carries, as a top-level
366    /// member of the introspection response. That section is how a resource server holding
367    /// an OPAQUE token learns what the token actually authorizes, which is the whole reason
368    /// the parameter exists.
369    ///
370    /// A resource server reads this member since 0.9.2, when it is registered in
371    /// [`crate::ServerConfig::resource_servers`] and the token is addressed to it.
372    ///
373    /// Omitted rather than empty when the grant carried none, for the same reason `aud` is:
374    /// an empty array reads as "authorized for nothing in particular", which is a statement,
375    /// and the truth here is silence.
376    #[cfg(feature = "rar")]
377    #[serde(
378        default,
379        skip_serializing_if = "crate::rar::AuthorizationDetails::is_empty"
380    )]
381    pub authorization_details: crate::rar::AuthorizationDetails,
382    /// How this token is sender constrained, present exactly when it is: RFC 9449 section 6.1
383    /// `jkt` for a DPoP key, RFC 8705 section 3.2 `x5t#S256` for a client certificate, or both.
384    ///
385    /// RFC 7662 section 2.2 lets a server return any claim it likes here, and RFC 9449 section 5
386    /// and RFC 8705 section 3.2 are each explicit that a resource server has to be able to
387    /// confirm the binding. Omitted rather than sent as `null` for an unbound token, because
388    /// `"cnf": null` reads to a careless RS as a confirmation it has already checked.
389    #[cfg(any(feature = "dpop", feature = "mtls"))]
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub cnf: Option<Confirmation>,
392    /// RFC 8693 section 4.1 `act`: who authority was delegated TO, present exactly when this token
393    /// came from a DELEGATION token exchange.
394    ///
395    /// RFC 7662 section 2.2 lets a server return any claim it likes here, and this is the claim an
396    /// opaque token has nowhere else to put. Without it a resource server cannot tell "A acting
397    /// for B" from "B", which is the entire distinction RFC 8693 section 1.1 draws between
398    /// delegation and impersonation, and the reason a deployment would choose delegation at all.
399    ///
400    /// Omitted rather than sent as `null`, for the same reason `cnf` next door is: a member that
401    /// is present and null invites a careless reader to treat it as answered.
402    #[cfg(feature = "token-exchange")]
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub act: Option<crate::token_exchange::ActClaim>,
405}
406
407impl IntrospectionResponse {
408    /// The one-member answer for a token that is unknown, expired, or not the caller's.
409    pub fn inactive() -> Self {
410        IntrospectionResponse {
411            active: false,
412            scope: None,
413            client_id: None,
414            sub: None,
415            token_type: None,
416            exp: None,
417            iat: None,
418            iss: None,
419            aud: None,
420            #[cfg(feature = "consent")]
421            auth_time: None,
422            #[cfg(feature = "consent")]
423            acr: None,
424            #[cfg(feature = "rar")]
425            authorization_details: crate::rar::AuthorizationDetails::none(),
426            #[cfg(any(feature = "dpop", feature = "mtls"))]
427            cnf: None,
428            #[cfg(feature = "token-exchange")]
429            act: None,
430        }
431    }
432}
433
434/// The deserialize-side mirror of [`IntrospectionResponse`], and the reason that type does not
435/// derive `Deserialize` directly.
436///
437/// WHAT IT IS FOR: five members of the response exist only under a cargo feature (`cnf` under
438/// `dpop` or `mtls`, `act` under `token-exchange`, `auth_time` and `acr` under `consent`,
439/// `authorization_details` under `rar`), and a derived `Deserialize` in a build without the
440/// feature DROPS them without a word. Four of the five say the token is NARROWER than a plain
441/// bearer token, so the reader that loses one concludes the safest-looking thing: a
442/// certificate-bound token read as an ordinary bearer token, a delegated token read as the
443/// principal itself, a transaction-scoped token read as unrestricted. The same object's
444/// `token_type` has never had that problem, because `"DPoP"` in a build without `dpop` is an
445/// unknown enum variant and serde says so. This makes the rest of the object behave like
446/// `token_type`.
447///
448/// NOT `#[serde(deny_unknown_fields)]`, deliberately. RFC 7662 section 2.2 says specific
449/// implementations "MAY extend this structure with their own service-specific response names as
450/// top-level members", so a response carrying `username` or `nbf` is conformant; denying every
451/// unknown member would convert a silent drop into an inability to read a legitimate response at
452/// all, which is a worse failure and a more likely one. Only the members THIS CRATE KNOWS THE
453/// MEANING OF, and would therefore be discarding knowingly, are refused.
454///
455/// The field list is a mirror and mirrors drift, so `tests/introspection_feature_bounds.rs`
456/// round-trips a fully populated response for whatever flag set it is built with.
457#[derive(Deserialize)]
458struct IntrospectionWire {
459    active: bool,
460    scope: Option<String>,
461    client_id: Option<String>,
462    sub: Option<String>,
463    token_type: Option<TokenType>,
464    exp: Option<u64>,
465    iat: Option<u64>,
466    iss: Option<String>,
467    aud: Option<Vec<String>>,
468    #[cfg(feature = "consent")]
469    auth_time: Option<u64>,
470    #[cfg(feature = "consent")]
471    acr: Option<String>,
472    #[cfg(feature = "rar")]
473    #[serde(default)]
474    authorization_details: crate::rar::AuthorizationDetails,
475    #[cfg(any(feature = "dpop", feature = "mtls"))]
476    cnf: Option<Confirmation>,
477    #[cfg(feature = "token-exchange")]
478    act: Option<crate::token_exchange::ActClaim>,
479    // The members this build cannot represent. `IgnoredAny` parses the value and keeps nothing:
480    // what is wanted is the knowledge that it was THERE, which is exactly what this build would
481    // otherwise not have.
482    #[cfg(not(feature = "consent"))]
483    auth_time: Option<serde::de::IgnoredAny>,
484    #[cfg(not(feature = "consent"))]
485    acr: Option<serde::de::IgnoredAny>,
486    #[cfg(not(feature = "rar"))]
487    authorization_details: Option<serde::de::IgnoredAny>,
488    #[cfg(not(any(feature = "dpop", feature = "mtls")))]
489    cnf: Option<serde::de::IgnoredAny>,
490    #[cfg(not(feature = "token-exchange"))]
491    act: Option<serde::de::IgnoredAny>,
492}
493
494/// Why one of the members above could not be kept. `&'static str` rather than a formatted
495/// message: this is a refusal on a parse path, and naming the member and the feature that would
496/// have carried it is the whole of what a reader needs.
497type UnrepresentableMember = &'static str;
498
499impl TryFrom<IntrospectionWire> for IntrospectionResponse {
500    type Error = UnrepresentableMember;
501
502    fn try_from(wire: IntrospectionWire) -> Result<Self, Self::Error> {
503        #[cfg(not(feature = "consent"))]
504        if wire.auth_time.is_some() {
505            return Err(
506                "introspection response carries `auth_time` (RFC 9470 s6.2), which this build of \
507                 oauth-as cannot represent: rebuild with the `consent` feature",
508            );
509        }
510        #[cfg(not(feature = "consent"))]
511        if wire.acr.is_some() {
512            return Err(
513                "introspection response carries `acr` (RFC 9470 s6.2), which this build of \
514                 oauth-as cannot represent: rebuild with the `consent` feature",
515            );
516        }
517        #[cfg(not(feature = "rar"))]
518        if wire.authorization_details.is_some() {
519            return Err(
520                "introspection response carries `authorization_details` (RFC 9396 s9.2), which \
521                 this build of oauth-as cannot represent: rebuild with the `rar` feature",
522            );
523        }
524        #[cfg(not(any(feature = "dpop", feature = "mtls")))]
525        if wire.cnf.is_some() {
526            return Err(
527                "introspection response carries `cnf` (RFC 9449 s6.1 / RFC 8705 s3.2), so the \
528                 token is sender constrained and this build of oauth-as cannot represent that: \
529                 rebuild with the `dpop` or `mtls` feature",
530            );
531        }
532        #[cfg(not(feature = "token-exchange"))]
533        if wire.act.is_some() {
534            return Err(
535                "introspection response carries `act` (RFC 8693 s4.1), so the token is a \
536                 delegation and this build of oauth-as cannot represent that: rebuild with the \
537                 `token-exchange` feature",
538            );
539        }
540        Ok(IntrospectionResponse {
541            active: wire.active,
542            scope: wire.scope,
543            client_id: wire.client_id,
544            sub: wire.sub,
545            token_type: wire.token_type,
546            exp: wire.exp,
547            iat: wire.iat,
548            iss: wire.iss,
549            aud: wire.aud,
550            #[cfg(feature = "consent")]
551            auth_time: wire.auth_time,
552            #[cfg(feature = "consent")]
553            acr: wire.acr,
554            #[cfg(feature = "rar")]
555            authorization_details: wire.authorization_details,
556            #[cfg(any(feature = "dpop", feature = "mtls"))]
557            cnf: wire.cnf,
558            #[cfg(feature = "token-exchange")]
559            act: wire.act,
560        })
561    }
562}
563
564/// The serde default for the two `grant_established_at` fields below: the epoch, because it is the
565/// fail-closed answer. Every barrier is recorded after it, so a record with no stated grant instant
566/// is REFUSED by a standing revocation rather than admitted by one. See
567/// [`IssuedToken::grant_established_at`], which states the whole argument.
568fn grant_established_at_default() -> SystemTime {
569    SystemTime::UNIX_EPOCH
570}
571
572/// A persisted access token: what introspection needs to answer for an opaque token.
573///
574/// `Debug` is hand-written (see below) rather than derived: `access_token` is a bearer credential
575/// (RFC 6750 section 1: possession of the string is the whole of the authorization), so a host
576/// doing the obvious `tracing::debug!(?record)` must not thereby write a live token to its logs.
577#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
578/// `#[non_exhaustive]`: `rar`, `dpop`, `mtls` and `consent` each add a field, so the shape of this
579/// record is decided by the flag set the final binary is linked with rather than by the host that
580/// writes against it.
581///
582/// A [`crate::store::Storage`] implementor does not lose anything: it is HANDED these records and
583/// round-trips them through the derived `Serialize`/`Deserialize`, both of which are generated in
584/// here and are unaffected. `oauth-as-postgres` persists them as a `jsonb` payload for exactly that
585/// reason and never spells a field. For everyone else, including a host seeding a store or writing
586/// a fixture, [`IssuedToken::new`] takes the fields a token cannot exist without and leaves the
587/// rest public to assign.
588///
589/// THE BOUND ON THAT SENTENCE, stated because it is not obvious: "does not lose anything" holds
590/// for ONE flag set. Two binaries built with different features over one store are a different
591/// situation, and this type has no answer for it: a reader without `rar` deserializes a record
592/// whose `authorization_details` it has no field for, the derived `Deserialize` drops the member
593/// in silence, and a rotation writes the shortened record back. Serialization is not where that
594/// gets caught -- [`IntrospectionResponse`] and [`Confirmation`] are read from a FOREIGN server
595/// and are guarded on the way in, whereas these records are this deployment's own and the guard
596/// would have to be a decision about what a store should do when it hands back a record this
597/// binary cannot represent. A MIXED-FLAG FLEET OVER ONE STORE IS THEREFORE NOT A SUPPORTED
598/// DEPLOYMENT of this crate, and the same caveat applies to [`RefreshTokenRecord`],
599/// [`crate::authorization::AuthorizationCodeRecord`] and
600/// [`crate::par::PushedAuthorizationRequest`].
601#[non_exhaustive]
602pub struct IssuedToken {
603    /// The opaque access token string (the storage key).
604    pub access_token: String,
605    /// The client the token was issued to.
606    pub client_id: ClientId,
607    /// The resource owner the token acts for; `None` for client-only grants.
608    pub subject: Option<String>,
609    /// The granted scope.
610    pub scope: ScopeSet,
611    /// The RFC 8707 resource indicators this token is restricted to; empty when the grant named
612    /// none. This is what RFC 7662 introspection reports as `aud`, and what the RFC 9068 `aud`
613    /// claim carries when the `jwt` feature signs the wire token.
614    ///
615    /// THE TWO CHANNELS PART COMPANY WHEN THIS IS EMPTY, which the sentence above used to deny.
616    /// Introspection then omits `aud` altogether (an empty array would read as "restricted to
617    /// nothing"; see [`IntrospectionResponse::aud`]), while the signed token cannot omit it,
618    /// because RFC 9068 section 2.2 makes the claim REQUIRED: it carries the deployment-wide
619    /// [`crate::jwt::JwtConfig::audience`] instead. So for a grant that named no resource a
620    /// caller reads "no restriction stated" from introspection and "restricted to the
621    /// configured audience" from the token, for one token. Both are true statements about
622    /// different things: this field is the GRANT'S narrowing, and the configured audience is the
623    /// deployment's standing one. Non-empty, they agree exactly.
624    ///
625    /// SINCE 0.9.2 THIS FIELD ALSO DECIDES WHO MAY ASK. It is what a registered resource server is
626    /// matched against, so a token whose grant named no resource is introspectable by its own
627    /// client alone; see [`crate::ServerConfig::resource_servers`]. A resource server that is
628    /// answered sees only its OWN identifiers here, not the whole set.
629    pub resource: Vec<String>,
630    /// The RFC 9396 authorization details this token carries (section 7: the AS returns the
631    /// details as granted and assigned to the access token). This is what RFC 7662
632    /// introspection reports as `authorization_details` (section 9.2) and what the RFC 9068
633    /// claim carries when the `jwt` feature signs the wire token (section 9.1).
634    ///
635    /// `#[serde(default)]`, for the same reason `grant_established_at` below has one and reached
636    /// by the other door: TURNING THE FEATURE ON must not make what is already in the store
637    /// unreadable. A record written by a build without `rar` carries no such key, this field is not
638    /// an `Option` so serde's derive supplies no default of its own, and the read fails outright
639    /// with `missing field "authorization_details"`. That is not a migration an operator can plan
640    /// around either, because cargo feature unification means a dependency can turn `rar` on
641    /// without the host asking (see `tests/host_api_shape.rs`): the build changes and every live
642    /// grant stops being readable at once. The default is the empty set, which is the truth about a
643    /// grant minted before the feature existed, and it is also the safe direction: an empty set
644    /// authorizes nothing extra.
645    #[cfg(feature = "rar")]
646    #[serde(default)]
647    pub authorization_details: crate::rar::AuthorizationDetails,
648    /// Issuance instant.
649    pub issued_at: SystemTime,
650    /// The instant the GRANT behind this token was authorized, which is NOT `issued_at`.
651    ///
652    /// For a code redemption it is when the code was minted; for a refresh rotation it is carried
653    /// forward unchanged from the chain, so every token along a chain reports the one decision that
654    /// started it. For a grant with no resource owner behind it (client credentials) it is the
655    /// instant of issuance, because the client's own registration is the only authorization there
656    /// is.
657    ///
658    /// FOR A DEVICE GRANT IT IS WHEN THE DEVICE ASKED, NOT WHEN THE USER APPROVED, and that is a
659    /// known approximation rather than the intent. RFC 8628 section 3.3 approval happens at the
660    /// host's own verification UI and [`crate::device::DeviceGrantState::Approved`] records only
661    /// the subject, so the approval instant is never persisted and there is nothing truer to carry;
662    /// [`crate::device::DeviceGrant::created_at`] is what exists. The gap is the grant's whole
663    /// lifetime (RFC 8628 section 3.2 `expires_in`, typically minutes), and it errs in the
664    /// FAIL-CLOSED direction: a barrier recorded inside that window refuses a decision the user
665    /// made after it, so a user who withdraws consent and then re-approves the same pending device
666    /// grant is refused and has to start the device flow again. A false refusal, never a false
667    /// admission. Closing it properly means persisting the approval instant, which is a breaking
668    /// change to that enum variant.
669    ///
670    /// A [`crate::store::RevocationBarrier`] is compared against this rather than against
671    /// `issued_at`: a rotation and a re-approval both WRITE at `now`, so `now` cannot tell a grant
672    /// that predates a revocation from one made after it. See
673    /// [`crate::store::RevocationWindow::recorded_at`].
674    ///
675    /// `#[serde(default)]`, and the default is the epoch, which is the FAIL-CLOSED direction.
676    /// This field is new in 0.9.1, so a record a 0.9.0 node wrote — or is still writing, during a
677    /// rolling upgrade — carries no such key, and without a default the read fails outright and
678    /// every token that release issued becomes unreadable the moment this one starts. With it, the
679    /// record deserializes and dates from before every barrier that could ever be recorded, so a
680    /// standing revocation REFUSES it rather than admitting it. A far-future default would
681    /// deserialize just as happily and ADMIT every record 0.9.0 wrote, which is exactly the
682    /// resurrection this field exists to close, reintroduced through the upgrade path. The
683    /// There is deliberately NO backfill migration: a backfill cannot reach a 0.9.0 node still
684    /// writing field-less payloads during a rolling upgrade, which is the window that matters, so
685    /// the serde default covers strictly more than one would.
686    #[serde(default = "grant_established_at_default")]
687    pub grant_established_at: SystemTime,
688    /// Expiry instant; the token is dead at and after this instant.
689    pub expires_at: SystemTime,
690    /// RFC 9449 section 6: the RFC 7638 thumbprint of the DPoP key this token is bound to, or
691    /// `None` for an ordinary bearer token.
692    ///
693    /// `Option<Box<str>>` rather than `Option<String>`, and feature gated, because this record is
694    /// written and read on every token-plane request and `tests/allocation.rs` holds it to a size
695    /// budget: the box is 16 bytes against a `String`'s 24, and a deployment without the `dpop`
696    /// feature pays neither. The value is a fixed 43-character base64url digest that is never
697    /// appended to, so the growable capacity a `String` carries would be dead weight.
698    #[cfg(feature = "dpop")]
699    pub jkt: Option<Box<str>>,
700    /// RFC 8705 section 3: the SHA-256 thumbprint of the client certificate this token is
701    /// bound to, or `None` for a token that is not certificate bound.
702    ///
703    /// Recorded on the AS side, and not only inside a signed JWT, for the same reason `jkt`
704    /// next door is: this crate's default access token is OPAQUE, and RFC 8705 section 3.2
705    /// has a resource server learn the binding by INTROSPECTING, which it can only be told
706    /// if it was persisted. The channel that carries it to a resource server arrived in 0.9.2;
707    /// the field was persisted before that, because the RECORD, not the response, is the thing
708    /// that cannot be added later.
709    ///
710    /// `Option<Box<_>>` rather than the 32-byte thumbprint inline, on the same measurement
711    /// as `jkt`: this record is written and read on every token-plane request and
712    /// `tests/allocation.rs` holds it to a size budget, so an unbound token pays one null
713    /// pointer and the allocation happens only for a token that is actually bound.
714    #[cfg(feature = "mtls")]
715    pub x5t_s256: Option<Box<crate::mtls::CertificateThumbprint>>,
716    /// The authorization grant this token belongs to (see [`RefreshTokenRecord::family_id`]).
717    ///
718    /// RFC 9700 section 4.14.2 requires that detecting refresh token reuse revokes "the tokens
719    /// issued for that authorization grant", not merely the refresh chain, so an access token has
720    /// to be reachable from the grant it came from. `None` for a grant that produced no refresh
721    /// chain (RFC 6749 section 4.4 client credentials), where there is no chain to be reused and
722    /// so nothing to revoke by family.
723    pub family_id: Option<String>,
724    /// RFC 8693 section 4.1 `act`: who authority was delegated TO, for a token issued by a
725    /// DELEGATION token exchange. `None` for every other grant, and for an impersonation exchange,
726    /// which by definition names no actor.
727    ///
728    /// # Why this is on the RECORD and not only in the response
729    ///
730    /// This crate's default access token is OPAQUE, so RFC 7662 introspection is the only channel
731    /// a resource server has for learning anything about it. A delegation that introspection
732    /// cannot see is a delegation the resource server has to take the host's word for, which is
733    /// the one thing section 1.1 delegation exists to avoid: the whole point is that the resource
734    /// can tell "A acting for B" from "B".
735    ///
736    /// The channel that carries it to a resource server arrived in 0.9.2; the field was persisted
737    /// before that, because the RECORD, not the response, is the thing that cannot be added later.
738    ///
739    /// It was left off through 0.9.0 for two reasons, and both are now spent. The first was
740    /// allocation, and [`crate::store::Storage::get_token`] returning an `Arc<IssuedToken>` ended
741    /// it: the record's shape costs a read nothing, and this field costs a deployment without the
742    /// feature zero bytes and one with it 8 bytes per token plus one allocation per DELEGATED
743    /// token. The second was the persistence contract, which was the real one: this is the record
744    /// every host's `Storage` writes, so a new field is a migration in stores this crate does not
745    /// own. That is exactly why it lands HERE, in the release that is already breaking that trait,
746    /// so a host migrates once rather than twice.
747    ///
748    /// BOXED for the same measured reason as `authentication` below: the common case is `None`,
749    /// and this record is written and read on every token-plane request.
750    #[cfg(feature = "token-exchange")]
751    #[cfg_attr(docsrs, doc(cfg(feature = "token-exchange")))]
752    pub act: Option<Box<crate::token_exchange::ActClaim>>,
753    /// What the host reported about the resource owner's authentication when this token's grant was
754    /// approved, or `None` when it reported nothing.
755    ///
756    /// BOXED, so the common `None` costs one null pointer on a record that is written and read on
757    /// every token-plane request rather than the whole struct; `tests/allocation.rs` holds this
758    /// type to a size budget precisely so that a convenience like an inline `SystemTime` plus an
759    /// `Option<String>` cannot be paid for silently. It is what BOTH halves of RFC 9470 section 6
760    /// are answered from: 6.2 at introspection time, and 6.1 at issuance, where the same report
761    /// becomes the `auth_time` and `acr` claims of the signed access token.
762    #[cfg(feature = "consent")]
763    pub authentication: Option<Box<crate::consent::Authentication>>,
764}
765
766impl IssuedToken {
767    /// The five things a persisted access token cannot be without: the string a client will
768    /// present, who it was issued to, who it acts for, what it may do, and when it lives between.
769    ///
770    /// Everything else describes a token that is more than the minimum (an audience restriction, a
771    /// sender-constraining binding, the family it can be revoked with) and is a public field on the
772    /// returned value, so a caller sets what applies and states nothing about what does not. That
773    /// split is the reason the arguments stop here rather than growing one per feature: a caller
774    /// building a record for a build with `dpop` off should not have to mention DPoP.
775    ///
776    /// `subject` is an argument rather than a field to assign because `None` is a real answer and
777    /// not an omission: it means an RFC 6749 section 4.4 client-credentials token, which acts for
778    /// no resource owner, and a caller should have to say so.
779    pub fn new(
780        access_token: impl Into<String>,
781        client_id: ClientId,
782        subject: Option<String>,
783        scope: ScopeSet,
784        issued_at: SystemTime,
785        expires_at: SystemTime,
786    ) -> Self {
787        IssuedToken {
788            // Same FAIL-CLOSED default as `RefreshTokenRecord::new`, and the same reason: a caller
789            // that has not said when its grant was authorized must not thereby outrank a standing
790            // revocation. A caller that knows sets the field on the returned value.
791            grant_established_at: SystemTime::UNIX_EPOCH,
792            access_token: access_token.into(),
793            client_id,
794            subject,
795            scope,
796            resource: Vec::new(),
797            #[cfg(feature = "rar")]
798            authorization_details: crate::rar::AuthorizationDetails::none(),
799            issued_at,
800            expires_at,
801            #[cfg(feature = "dpop")]
802            jkt: None,
803            #[cfg(feature = "mtls")]
804            x5t_s256: None,
805            family_id: None,
806            #[cfg(feature = "token-exchange")]
807            act: None,
808            #[cfg(feature = "consent")]
809            authentication: None,
810        }
811    }
812}
813
814/// Hand-written so the opaque `access_token` never prints. EVERY other field prints, because every
815/// other field is metadata ABOUT the token rather than the credential itself, and the record has to
816/// stay debuggable: `family_id` in particular is what makes an RFC 9700 section 4.14.2 family
817/// revocation traceable, and it is an internal grouping identifier, not a bearer credential.
818///
819/// "Every other field" is the whole rule and it is stated that way deliberately. This impl used to
820/// print eight of thirteen, and the five it dropped were the five added since it was written, which
821/// is what a hand-written `Debug` costs if nobody restates the rule when a field arrives. The
822/// worst omission was `grant_established_at`: it is the SOLE time input to every
823/// [`crate::store::RevocationBarrier`] comparison and its fail-closed default is the epoch, so
824/// "this token is refused by a barrier and I cannot see why" was exactly the question `{:?}` could
825/// not answer. `jkt` and `x5t_s256` are public-key and certificate THUMBPRINTS, which a resource
826/// server is given on the wire in the RFC 7800 `cnf` claim, so neither is secret; `act` is the RFC
827/// 8693 section 4.1 delegation chain, which introspection publishes; `authentication` is the RFC
828/// 9470 report, which introspection publishes as `auth_time` and `acr` (section 6.2) and which a
829/// signed access token carries under the same two names (section 6.1).
830impl fmt::Debug for IssuedToken {
831    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
832        let mut out = f.debug_struct("IssuedToken");
833        out.field("access_token", &"[redacted]")
834            .field("client_id", &self.client_id)
835            .field("subject", &self.subject)
836            .field("scope", &self.scope)
837            .field("resource", &self.resource);
838        #[cfg(feature = "rar")]
839        out.field("authorization_details", &self.authorization_details);
840        out.field("issued_at", &self.issued_at)
841            .field("grant_established_at", &self.grant_established_at)
842            .field("expires_at", &self.expires_at);
843        #[cfg(feature = "dpop")]
844        out.field("jkt", &self.jkt);
845        #[cfg(feature = "mtls")]
846        out.field("x5t_s256", &self.x5t_s256);
847        out.field("family_id", &self.family_id);
848        #[cfg(feature = "token-exchange")]
849        out.field("act", &self.act);
850        #[cfg(feature = "consent")]
851        out.field("authentication", &self.authentication);
852        out.finish()
853    }
854}
855
856/// Whether a persisted refresh token is still redeemable.
857///
858/// Rotated tokens are RETAINED in the `Spent` state rather than deleted, exactly as consumed
859/// authorization codes are (see [`crate::authorization::AuthorizationCodeState`]) and for exactly
860/// the same reason: a token deleted on rotation makes a later presentation indistinguishable from
861/// a typo, and the AS then answers the one signal it gets that a token leaked by disconnecting
862/// whichever party redeemed second, which in practice is the honest one.
863#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
864pub enum RefreshTokenState {
865    /// Live: redeemable exactly once.
866    Active,
867    /// Already rotated away. Presenting it is REUSE, which OAuth 2.1 draft section 6.1 and RFC
868    /// 9700 section 4.14.2 treat as evidence of compromise: the whole family dies.
869    Spent,
870}
871
872/// A persisted refresh token. Single use: redemption goes through
873/// [`crate::store::Storage::take_refresh_token`], and rotation issues a replacement carrying the
874/// SAME `expires_at`, so a chain has an absolute lifetime rather than a sliding one.
875///
876/// `Debug` is hand-written (see below) rather than derived: `refresh_token` is a bearer credential
877/// whose leak is exactly the compromise RFC 9700 section 4.14.2 defends against, so it must not
878/// reach a host's logs through `{:?}`.
879#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
880/// `#[non_exhaustive]`, on the same four features and the same storage argument as
881/// [`IssuedToken`]: a `Storage` implementor round-trips this through serde and never names a field,
882/// and anyone assembling one by hand goes through [`RefreshTokenRecord::new`].
883#[non_exhaustive]
884pub struct RefreshTokenRecord {
885    /// The opaque refresh token string (the storage key).
886    pub refresh_token: String,
887    /// The client the token was issued to; presentation by any other client is `invalid_grant`
888    /// and leaves the record untouched.
889    pub client_id: ClientId,
890    /// The resource owner the chain acts for.
891    pub subject: Option<String>,
892    /// The scope originally granted; refreshes may narrow, never widen.
893    pub scope: ScopeSet,
894    /// The RFC 8707 resource indicators originally granted. Carried across rotation for the same
895    /// reason `scope` is: section 2 lets a token request narrow the set and never widen it, so the
896    /// chain has to remember what it started with. Empty when the grant named none.
897    pub resource: Vec<String>,
898    /// The RFC 9396 authorization details originally granted. Carried across rotation for
899    /// the same reason `scope` and `resource` are: section 6 lets a token request narrow the
900    /// set and never widen it, so the chain has to remember what it started with, and a
901    /// rotation that narrowed must not be climbable back on the next one.
902    ///
903    /// `#[serde(default)]`, which [`IssuedToken::authorization_details`] states in full: a chain
904    /// written by a build without `rar` carries no such key, this is not an `Option` so serde
905    /// supplies no default of its own, and without one every live chain becomes unreadable the
906    /// moment anything in the host's dependency graph turns the feature on.
907    #[cfg(feature = "rar")]
908    #[serde(default)]
909    pub authorization_details: crate::rar::AuthorizationDetails,
910    /// The instant the GRANT behind this chain was authorized, CARRIED ACROSS ROTATION and never
911    /// restamped, for the same reason `scope` and `resource` are carried: the chain has to remember
912    /// the one decision that started it. Restamping it on each rotation would let a chain walk
913    /// forward past a revocation it was supposed to die to.
914    ///
915    /// See [`IssuedToken::grant_established_at`], which this is copied into on every rotation, and
916    /// which states in full why the serde default below is the epoch: a chain a 0.9.0 node wrote
917    /// carries no such key, and the epoch is the reading that a standing revocation REFUSES rather
918    /// than the one it admits.
919    #[serde(default = "grant_established_at_default")]
920    pub grant_established_at: SystemTime,
921    /// Absolute chain expiry; `None` means the chain does not expire by time.
922    ///
923    /// On a `Spent` record this doubles as the RETENTION deadline: a spent token is kept only so
924    /// that its reuse can be recognised, and a chain with no absolute expiry would otherwise keep
925    /// every superseded link forever. The server therefore stamps a spent record from a
926    /// never-expiring chain with `now + ServerConfig::refresh_reuse_window`, which is what makes
927    /// [`crate::store::Storage::sweep_expired`] able to reclaim it.
928    pub expires_at: Option<SystemTime>,
929    /// RFC 9449 section 5: the RFC 7638 thumbprint of the DPoP key this refresh chain is bound
930    /// to, or `None` for an unbound chain.
931    ///
932    /// Carried across rotation and CHECKED on redemption. Without it the binding would be
933    /// decorative for anything but the first access token: a stolen refresh token could simply be
934    /// re-bound to the thief's key on the next rotation, leaving the attacker holding a token they
935    /// can prove possession for and the victim's key the one that gets refused.
936    #[cfg(feature = "dpop")]
937    pub jkt: Option<Box<str>>,
938    /// RFC 8705 section 3: the client certificate this refresh chain is bound to, or `None`
939    /// for an unbound chain.
940    ///
941    /// Carried across rotation and CHECKED on redemption, exactly as `jkt` is and for the
942    /// same argument: without it the binding would be decorative past the first access
943    /// token, because a stolen refresh token could simply be re-bound to whatever
944    /// certificate the thief holds on the next rotation. Section 3 makes this a MUST for
945    /// public clients specifically; this crate applies it to every chain that was issued
946    /// over a certificate, because a chain whose holder proved possession of a key once
947    /// should have to keep proving it, and for a confidential mutual-TLS client the rule
948    /// costs nothing (it presents that certificate on every request anyway).
949    #[cfg(feature = "mtls")]
950    pub x5t_s256: Option<Box<crate::mtls::CertificateThumbprint>>,
951    /// The FAMILY this token belongs to: one identifier shared by every token, access or refresh,
952    /// minted from the same authorization grant, and carried across rotation unchanged.
953    ///
954    /// This is what makes RFC 9700 section 4.14.2 implementable at all. Without it the AS can
955    /// refuse a reused token but cannot reach the tokens the thief already rotated into, which is
956    /// the defence exactly inverted: the victim is locked out and the attacker is not.
957    pub family_id: String,
958    /// Whether this link is still redeemable, or is a retained rotated one.
959    pub state: RefreshTokenState,
960    /// The authentication the host reported when the grant this chain came from was approved,
961    /// carried across rotation UNCHANGED.
962    ///
963    /// Carried rather than restamped because a rotation is not a new authentication: the user is
964    /// not present, nothing has been proven again, and giving a refreshed token a fresh `auth_time`
965    /// would let any client defeat an RFC 9470 `max_age` by refreshing. See
966    /// [`IssuedToken::authentication`] for why it is boxed.
967    #[cfg(feature = "consent")]
968    pub authentication: Option<Box<crate::consent::Authentication>>,
969}
970
971impl RefreshTokenRecord {
972    /// A LIVE link: `state` is [`RefreshTokenState::Active`], because a record nobody has rotated
973    /// yet is the only kind worth minting, and a caller building a spent one for a reuse test
974    /// assigns the field afterwards rather than passing a flag that is `Active` every real time.
975    ///
976    /// `family_id` is an argument and not a default, unlike almost everything else here, because
977    /// there is no honest default for it: an invented one would put this chain in a family of its
978    /// own and quietly cost RFC 9700 section 4.14.2 the access tokens minted alongside it, which is
979    /// the failure the field exists to prevent. `expires_at` starts `None`, a chain with no
980    /// absolute lifetime, which is what [`crate::server::ServerConfig`] produces when the host has
981    /// set no refresh TTL.
982    pub fn new(
983        refresh_token: impl Into<String>,
984        client_id: ClientId,
985        subject: Option<String>,
986        scope: ScopeSet,
987        family_id: impl Into<String>,
988    ) -> Self {
989        RefreshTokenRecord {
990            refresh_token: refresh_token.into(),
991            client_id,
992            subject,
993            scope,
994            resource: Vec::new(),
995            #[cfg(feature = "rar")]
996            authorization_details: crate::rar::AuthorizationDetails::none(),
997            // UNIX_EPOCH is the FAIL-CLOSED default, and it is deliberate. A record assembled by
998            // hand has not said when its grant was authorized, and the epoch predates every
999            // revocation, so a standing barrier refuses it. The other direction would have a
1000            // hand-built record silently outrank a revocation.
1001            grant_established_at: SystemTime::UNIX_EPOCH,
1002            expires_at: None,
1003            #[cfg(feature = "dpop")]
1004            jkt: None,
1005            #[cfg(feature = "mtls")]
1006            x5t_s256: None,
1007            family_id: family_id.into(),
1008            state: RefreshTokenState::Active,
1009            #[cfg(feature = "consent")]
1010            authentication: None,
1011        }
1012    }
1013}
1014
1015/// Hand-written so the opaque `refresh_token` never prints. EVERY other field prints, on the rule
1016/// [`IssuedToken`]'s `Debug` states in full: `state` and `family_id` are precisely what an operator
1017/// debugging an RFC 9700 section 4.14.2 family revocation needs to see, `grant_established_at` is
1018/// what a [`crate::store::RevocationBarrier`] is compared against, and none of the three is a
1019/// credential.
1020impl fmt::Debug for RefreshTokenRecord {
1021    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1022        let mut out = f.debug_struct("RefreshTokenRecord");
1023        out.field("refresh_token", &"[redacted]")
1024            .field("client_id", &self.client_id)
1025            .field("subject", &self.subject)
1026            .field("scope", &self.scope)
1027            .field("resource", &self.resource);
1028        #[cfg(feature = "rar")]
1029        out.field("authorization_details", &self.authorization_details);
1030        out.field("grant_established_at", &self.grant_established_at)
1031            .field("expires_at", &self.expires_at);
1032        #[cfg(feature = "dpop")]
1033        out.field("jkt", &self.jkt);
1034        #[cfg(feature = "mtls")]
1035        out.field("x5t_s256", &self.x5t_s256);
1036        out.field("family_id", &self.family_id)
1037            .field("state", &self.state);
1038        #[cfg(feature = "consent")]
1039        out.field("authentication", &self.authentication);
1040        out.finish()
1041    }
1042}
1043
1044#[cfg(test)]
1045#[path = "tests/token.rs"]
1046mod tests;