Skip to main content

AuthorizedToken

Struct AuthorizedToken 

Source
#[non_exhaustive]
pub struct AuthorizedToken { pub subject: Option<String>, pub principal: Option<String>, pub scopes: Vec<String>, pub issuer: String, pub audiences: Vec<String>, pub expires_at: SystemTime, pub issued_at: Option<SystemTime>, pub client_id: Option<String>, pub jti: Option<String>, /* private fields */ }
Expand description

A successfully validated access token. The axum middleware (feature axum) inserts it into request extensions, so handlers can read who called and with which scopes.

The scopes come from the one place that actually verified them, so a handler enforcing a finer-grained scope (say, a write scope on some routes) should ask AuthorizedToken::has_scope rather than re-parse the header.

§The verified claims

Besides the fields below, the token keeps the whole claim set the signature covered, read with AuthorizedToken::claims (raw) or AuthorizedToken::claims_as (into your own type), for anything this crate has no field for: groups, roles, email, a tenant id. There is no need to decode the JWT a second time in a handler. The claims are stored once and shared (Arc), so cloning a token is cheap. Their size is bounded by the credential cap: a token is refused above 16 KiB before it is decoded, so the stored claims cannot outgrow that (their parsed in-memory form is a small multiple of it).

§Debug

Debug prints every field except the claim values: the claims appear as their names only. Claims routinely carry personal data (email, name, group memberships) and a {token:?} in a log line or a panic message must not leak it. subject, principal and client_id are printed, as they always have been (subject, principal) or are identifiers meant for logs. Read values deliberately through AuthorizedToken::claims.

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.
§subject: Option<String>

The token’s sub, verbatim, when it carried one as a string. It is signed, so it is safe to key per-user decisions on (it is bounded only by the 16 KiB credential cap); this crate’s own log lines truncate it.

§principal: Option<String>

The first present, non-empty string claim of crate::OAuthConfig::principal_claims, verbatim — who the request is from, for logs and attribution. Never the token itself. Which claim it came from depends on config, so key authorization on AuthorizedToken::subject rather than on this.

§scopes: Vec<String>

The union of every crate::OAuthConfig::scope_claims claim, in first-seen order, deduplicated.

§issuer: String

The token’s iss: the exact string that matched the configured issuer. Empty on a token built with AuthorizedToken::new.

§audiences: Vec<String>

The token’s aud, normalized to a list whether the token carried a single string or an array (non-string entries are dropped). On a validated token at least one entry is a configured audience. Empty on a token built with AuthorizedToken::new.

§expires_at: SystemTime

The token’s exp. A validated token was not expired at the moment of validation (within the configured leeway), so a long-lived connection, such as an SSE stream, can close itself at this instant. A fractional exp is rounded to whole seconds exactly as the validation rounded it, and one later than 9999-12-31T23:59:59Z saturates to that instant. A token built with AuthorizedToken::new gets 2100-01-01T00:00:00Z.

§issued_at: Option<SystemTime>

The token’s iat, when it carried a valid NumericDate. Checked only when crate::OAuthConfig::max_token_age_secs is set. Rounded and saturated like expires_at; None when iat is absent or not a non-negative number (the token is still accepted then). None on a token built with AuthorizedToken::new.

§client_id: Option<String>

The OAuth client the token was issued to: client_id (RFC 9068 §2.2), else azp, the first that is a non-empty string. The crate::OAuthConfig::allowed_client_ids check reads the same claims more strictly: there, a client_id that is present but empty or not a string refuses the token instead of falling through to azp. None when the token carries neither, and on a token built with AuthorizedToken::new.

§jti: Option<String>

The token’s jti, when it carried one as a non-empty string. None on a token built with AuthorizedToken::new.

Implementations§

Source§

impl AuthorizedToken

Source

pub fn new( subject: Option<String>, principal: Option<String>, scopes: impl IntoIterator<Item = impl Into<String>>, ) -> Self

