Skip to main content

IssuedToken

Struct IssuedToken 

Source
#[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
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§access_token: String

The opaque access token string (the storage key).

§client_id: ClientId

The client the token was issued to.

§subject: Option<String>

The resource owner the token acts for; None for client-only grants.

§scope: ScopeSet

The 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.

§authorization_details: AuthorizationDetails
Available on crate feature 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: SystemTime

Issuance instant.

§grant_established_at: SystemTime

The 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: SystemTime

Expiry instant; the token is dead at and after this instant.

§jkt: Option<Box<str>>
Available on crate feature 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>>
Available on crate feature 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>>
Available on crate feature 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>>
Available on crate feature 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

Source

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

Source§

fn clone(&self) -> IssuedToken

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

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).

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for IssuedToken

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Eq for IssuedToken

Source§

impl PartialEq for IssuedToken

Source§

fn eq(&self, other: &IssuedToken) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for IssuedToken

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for IssuedToken

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.