#[non_exhaustive]pub struct RefreshTokenRecord {Show 13 fields
pub refresh_token: String,
pub client_id: ClientId,
pub subject: Option<String>,
pub scope: ScopeSet,
pub resource: Vec<String>,
pub authorization_details: AuthorizationDetails,
pub grant_established_at: SystemTime,
pub expires_at: Option<SystemTime>,
pub jkt: Option<Box<str>>,
pub x5t_s256: Option<Box<CertificateThumbprint>>,
pub family_id: String,
pub state: RefreshTokenState,
pub authentication: Option<Box<Authentication>>,
}Expand description
A persisted refresh token. Single use: redemption goes through
crate::store::Storage::take_refresh_token, and rotation issues a replacement carrying the
SAME expires_at, so a chain has an absolute lifetime rather than a sliding one.
Debug is hand-written (see below) rather than derived: refresh_token is a bearer credential
whose leak is exactly the compromise RFC 9700 section 4.14.2 defends against, so it must not
reach a host’s logs through {:?}.
#[non_exhaustive], on the same four features and the same storage argument as
IssuedToken: a Storage implementor round-trips this through serde and never names a field,
and anyone assembling one by hand goes through RefreshTokenRecord::new.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.refresh_token: StringThe opaque refresh token string (the storage key).
client_id: ClientIdThe client the token was issued to; presentation by any other client is invalid_grant
and leaves the record untouched.
subject: Option<String>The resource owner the chain acts for.
scope: ScopeSetThe scope originally granted; refreshes may narrow, never widen.
resource: Vec<String>The RFC 8707 resource indicators originally granted. Carried across rotation for the same
reason scope is: section 2 lets a token request narrow the set and never widen it, so the
chain has to remember what it started with. Empty when the grant named none.
rar only.The RFC 9396 authorization details originally granted. Carried across rotation for
the same reason scope and resource are: section 6 lets a token request narrow the
set and never widen it, so the chain has to remember what it started with, and a
rotation that narrowed must not be climbable back on the next one.
#[serde(default)], which IssuedToken::authorization_details states in full: a chain
written by a build without rar carries no such key, this is not an Option so serde
supplies no default of its own, and without one every live chain becomes unreadable the
moment anything in the host’s dependency graph turns the feature on.
grant_established_at: SystemTimeThe instant the GRANT behind this chain was authorized, CARRIED ACROSS ROTATION and never
restamped, for the same reason scope and resource are carried: the chain has to remember
the one decision that started it. Restamping it on each rotation would let a chain walk
forward past a revocation it was supposed to die to.
See IssuedToken::grant_established_at, which this is copied into on every rotation, and
which states in full why the serde default below is the epoch: a chain a 0.9.0 node wrote
carries no such key, and the epoch is the reading that a standing revocation REFUSES rather
than the one it admits.
expires_at: Option<SystemTime>Absolute chain expiry; None means the chain does not expire by time.
On a Spent record this doubles as the RETENTION deadline: a spent token is kept only so
that its reuse can be recognised, and a chain with no absolute expiry would otherwise keep
every superseded link forever. The server therefore stamps a spent record from a
never-expiring chain with now + ServerConfig::refresh_reuse_window, which is what makes
crate::store::Storage::sweep_expired able to reclaim it.
jkt: Option<Box<str>>dpop only.RFC 9449 section 5: the RFC 7638 thumbprint of the DPoP key this refresh chain is bound
to, or None for an unbound chain.
Carried across rotation and CHECKED on redemption. Without it the binding would be decorative for anything but the first access token: a stolen refresh token could simply be re-bound to the thief’s key on the next rotation, leaving the attacker holding a token they can prove possession for and the victim’s key the one that gets refused.
x5t_s256: Option<Box<CertificateThumbprint>>mtls only.RFC 8705 section 3: the client certificate this refresh chain is bound to, or None
for an unbound chain.
Carried across rotation and CHECKED on redemption, exactly as jkt is and for the
same argument: without it the binding would be decorative past the first access
token, because a stolen refresh token could simply be re-bound to whatever
certificate the thief holds on the next rotation. Section 3 makes this a MUST for
public clients specifically; this crate applies it to every chain that was issued
over a certificate, because a chain whose holder proved possession of a key once
should have to keep proving it, and for a confidential mutual-TLS client the rule
costs nothing (it presents that certificate on every request anyway).
family_id: StringThe FAMILY this token belongs to: one identifier shared by every token, access or refresh, minted from the same authorization grant, and carried across rotation unchanged.
This is what makes RFC 9700 section 4.14.2 implementable at all. Without it the AS can refuse a reused token but cannot reach the tokens the thief already rotated into, which is the defence exactly inverted: the victim is locked out and the attacker is not.
state: RefreshTokenStateWhether this link is still redeemable, or is a retained rotated one.
authentication: Option<Box<Authentication>>consent only.The authentication the host reported when the grant this chain came from was approved, carried across rotation UNCHANGED.
Carried rather than restamped because a rotation is not a new authentication: the user is
not present, nothing has been proven again, and giving a refreshed token a fresh auth_time
would let any client defeat an RFC 9470 max_age by refreshing. See
IssuedToken::authentication for why it is boxed.
Implementations§
Source§impl RefreshTokenRecord
impl RefreshTokenRecord
Sourcepub fn new(
refresh_token: impl Into<String>,
client_id: ClientId,
subject: Option<String>,
scope: ScopeSet,
family_id: impl Into<String>,
) -> Self
pub fn new( refresh_token: impl Into<String>, client_id: ClientId, subject: Option<String>, scope: ScopeSet, family_id: impl Into<String>, ) -> Self
A LIVE link: state is RefreshTokenState::Active, because a record nobody has rotated
yet is the only kind worth minting, and a caller building a spent one for a reuse test
assigns the field afterwards rather than passing a flag that is Active every real time.
family_id is an argument and not a default, unlike almost everything else here, because
there is no honest default for it: an invented one would put this chain in a family of its
own and quietly cost RFC 9700 section 4.14.2 the access tokens minted alongside it, which is
the failure the field exists to prevent. expires_at starts None, a chain with no
absolute lifetime, which is what crate::server::ServerConfig produces when the host has
set no refresh TTL.
Trait Implementations§
Source§impl Clone for RefreshTokenRecord
impl Clone for RefreshTokenRecord
Source§fn clone(&self) -> RefreshTokenRecord
fn clone(&self) -> RefreshTokenRecord
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for RefreshTokenRecord
Hand-written so the opaque refresh_token never prints. EVERY other field prints, on the rule
IssuedToken’s Debug states in full: state and family_id are precisely what an operator
debugging an RFC 9700 section 4.14.2 family revocation needs to see, grant_established_at is
what a crate::store::RevocationBarrier is compared against, and none of the three is a
credential.
impl Debug for RefreshTokenRecord
Hand-written so the opaque refresh_token never prints. EVERY other field prints, on the rule
IssuedToken’s Debug states in full: state and family_id are precisely what an operator
debugging an RFC 9700 section 4.14.2 family revocation needs to see, grant_established_at is
what a crate::store::RevocationBarrier is compared against, and none of the three is a
credential.