Build a token record from parts, with scopes deduplicated in first-seen order (blank entries dropped), as a validation produces them.

This verifies nothing — it is a plain value constructor for code that needs an AuthorizedToken without a validation, chiefly tests that place one in request extensions. crate::OAuthValidator::validate is the only source of a token that was actually checked. The struct is #[non_exhaustive], so a field added later gets a default here rather than breaking callers.

The fields beyond the three arguments get test-friendly defaults: empty issuer, audiences and claims, None for issued_at, client_id and jti, and an expires_at of 2100-01-01T00:00:00Z. The far-future expiry is deliberate: a handler that closes a stream when the token expires must not see a fixture as already expired. Set any of them with the with_* builders.

Source

pub fn claims(&self) -> &Map<String, Value>

The verified claim set, exactly as the token carried it: every claim the signature covered, including the ones with their own field here. Empty for a token built with AuthorizedToken::new unless with_claims was used.

§Security

Claims are trustworthy only on a token a validation produced; one from AuthorizedToken::new holds whatever the caller put there. Claim values can be personal data, so Debug does not print them.

§Examples
use oauth_resource_server::AuthorizedToken;
use serde_json::json;

let token = AuthorizedToken::new(Some("user-1".into()), None, ["api:read"])
    .with_claims(json!({"groups": ["admins", "dev"]}).as_object().unwrap().clone());
let in_admins = token.claims()["groups"]
    .as_array()
    .is_some_and(|g| g.iter().any(|v| v == "admins"));
assert!(in_admins);
Source

pub fn claims_as<T: DeserializeOwned>(&self) -> Result<T, Error>

The verified claim set deserialized into your own type, for a typed view of the claims your application cares about. Unknown claims are ignored unless your type says otherwise (deny_unknown_fields). The claim map is cloned to deserialize it, so call this once per request, not per field.

§Errors

serde_json::Error when the claims do not fit T, for example a required field the token did not carry or one of the wrong type.

§Examples
use oauth_resource_server::AuthorizedToken;
use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct Claims {
    email: String,
    #[serde(default)]
    groups: Vec<String>,
}

let token = AuthorizedToken::new(None, None, ["api:read"]).with_claims(
    json!({"email": "ada@example.com", "groups": ["admins"]})
        .as_object()
        .unwrap()
        .clone(),
);
let claims: Claims = token.claims_as().unwrap();
assert_eq!(claims.email, "ada@example.com");
assert_eq!(claims.groups, ["admins"]);
Source

pub fn with_claims(self, claims: Map<String, Value>) -> Self

Replace the claim set, for building a token in a test. Only the claim set changes: the typed fields (issuer, client_id, …) are not re-derived from it, so set them with their own with_* builders. Verifies nothing.

Source

pub fn with_issuer(self, issuer: impl Into<String>) -> Self

Set issuer, for building a token in a test.

Source

pub fn with_audiences( self, audiences: impl IntoIterator<Item = impl Into<String>>, ) -> Self

Set audiences, for building a token in a test.

Source

pub fn with_expires_at(self, expires_at: SystemTime) -> Self

Set expires_at, for building a token in a test.

Source

pub fn with_issued_at(self, issued_at: SystemTime) -> Self

Set issued_at, for building a token in a test.

Source

pub fn with_client_id(self, client_id: impl Into<String>) -> Self

Set client_id, for building a token in a test.

Source

pub fn with_jti(self, jti: impl Into<String>) -> Self

Set jti, for building a token in a test.

Source

pub fn has_scope(&self, scope: &str) -> bool

Whether the token carries scope (exact, case-sensitive match, RFC 6749 §3.3). The single place that answers the question, so callers never hand-roll a .iter().any() over scopes.

§Examples
use oauth_resource_server::AuthorizedToken;

let token = AuthorizedToken::new(Some("user-1".into()), None, ["api:read", "api:write"]);
assert!(token.has_scope("api:write"));
assert!(!token.has_scope("API:WRITE"));
Source

pub fn require_scopes(&self, required: &[&str]) -> Result<(), MissingScopes>

