Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/rust-mcp-extra/examples/keycloak-auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ async fn main() -> SdkResult<()> {
client_secret: env::var("CLIENT_SECRET").ok(),
token_verifier: None,
resource_documentation: None,
validate_audience: None,
disable_audience_validation: false,
})?;

let server = hyper_server::create_server(
Expand Down
2 changes: 2 additions & 0 deletions crates/rust-mcp-extra/examples/scalekit-auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ async fn main() -> SdkResult<()> {
token_verifier: None,
resource_name: Some("Scalekit Oauth Test MCP Server".to_string()),
resource_documentation: None,
validate_audience: None,
disable_audience_validation: false,
environment_url: env::var("ENVIRONMENT_URL")
.expect("Please set 'ENVIRONMENT_URL' evnrionment variable and try again."),
resource_id: env::var("RESOURCE_ID")
Expand Down
2 changes: 2 additions & 0 deletions crates/rust-mcp-extra/examples/workos-auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ async fn main() -> SdkResult<()> {
resource_name: Some("Workos Oauth Test MCP Server".to_string()),
resource_documentation: None,
token_verifier: None,
validate_audience: None,
disable_audience_validation: false,
})?;

let server = hyper_server::create_server(
Expand Down
15 changes: 15 additions & 0 deletions crates/rust-mcp-extra/src/auth_provider.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
pub mod keycloak;
pub mod scalekit;
pub mod work_os;

use rust_mcp_sdk::auth::Audience;

/// Resolves the audience used to validate a token's `aud` claim.
///
/// Audience validation is enabled by default: when no explicit audience is
/// provided, the resource identifier (`mcp_server_url`) is used. It is disabled
/// only when `disable` is set, which is strongly discouraged.
fn resolve_audience(disable: bool, explicit: Option<Audience>, resource: &str) -> Option<Audience> {
if disable {
None
} else {
Some(explicit.unwrap_or_else(|| Audience::Single(resource.to_string())))
}
}
18 changes: 16 additions & 2 deletions crates/rust-mcp-extra/src/auth_provider/keycloak.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::resolve_audience;
use crate::token_verifier::{
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
};
Expand All @@ -7,7 +8,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
use http_body_util::{BodyExt, Full};
use rust_mcp_sdk::{
auth::{
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
OauthProtectedResourceMetadata, OauthTokenVerifier,
},
Expand Down Expand Up @@ -55,6 +56,13 @@ pub struct KeycloakAuthOptions<'a> {
pub resource_name: Option<String>,
/// Documentation URL for this resource (optional)
pub resource_documentation: Option<String>,
/// Audience to validate the token's `aud` claim against.
/// When `None`, the audience defaults to `mcp_server_url` (the resource
/// identifier), unless `disable_audience_validation` is set.
pub validate_audience: Option<Audience>,
/// Disables audience validation entirely. Strongly discouraged: without it a
/// token issued for another resource can be replayed against this server.
pub disable_audience_validation: bool,
}

/// Keycloak integration implementing `AuthProvider` for MCP servers.
Expand Down Expand Up @@ -165,11 +173,17 @@ impl KeycloakAuthProvider {
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.")
};

let validate_audience = resolve_audience(
options.disable_audience_validation,
options.validate_audience.take(),
&options.mcp_server_url,
);

let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
Some(verifier) => verifier,
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
strategies,
validate_audience: None,
validate_audience,
validate_issuer: Some(options.keycloak_base_url.clone()),
cache_capacity: None,
})?),
Expand Down
20 changes: 17 additions & 3 deletions crates/rust-mcp-extra/src/auth_provider/scalekit.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::resolve_audience;
use crate::token_verifier::{
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
};
Expand All @@ -7,7 +8,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
use http_body_util::{BodyExt, Full};
use rust_mcp_sdk::{
auth::{
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
OauthProtectedResourceMetadata, OauthTokenVerifier,
},
Expand Down Expand Up @@ -43,6 +44,13 @@ pub struct ScalekitAuthOptions<'a> {
/// Optional custom token verifier.
/// If omitted, a default JWK-based [`GenericOauthTokenVerifier`] is created.
pub token_verifier: Option<Box<dyn OauthTokenVerifier>>,
/// Audience to validate the token's `aud` claim against.
/// When `None`, the audience defaults to `mcp_server_url` (the resource
/// identifier), unless `disable_audience_validation` is set.
pub validate_audience: Option<Audience>,
/// Disables audience validation entirely. Strongly discouraged: without it a
/// token issued for another resource can be replayed against this server.
pub disable_audience_validation: bool,
}

