pub fn verify(
claims: &TokenClaims,
policy: &TokenPolicy<'_>,
scopes: &dyn ScopePolicy,
now: SystemTime,
) -> Result<AuthContext, Refusal>Expand description
Turns claims an adapter authenticated into an AuthContext, or refuses.
§Errors
The Refusal naming the first rule claims fails. Order is deliberate:
time is checked before audience so a stale token does not report a mismatch,
and audience before issuer because the audience is the binding that keeps a
token from crossing between servers, which is the one this crate must not get
wrong.
§Examples
use std::time::{Duration, SystemTime};
use protocol_droid::application::oauth::{ScopePolicy, TokenClaims, TokenPolicy, verify};
use protocol_droid::domain::mcp::ResourceUri;
use protocol_droid::domain::registry::{Scope, ScopeSet};
use protocol_droid::domain::{ProtocolVersion, authorization_model};
struct Permissive;
impl ScopePolicy for Permissive {
fn resolve(&self, scope: &str) -> Option<Scope> {
match scope {
"mcp:read" => Some(Scope::Read),
_ => None,
}
}
}
let resource = ResourceUri::new("https://example.com/mcp")?;
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000);
let policy = TokenPolicy::new(&resource, authorization_model(ProtocolVersion::V2026_07_28));
let good = TokenClaims::new()
.with_subject("alice")
.with_audience(resource.clone())
.with_expiry(now + Duration::from_secs(60))
.with_scope("mcp:read");
let context = verify(&good, &policy, &Permissive, now)?;
assert_eq!(context.principal(), "alice");
assert!(context.holds(ScopeSet::empty().with(Scope::Read)));
// A token minted for a different server is refused.
let elsewhere = TokenClaims::new()
.with_subject("alice")
.with_audience(ResourceUri::new("https://evil.example/mcp")?)
.with_expiry(now + Duration::from_secs(60));
assert!(verify(&elsewhere, &policy, &Permissive, now).is_err());