Skip to content

Commit 1f714bf

Browse files
SVilgelmhashemix
andauthored
fix(auth): validate token audience by default (#149)
Built-in providers now default the audience to the MCP server's resource identifier, with an explicit opt-out, instead of disabling audience validation. Assisted-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Sergey Vilgelm <sergey@vilgelm.com> Co-authored-by: Ali Hashemi <14126952+hashemix@users.noreply.github.com>
1 parent 3e22cfe commit 1f714bf

8 files changed

Lines changed: 74 additions & 9 deletions

File tree

crates/rust-mcp-extra/examples/keycloak-auth.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ async fn main() -> SdkResult<()> {
2525
client_secret: env::var("CLIENT_SECRET").ok(),
2626
token_verifier: None,
2727
resource_documentation: None,
28+
validate_audience: None,
29+
disable_audience_validation: false,
2830
})?;
2931

3032
let server = create_axum_server(

crates/rust-mcp-extra/examples/scalekit-auth.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ async fn main() -> SdkResult<()> {
2121
token_verifier: None,
2222
resource_name: Some("Scalekit Oauth Test MCP Server".to_string()),
2323
resource_documentation: None,
24+
validate_audience: None,
25+
disable_audience_validation: false,
2426
environment_url: env::var("ENVIRONMENT_URL")
2527
.expect("Please set 'ENVIRONMENT_URL' evnrionment variable and try again."),
2628
resource_id: env::var("RESOURCE_ID")

crates/rust-mcp-extra/examples/workos-auth.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ async fn main() -> SdkResult<()> {
2323
resource_name: Some("Workos Oauth Test MCP Server".to_string()),
2424
resource_documentation: None,
2525
token_verifier: None,
26+
validate_audience: None,
27+
disable_audience_validation: false,
2628
})?;
2729

2830
let server = create_axum_server(
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
11
pub mod keycloak;
22
pub mod scalekit;
33
pub mod work_os;
4+
5+
use rust_mcp_sdk::auth::Audience;
6+
7+
/// Resolves the audience used to validate a token's `aud` claim.
8+
///
9+
/// Audience validation is enabled by default: when no explicit audience is
10+
/// provided, the resource identifier (`mcp_server_url`) is used. It is disabled
11+
/// only when `disable` is set, which is strongly discouraged.
12+
fn resolve_audience(disable: bool, explicit: Option<Audience>, resource: &str) -> Option<Audience> {
13+
if disable {
14+
None
15+
} else {
16+
Some(explicit.unwrap_or_else(|| Audience::Single(resource.to_string())))
17+
}
18+
}

crates/rust-mcp-extra/src/auth_provider/keycloak.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::resolve_audience;
12
use crate::token_verifier::{
23
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
34
};
@@ -7,7 +8,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
78
use http_body_util::{BodyExt, Full};
89
use rust_mcp_sdk::{
910
auth::{
10-
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
11+
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
1112
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
1213
OauthProtectedResourceMetadata, OauthTokenVerifier,
1314
},
@@ -55,6 +56,13 @@ pub struct KeycloakAuthOptions<'a> {
5556
pub resource_name: Option<String>,
5657
/// Documentation URL for this resource (optional)
5758
pub resource_documentation: Option<String>,
59+
/// Audience to validate the token's `aud` claim against.
60+
/// When `None`, the audience defaults to `mcp_server_url` (the resource
61+
/// identifier), unless `disable_audience_validation` is set.
62+
pub validate_audience: Option<Audience>,
63+
/// Disables audience validation entirely. Strongly discouraged: without it a
64+
/// token issued for another resource can be replayed against this server.
65+
pub disable_audience_validation: bool,
5866
}
5967

6068
/// Keycloak integration implementing `AuthProvider` for MCP servers.
@@ -165,11 +173,17 @@ impl KeycloakAuthProvider {
165173
tracing::warn!("Keycloak token verification is missing both Introspection and UserInfo strategies. Please provide client_id and client_secret, or ensure openid is included as a required scope.")
166174
};
167175

176+
let validate_audience = resolve_audience(
177+
options.disable_audience_validation,
178+
options.validate_audience.take(),
179+
&options.mcp_server_url,
180+
);
181+
168182
let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
169183
Some(verifier) => verifier,
170184
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
171185
strategies,
172-
validate_audience: None,
186+
validate_audience,
173187
validate_issuer: Some(options.keycloak_base_url.clone()),
174188
cache_capacity: None,
175189
})?),