/// MCP OAuth provider implementation for Scalekit.
Expand Down Expand Up @@ -108,7 +116,7 @@ impl ScalekitAuthProvider {

let mut builder = AuthMetadataBuilder::from_discovery_url(
discovery_url.as_str(),
options.mcp_server_url,
options.mcp_server_url.clone(),
required_scopes.clone(),
)
.await
Expand Down Expand Up @@ -149,11 +157,17 @@ impl ScalekitAuthProvider {
});
};

let validate_audience = resolve_audience(
options.disable_audience_validation,
options.validate_audience.take(),
&options.mcp_server_url,
);

let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
Some(verifier) => verifier,
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
strategies: vec![VerificationStrategies::JWKs { jwks_uri }],
validate_audience: None,
validate_audience,
validate_issuer: Some(issuer.to_string().trim_end_matches("/").to_string()),
cache_capacity: None,
})?),
Expand Down
18 changes: 16 additions & 2 deletions crates/rust-mcp-extra/src/auth_provider/work_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
//! ..Default::default()
//! });
//! ```
use super::resolve_audience;
use crate::token_verifier::{
GenericOauthTokenVerifier, TokenVerifierOptions, VerificationStrategies,
};
Expand All @@ -48,7 +49,7 @@ use http::{header::CONTENT_TYPE, StatusCode};
use http_body_util::{BodyExt, Full};
use rust_mcp_sdk::{
auth::{
create_discovery_endpoints, AuthInfo, AuthMetadataBuilder, AuthProvider,
create_discovery_endpoints, Audience, AuthInfo, AuthMetadataBuilder, AuthProvider,
AuthenticationError, AuthorizationServerMetadata, OauthEndpoint,
OauthProtectedResourceMetadata, OauthTokenVerifier,
},
Expand All @@ -71,6 +72,13 @@ pub struct WorkOSAuthOptions<'a> {
pub token_verifier: Option<Box<dyn OauthTokenVerifier>>,
pub resource_name: Option<String>,
pub resource_documentation: Option<String>,
/// Audience to validate the token's `aud` claim against.
/// When `None`, the audience defaults to `mcp_server_url` (the resource
/// identifier), unless `disable_audience_validation` is set.
pub validate_audience: Option<Audience>,
/// Disables audience validation entirely. Strongly discouraged: without it a
/// token issued for another resource can be replayed against this server.
pub disable_audience_validation: bool,
}

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

let validate_audience = resolve_audience(
options.disable_audience_validation,
options.validate_audience.take(),
&options.mcp_server_url,
);

let token_verifier: Box<dyn OauthTokenVerifier> = match options.token_verifier {
Some(verifier) => verifier,
None => Box::new(GenericOauthTokenVerifier::new(TokenVerifierOptions {
strategies: vec![
VerificationStrategies::JWKs { jwks_uri },
VerificationStrategies::UserInfo { userinfo_uri },
],
validate_audience: None,
validate_audience,
validate_issuer: Some(options.authkit_domain.clone()),
cache_capacity: None,
})?),
Expand Down
6 changes: 4 additions & 2 deletions crates/rust-mcp-sdk/examples/mcp-server-oauth-remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use rust_mcp_sdk::schema::{
LATEST_PROTOCOL_VERSION,
};
use rust_mcp_sdk::{
auth::{AuthMetadataBuilder, RemoteAuthProvider},
auth::{Audience, AuthMetadataBuilder, RemoteAuthProvider},
error::SdkResult,
event_store::InMemoryEventStore,
mcp_icon,
Expand Down Expand Up @@ -54,7 +54,9 @@ pub async fn create_oauth_provider() -> SdkResult<RemoteAuthProvider> {
// GenericOauthTokenVerifier is used from rust-mcp-extra crate
// you can implement yours by implementing the OauthTokenVerifier trait
let token_verifier = GenericOauthTokenVerifier::new(TokenVerifierOptions {
validate_audience: None,
// Validate the audience against this server's resource identifier so a
// token minted for another resource cannot be replayed here.
validate_audience: Some(Audience::Single("http://localhost:3000".to_string())),
validate_issuer: Some(auth_server_meta.issuer.to_string()),
strategies: vec![
VerificationStrategies::JWKs {
Expand Down
Loading