#[non_exhaustive]pub struct IssuedToken {Show 14 fields
pub access_token: String,
pub client_id: ClientId,
pub subject: Option<String>,
pub scope: ScopeSet,
pub resource: Vec<String>,
pub authorization_details: AuthorizationDetails,
pub issued_at: SystemTime,
pub grant_established_at: SystemTime,
pub expires_at: SystemTime,
pub jkt: Option<Box<str>>,
pub x5t_s256: Option<Box<CertificateThumbprint>>,
pub family_id: Option<String>,
pub act: Option<Box<ActClaim>>,
pub authentication: Option<Box<Authentication>>,
}Expand description
A persisted access token: what introspection needs to answer for an opaque token.
Debug is hand-written (see below) rather than derived: access_token is a bearer credential
(RFC 6750 section 1: possession of the string is the whole of the authorization), so a host
doing the obvious tracing::debug!(?record) must not thereby write a live token to its logs.
#[non_exhaustive]: rar, dpop, mtls and consent each add a field, so the shape of this
record is decided by the flag set the final binary is linked with rather than by the host that
writes against it.
A crate::store::Storage implementor does not lose anything: it is HANDED these records and
round-trips them through the derived Serialize/Deserialize, both of which are generated in
here and are unaffected. oauth-as-postgres persists them as a jsonb payload for exactly that
reason and never spells a field. For everyone else, including a host seeding a store or writing
a fixture, IssuedToken::new takes the fields a token cannot exist without and leaves the
rest public to assign.
THE BOUND ON THAT SENTENCE, stated because it is not obvious: “does not lose anything” holds
for ONE flag set. Two binaries built with different features over one store are a different
situation, and this type has no answer for it: a reader without rar deserializes a record
whose authorization_details it has no field for, the derived Deserialize drops the member
in silence, and a rotation writes the shortened record back. Serialization is not where that
gets caught – IntrospectionResponse and Confirmation are read from a FOREIGN server
and are guarded on the way in, whereas these records are this deployment’s own and the guard
would have to be a decision about what a store should do when it hands back a record this
binary cannot represent. A MIXED-FLAG FLEET OVER ONE STORE IS THEREFORE NOT A SUPPORTED
DEPLOYMENT of this crate, and the same caveat applies to RefreshTokenRecord,
crate::authorization::AuthorizationCodeRecord and
crate::par::PushedAuthorizationRequest.
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.access_token: StringThe opaque access token string (the storage key).
client_id: ClientIdThe client the token was issued to.
subject: Option<String>The resource owner the token acts for; None for client-only grants.
scope: ScopeSetThe granted scope.
resource: Vec<String>The RFC 8707 resource indicators this token is restricted to; empty when the grant named
none. This is what RFC 7662 introspection reports as aud, and what the RFC 9068 aud
claim carries when the jwt feature signs the wire token.
THE TWO CHANNELS PART COMPANY WHEN THIS IS EMPTY, which the sentence above used to deny.
Introspection then omits aud altogether (an empty array would read as “restricted to
nothing”; see IntrospectionResponse::aud), while the signed token cannot omit it,
because RFC 9068 section 2.2 makes the claim REQUIRED: it carries the deployment-wide
crate::jwt::JwtConfig::audience instead. So for a grant that named no resource a
caller reads “no restriction stated” from introspection and “restricted to the
configured audience” from the token, for one token. Both are true statements about
different things: this field is the GRANT’S narrowing, and the configured audience is the
deployment’s standing one. Non-empty, they agree exactly.
SINCE 0.9.2 THIS FIELD ALSO DECIDES WHO MAY ASK. It is what a registered resource server is
matched against, so a token whose grant named no resource is introspectable by its own
client alone; see crate::ServerConfig::resource_servers. A resource server that is
answered sees only its OWN identifiers here, not the whole set.
rar only.The RFC 9396 authorization details this token carries (section 7: the AS returns the
details as granted and assigned to the access token). This is what RFC 7662
introspection reports as authorization_details (section 9.2) and what the RFC 9068
claim carries when the jwt feature signs the wire token (section 9.1).
#[serde(default)], for the same reason grant_established_at below has one and reached
by the other door: TURNING THE FEATURE ON must not make what is already in the store
unreadable. A record written by a build without rar carries no such key, this field is not
an Option so serde’s derive supplies no default of its own, and the read fails outright
with missing field "authorization_details". That is not a migration an operator can plan
around either, because cargo feature unification means a dependency can turn rar on
without the host asking (see tests/host_api_shape.rs): the build changes and every live
grant stops being readable at once. The default is the empty set, which is the truth about a
grant minted before the feature existed, and it is also the safe direction: an empty set
authorizes nothing extra.
issued_at: SystemTimeIssuance instant.
grant_established_at: SystemTimeThe instant the GRANT behind this token was authorized, which is NOT issued_at.
For a code redemption it is when the code was minted; for a refresh rotation it is carried forward unchanged from the chain, so every token along a chain reports the one decision that started it. For a grant with no resource owner behind it (client credentials) it is the instant of issuance, because the client’s own registration is the only authorization there is.
FOR A DEVICE GRANT IT IS WHEN THE DEVICE ASKED, NOT WHEN THE USER APPROVED, and that is a
known approximation rather than the intent. RFC 8628 section 3.3 approval happens at the
host’s own verification UI and crate::device::DeviceGrantState::Approved records only
the subject, so the approval instant is never persisted and there is nothing truer to carry;
crate::device::DeviceGrant::created_at is what exists. The gap is the grant’s whole
lifetime (RFC 8628 section 3.2 expires_in, typically minutes), and it errs in the
FAIL-CLOSED direction: a barrier recorded inside that window refuses a decision the user
made after it, so a user who withdraws consent and then re-approves the same pending device
grant is refused and has to start the device flow again. A false refusal, never a false
admission. Closing it properly means persisting the approval instant, which is a breaking
change to that enum variant.
A crate::store::RevocationBarrier is compared against this rather than against
issued_at: a rotation and a re-approval both WRITE at now, so now cannot tell a grant
that predates a revocation from one made after it. See
crate::store::RevocationWindow::recorded_at.
#[serde(default)], and the default is the epoch, which is the FAIL-CLOSED direction.
This field is new in 0.9.1, so a record a 0.9.0 node wrote — or is still writing, during a
rolling upgrade — carries no such key, and without a default the read fails outright and
every token that release issued becomes unreadable the moment this one starts. With it, the
record deserializes and dates from before every barrier that could ever be recorded, so a
standing revocation REFUSES it rather than admitting it. A far-future default would
deserialize just as happily and ADMIT every record 0.9.0 wrote, which is exactly the
resurrection this field exists to close, reintroduced through the upgrade path. The
There is deliberately NO backfill migration: a backfill cannot reach a 0.9.0 node still
writing field-less payloads during a rolling upgrade, which is the window that matters, so
the serde default covers strictly more than one would.
expires_at: SystemTimeExpiry instant; the token is dead at and after this instant.
jkt: Option<Box<str>>dpop only.RFC 9449 section 6: the RFC 7638 thumbprint of the DPoP key this token is bound to, or
None for an ordinary bearer token.
Option<Box<str>> rather than Option<String>, and feature gated, because this record is
written and read on every token-plane request and tests/allocation.rs holds it to a size
budget: the box is 16 bytes against a String’s 24, and a deployment without the dpop
feature pays neither. The value is a fixed 43-character base64url digest that is never
appended to, so the growable capacity a String carries would be dead weight.
x5t_s256: Option<Box<CertificateThumbprint>>mtls only.RFC 8705 section 3: the SHA-256 thumbprint of the client certificate this token is
bound to, or None for a token that is not certificate bound.
Recorded on the AS side, and not only inside a signed JWT, for the same reason jkt
next door is: this crate’s default access token is OPAQUE, and RFC 8705 section 3.2
has a resource server learn the binding by INTROSPECTING, which it can only be told
if it was persisted. The channel that carries it to a resource server arrived in 0.9.2;
the field was persisted before that, because the RECORD, not the response, is the thing
that cannot be added later.
Option<Box<_>> rather than the 32-byte thumbprint inline, on the same measurement
as jkt: this record is written and read on every token-plane request and
tests/allocation.rs holds it to a size budget, so an unbound token pays one null
pointer and the allocation happens only for a token that is actually bound.
family_id: Option<String>The authorization grant this token belongs to (see RefreshTokenRecord::family_id).
RFC 9700 section 4.14.2 requires that detecting refresh token reuse revokes “the tokens
issued for that authorization grant”, not merely the refresh chain, so an access token has
to be reachable from the grant it came from. None for a grant that produced no refresh
chain (RFC 6749 section 4.4 client credentials), where there is no chain to be reused and
so nothing to revoke by family.
act: Option<Box<ActClaim>>token-exchange only.RFC 8693 section 4.1 act: who authority was delegated TO, for a token issued by a
DELEGATION token exchange. None for every other grant, and for an impersonation exchange,
which by definition names no actor.
§Why this is on the RECORD and not only in the response
This crate’s default access token is OPAQUE, so RFC 7662 introspection is the only channel a resource server has for learning anything about it. A delegation that introspection cannot see is a delegation the resource server has to take the host’s word for, which is the one thing section 1.1 delegation exists to avoid: the whole point is that the resource can tell “A acting for B” from “B”.
The channel that carries it to a resource server arrived in 0.9.2; the field was persisted before that, because the RECORD, not the response, is the thing that cannot be added later.
It was left off through 0.9.0 for two reasons, and both are now spent. The first was
allocation, and crate::store::Storage::get_token returning an Arc<IssuedToken> ended
it: the record’s shape costs a read nothing, and this field costs a deployment without the
feature zero bytes and one with it 8 bytes per token plus one allocation per DELEGATED
token. The second was the persistence contract, which was the real one: this is the record
every host’s Storage writes, so a new field is a migration in stores this crate does not
own. That is exactly why it lands HERE, in the release that is already breaking that trait,
so a host migrates once rather than twice.
BOXED for the same measured reason as authentication below: the common case is None,
and this record is written and read on every token-plane request.
authentication: Option<Box<Authentication>>consent only.What the host reported about the resource owner’s authentication when this token’s grant was
approved, or None when it reported nothing.
BOXED, so the common None costs one null pointer on a record that is written and read on
every token-plane request rather than the whole struct; tests/allocation.rs holds this
type to a size budget precisely so that a convenience like an inline SystemTime plus an
Option<String> cannot be paid for silently. It is what BOTH halves of RFC 9470 section 6
are answered from: 6.2 at introspection time, and 6.1 at issuance, where the same report
becomes the auth_time and acr claims of the signed access token.
Implementations§
Source§impl IssuedToken
impl IssuedToken
Sourcepub fn new(
access_token: impl Into<String>,
client_id: ClientId,
subject: Option<String>,
scope: ScopeSet,
issued_at: SystemTime,
expires_at: SystemTime,
) -> Self
pub fn new( access_token: impl Into<String>, client_id: ClientId, subject: Option<String>, scope: ScopeSet, issued_at: SystemTime, expires_at: SystemTime, ) -> Self
The five things a persisted access token cannot be without: the string a client will present, who it was issued to, who it acts for, what it may do, and when it lives between.
Everything else describes a token that is more than the minimum (an audience restriction, a
sender-constraining binding, the family it can be revoked with) and is a public field on the
returned value, so a caller sets what applies and states nothing about what does not. That
split is the reason the arguments stop here rather than growing one per feature: a caller
building a record for a build with dpop off should not have to mention DPoP.
subject is an argument rather than a field to assign because None is a real answer and
not an omission: it means an RFC 6749 section 4.4 client-credentials token, which acts for
no resource owner, and a caller should have to say so.
Trait Implementations§
Source§impl Clone for IssuedToken
impl Clone for IssuedToken
Source§fn clone(&self) -> IssuedToken
fn clone(&self) -> IssuedToken
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 IssuedToken
Hand-written so the opaque access_token never prints. EVERY other field prints, because every
other field is metadata ABOUT the token rather than the credential itself, and the record has to
stay debuggable: family_id in particular is what makes an RFC 9700 section 4.14.2 family
revocation traceable, and it is an internal grouping identifier, not a bearer credential.
impl Debug for IssuedToken
Hand-written so the opaque access_token never prints. EVERY other field prints, because every
other field is metadata ABOUT the token rather than the credential itself, and the record has to
stay debuggable: family_id in particular is what makes an RFC 9700 section 4.14.2 family
revocation traceable, and it is an internal grouping identifier, not a bearer credential.
“Every other field” is the whole rule and it is stated that way deliberately. This impl used to
print eight of thirteen, and the five it dropped were the five added since it was written, which
is what a hand-written Debug costs if nobody restates the rule when a field arrives. The
worst omission was grant_established_at: it is the SOLE time input to every
crate::store::RevocationBarrier comparison and its fail-closed default is the epoch, so
“this token is refused by a barrier and I cannot see why” was exactly the question {:?} could
not answer. jkt and x5t_s256 are public-key and certificate THUMBPRINTS, which a resource
server is given on the wire in the RFC 7800 cnf claim, so neither is secret; act is the RFC
8693 section 4.1 delegation chain, which introspection publishes; authentication is the RFC
9470 report, which introspection publishes as auth_time and acr (section 6.2) and which a
signed access token carries under the same two names (section 6.1).