pub struct TokenPolicy<'a> { /* private fields */ }Expand description
What the request is allowed to be, given the resource it reached and the revision it negotiated.
Two deployment facts and one spec fact, all of which change whether a token is checked at all. Bundled because passing three arguments to every call site invites two of them being forgotten.
Implementations§
Source§impl<'a> TokenPolicy<'a>
impl<'a> TokenPolicy<'a>
Sourcepub const fn new(resource: &'a ResourceUri, model: AuthorizationModel) -> Self
pub const fn new(resource: &'a ResourceUri, model: AuthorizationModel) -> Self
A policy that accepts any issuer.
§Examples
use protocol_droid::domain::{ProtocolVersion, authorization_model};
use protocol_droid::domain::mcp::ResourceUri;
use protocol_droid::application::oauth::TokenPolicy;
let resource = ResourceUri::new("https://example.com/mcp")?;
let policy = TokenPolicy::new(&resource, authorization_model(ProtocolVersion::V2026_07_28));
assert!(policy.accepts_any_issuer());Sourcepub fn with_expected_issuer(self, issuer: impl Into<Arc<str>>) -> Self
pub fn with_expected_issuer(self, issuer: impl Into<Arc<str>>) -> Self
Requires tokens to name issuer, refusing any minted by anyone else.
Sourcepub fn with_expected_audience(self, audience: impl Into<Audience>) -> Self
pub fn with_expected_audience(self, audience: impl Into<Audience>) -> Self
Accepts tokens bound to audience as well as to the resource.
The single-audience form of Self::with_expected_audiences; see that
method for what this is for and why the resource stays accepted.
§Examples
use protocol_droid::application::oauth::TokenPolicy;
use protocol_droid::domain::mcp::ResourceUri;
use protocol_droid::domain::{ProtocolVersion, authorization_model};
let resource = ResourceUri::new("https://example.com/mcp")?;
// A Microsoft Entra v2.0 token carries the web API's client ID in `aud`.
let policy = TokenPolicy::new(&resource, authorization_model(ProtocolVersion::V2026_07_28))
.with_expected_audience("8f7c1e42-0b3a-4f5d-9c6e-1a2b3c4d5e6f");
assert_eq!(policy.expected_audiences().map(<[Audience]>::len), Some(1));Sourcepub fn with_expected_audiences<I, T>(self, audiences: I) -> Self
pub fn with_expected_audiences<I, T>(self, audiences: I) -> Self
Accepts tokens bound to any of audiences, as well as to the resource.
§Why this exists
The resource identifier and the aud claim are frequently two different
strings, and binding them together refuses valid tokens. RFC 9728 §3.1
derives the metadata URL from the resource identifier, so the resource
must be the HTTPS URL this server answers on — but that URL need not be
what the authorization server writes into aud.
Microsoft Entra is the case that forced it.
Validate the audience
states that for v2.0 tokens the audience is the client ID of the
web API — a bare GUID — while for v1.0 tokens it is one of the app ID
URIs, such as api://{ApplicationID}. So three identifiers are in play,
and conflating any two of them breaks:
| identifier | example | role |
|---|---|---|
| Application ID URI | api://<guid> | what a client asks for as scope |
| client ID | <guid> | aud in a v2.0 token |
| RFC 9728 resource | https://host/mcp | derives the metadata path; RFC 8707 indicator |
A deployment fronted by Entra therefore configures the GUID here and keeps the HTTPS URL as the resource, and both verify.
§What this does not change
Comparison is still exact equality against each canonicalised
ResourceUri, never “contains” and never a host or path prefix, so a
sibling server’s token still fails. The resource identifier stays
accepted, so adding audiences can only widen what this deployment already
trusted — it never withdraws the RFC 8707 binding. Accepting an audience
is a deliberate act by whoever holds the configuration; the default is
None, which leaves behaviour exactly as it was.
A repeated call replaces the set rather than extending it, so the configured audiences are always the ones the last call named.
§Examples
use protocol_droid::application::oauth::TokenPolicy;
use protocol_droid::domain::mcp::ResourceUri;
use protocol_droid::domain::{ProtocolVersion, authorization_model};
let resource = ResourceUri::new("https://mcp.example.com/mcp")?;
let policy = TokenPolicy::new(&resource, authorization_model(ProtocolVersion::V2026_07_28))
.with_expected_audiences([
// A v2.0 client ID, and a v1.0 app ID URI.
"8f7c1e42-0b3a-4f5d-9c6e-1a2b3c4d5e6f",
"api://8f7c1e42-0b3a-4f5d-9c6e-1a2b3c4d5e6f",
]);
assert_eq!(policy.expected_audiences().map(<[Audience]>::len), Some(2));Sourcepub fn expected_audiences(&self) -> Option<&[Audience]>
pub fn expected_audiences(&self) -> Option<&[Audience]>
The audiences accepted in addition to the resource, if any were set.
Sourcepub const fn accepts_any_issuer(&self) -> bool
pub const fn accepts_any_issuer(&self) -> bool
Whether an issuer check will be made.