Verifying tokens
verify_token() validates an OIDC access/ID token locally: it checks the ES256 signature against
the server’s JWKS, then validates the registered claims. This guide is the practical how-to; the theory
is in JWT / JWKS verification.
Motivation
A consumer service receives a JWT and must decide whether to trust it — without a network round-trip per
request. The SDK verifies the signature and claims against the server’s published public keys, so
trust flows from the IAM server’s signing key, not from the token’s self-asserted contents.
Configure issuer and audience
Issuer and audience are mandatory for verification. They are set on the builder:
use laravel_iam::IamClient;
let iam = IamClient::builder()
.base_url("https://iam.example.com/api/iam/v1")
.issuer("https://iam.example.com") // expected `iss`
.audience("warehouse-api") // expected `aud`
.build()?;
If you call verify_token() without configuring both an issuer and an audience, the result is
IamError::Config — never an accepted token. A token the client cannot fully
validate must never be trusted. This is enforced in wire::verify_jwt.
Verify
match iam.verify_token(jwt).await {
Ok(claims) => {
// trusted
println!("subject = {}", claims.sub);
}
Err(_) => {
// reject — bad signature, expired, wrong aud/iss, unknown key, malformed, …
}
}
verify_token() returns Result<Claims, IamError>. On success you get verified
Claims: sub, iss, aud, exp, optional nbf/iat, and any extra claims
flattened into claims.extra.
What is checked, in order
Extract
kidfrom the JWT header (no verification yet).Resolve the key. The cached JWKS is consulted; on a cache miss (or unknown
kid) the SDK fetches
{base}/.well-known/jwks.jsononce and re-caches it. This handles key rotation transparently.Pin the algorithm. The header
algmust be exactlyES256; anything else is rejected. This
blocksalgconfusion /noneattacks.Verify the signature over
header.payloadwith pure-Rustp256before any claim is trusted.Validate claims with no leeway:
issmust match,audmust match (string or array per
RFC 7519),expmust be in the future, andnbf(if present) must be in the past.
A token is accepted only when every step passes. Any failure is
IamError::TokenInvalid.
Worked example: an auth middleware shape
use laravel_iam::IamClient;
async fn authenticate(iam: &IamClient, bearer: &str) -> Result<String, ()> {
// `bearer` is the raw JWT (strip "Bearer " yourself upstream).
match iam.verify_token(bearer).await {
Ok(claims) => Ok(claims.sub), // authenticated principal
Err(_) => Err(()), // 401 — reject
}
}
The verified claims.sub is then a good Subject::user(..) id for a follow-up
check().
JWKS caching
The JWKS is cached in-process behind an RwLock (async: tokio::sync::RwLock; blocking:
std::sync::RwLock). The cache is populated on first use and refreshed automatically when a token
presents a kid the cache does not contain. There is no TTL: rotation is detected by kid, not by
clock.
Because the cache is per-client-instance, share one IamClient (it is Clone and cheap to clone —
it wraps Arcs) across your handlers rather than building a new one per request, so the JWKS is fetched
once.
Gotchas
- Both
issuerandaudienceare required — omitting either yieldsIamError::Config, not a pass. - No clock leeway. A token that expired one second ago is rejected. Keep server and client clocks in
sync (NTP). - Only ES256. RS256/HS256 tokens are rejected by design; the IAM server signs with EC P-256.
- Don’t trust unverified claims. Never read claims out of a raw JWT yourself; only the
Ok(Claims)
fromverify_token()is trustworthy.
See also: JWT / JWKS verification, Security.