Whether the token carries EVERY scope in required (all-of): Ok(()), or the MissingScopes naming what it lacks. An empty required passes every token.

The same matching the validator’s own required_scopes check runs, on the same scopes (every configured scope claim, a string split on whitespace, an array taken element by element), so a route asking for a scope here and a validator requiring it agree exactly: an exact, case-sensitive comparison per scope (RFC 6749 §3.3). An entry that is blank or contains whitespace can never be carried, so it is always missing.

This is the per-route or per-operation check on top of the validator’s floor; the layers’ require_scopes, the axum feature’s RequireScopes and Scoped extractor, and the mcp feature all run it. To answer a refusal with a 403 whose challenge names these scopes, see crate::refusal_for_scopes; ? converts the error into TokenRejection::InsufficientScope.

§Errors

MissingScopes when at least one entry of required is not among the token’s scopes.

§Examples
use oauth_resource_server::AuthorizedToken;

let token = AuthorizedToken::new(None, None, ["docs:read"]);
assert!(token.require_scopes(&["docs:read"]).is_ok());

let missing = token.require_scopes(&["docs:read", "docs:write"]).unwrap_err();
assert_eq!(missing.required(), ["docs:read", "docs:write"]);
assert_eq!(missing.missing(), ["docs:write"]);

Trait Implementations§

Source§

impl Clone for AuthorizedToken

Source§

fn clone(&self) -> Self

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 AuthorizedToken

Source§

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

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

impl Eq for AuthorizedToken

Source§

impl<S: Send + Sync> FromRequestParts<S> for AuthorizedToken

Available on crate feature axum only.

Extracts the OAuth token an AuthLayer accepted.

Refuses with the layer’s own 401 and WWW-Authenticate challenge when no token is in the extensions — an optional or allow_unauthenticated layer passed the request through, or the static token was accepted — and with 500 (logged at error) on a route no AuthLayer covers. Behind nested strict layers the token may have been inserted by an OUTER layer even when the innermost one accepted the static token; see the module docs.

§Examples

use axum::{Router, routing::get};
use oauth_resource_server::AuthorizedToken;

async fn subject(token: AuthorizedToken) -> String {
    token.subject.unwrap_or_default()
}
Source§

type Rejection = Response<Body>

If the extractor fails it’ll use this “rejection” type. A rejection is a kind of error that can be converted into a response.
Source§

async fn from_request_parts( parts: &mut Parts, _state: &S, ) -> Result<Self, Response>

Perform the extraction.
Source§

impl<S: Send + Sync> OptionalFromRequestParts<S> for AuthorizedToken

Available on crate feature axum only.

Option<AuthorizedToken>: None when an AuthLayer ran and no OAuth token is in the extensions (no credential under an optional or allow_unauthenticated layer, or the static token was accepted and no outer strict layer inserted a token — see nested layers). On a route no AuthLayer covers it still refuses with 500, logged at error, rather than reading as anonymous.

Source§

type Rejection = Response<Body>

If the extractor fails, it will use this “rejection” type. Read more
Source§

async fn from_request_parts( parts: &mut Parts, _state: &S, ) -> Result<Option<Self>, Response>

Perform the extraction.
Source§

impl PartialEq for AuthorizedToken

Source§

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

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

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

Inequality operator !=. Read more
Source§

impl StructuralPartialEq for AuthorizedToken

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> 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<S, T> FromRequest<S, ViaParts> for T
where S: Send + Sync, T: FromRequestParts<S>,

Source§

type Rejection = <T as FromRequestParts<S>>::Rejection

If the extractor fails it’ll use this “rejection” type. A rejection is a kind of error that can be converted into a response.
Source§

fn from_request( req: Request<Body>, state: &S, ) -> impl Future<Output = Result<T, <T as FromRequest<S, ViaParts>>::Rejection>>

Perform the extraction.
Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
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> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
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 = !

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

fn try_from(value: U) -> Result<T, !>

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.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more