crates/rust-mcp-extra/src/auth_provider/scalekit.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::resolve_audience;
12
use crate::token_verifier::{
23
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
34
};
@@ -7,7 +8,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
78
use http_body_util::{BodyExt, Full};
89
use rust_mcp_sdk::{
910
auth::{
10-
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
11+
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
1112
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
1213
OauthProtectedResourceMetadata, OauthTokenVerifier,
1314
},
@@ -43,6 +44,13 @@ pub struct ScalekitAuthOptions<'a> {
4344
/// Optional custom token verifier.
4445
/// If omitted, a default JWK-based [`GenericOauthTokenVerifier`] is created.
4546
pub token_verifier: Option<Box<dyn OauthTokenVerifier>>,
47+
/// Audience to validate the token's `aud` claim against.
48+
/// When `None`, the audience defaults to `mcp_server_url` (the resource
49+
/// identifier), unless `disable_audience_validation` is set.
50+
pub validate_audience: Option<Audience>,
51+
/// Disables audience validation entirely. Strongly discouraged: without it a
52+
/// token issued for another resource can be replayed against this server.
53+
pub disable_audience_validation: bool,
4654
}
4755

4856
/// MCP OAuth provider implementation for Scalekit.
@@ -108,7 +116,7 @@ impl ScalekitAuthProvider {
108116

109117
let mut builder = AuthMetadataBuilder::from_discovery_url(
110118
discovery_url.as_str(),
111-
options.mcp_server_url,
119+
options.mcp_server_url.clone(),
112120
required_scopes.clone(),
113121
)
114122
.await
@@ -149,11 +157,17 @@ impl ScalekitAuthProvider {
149157
});
150158
};
151159

160+
let validate_audience = resolve_audience(
161+
options.disable_audience_validation,
162+
options.validate_audience.take(),
163+
&options.mcp_server_url,
164+
);
165+
152166
let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
153167
Some(verifier) => verifier,
154168
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
155169
strategies: vec![VerificationStrategies::JWKs { jwks_uri }],
156-
validate_audience: None,
170+
validate_audience,
157171
validate_issuer: Some(issuer.to_string().trim_end_matches("/").to_string()),
158172
cache_capacity: None,
159173
})?),

crates/rust-mcp-extra/src/auth_provider/work_os.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
//! ..Default::default()
4040
//! });
4141
//! ```
42+
use super::resolve_audience;
4243
use crate::token_verifier::{
4344
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
4445
};
@@ -48,7 +49,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
4849
use http_body_util::{BodyExt, Full};
4950
use rust_mcp_sdk::{
5051
auth::{
51-
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
52+
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
5253
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
5354
OauthProtectedResourceMetadata, OauthTokenVerifier,
5455
},
@@ -71,6 +72,13 @@ pub struct WorkOSAuthOptions<'a> {
7172
pub token_verifier: Option<Box<dyn OauthTokenVerifier>>,
7273
pub resource_name: Option<String>,
7374
pub resource_documentation: Option<String>,
75+
/// Audience to validate the token's `aud` claim against.
76+
/// When `None`, the audience defaults to `mcp_server_url` (the resource
77+
/// identifier), unless `disable_audience_validation` is set.
78+
pub validate_audience: Option<Audience>,
79+
/// Disables audience validation entirely. Strongly discouraged: without it a
80+
/// token issued for another resource can be replayed against this server.
81+
pub disable_audience_validation: bool,
7482
}
7583

7684
/// WorkOS AuthKit integration implementing `AuthProvider` for MCP servers.
@@ -145,14 +153,20 @@ impl WorkOsAuthProvider {
145153
})?
146154
.to_string();
147155

156+
let validate_audience = resolve_audience(
157+
options.disable_audience_validation,
158+
options.validate_audience.take(),
159+
&options.mcp_server_url,
160+
);
161+
148162
let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
149163
Some(verifier) => verifier,
150164
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
151165
strategies: vec![
152166
VerificationStrategies::JWKs { jwks_uri },
153167
VerificationStrategies::UserInfo { userinfo_uri },
154168
],
155-
validate_audience: None,
169+
validate_audience,
156170
validate_issuer: Some(options.authkit_domain.clone()),
157171
cache_capacity: None,
158172
})?),

crates/rust-mcp-sdk/examples/mcp-server-oauth-remote.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use rust_mcp_sdk::schema::{
1111
LATEST_PROTOCOL_VERSION,
1212
};
1313
use rust_mcp_sdk::{
14-
auth::{AuthMetadataBuilder, RemoteAuthProvider},
14+
auth::{Audience, AuthMetadataBuilder, RemoteAuthProvider},
1515
error::SdkResult,
1616
event_store::InMemoryEventStore,
1717
mcp_icon, ToMcpServerHandler,
@@ -53,7 +53,9 @@ pub async fn create_oauth_provider() -> SdkResult<RemoteAuthProvider> {
5353
// GenericOauthTokenVerifier is used from rust-mcp-extra crate
5454
// you can implement yours by implementing the OauthTokenVerifier trait
5555
let token_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
56-
validate_audience: None,
56+
// Validate the audience against this server's resource identifier so a
57+
// token minted for another resource cannot be replayed here.
58+
validate_audience: Some(Audience::Single("http://localhost:3000".to_string())),
5759
validate_issuer: Some(auth_server_meta.issuer.to_string()),
5860
strategies: vec![
5961
VerificationStrategies::JWKs {

0 commit comments

Comments
 (0)