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
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use async_trait::async_trait;
use reqwest::{header::AUTHORIZATION, StatusCode};
use rust_mcp_sdk::{
auth::{
decode_token_header, Audience, AuthInfo, AuthenticationError, IntrospectionResponse,
JsonWebKeySet, OauthTokenVerifier,
decode_token_header, default_jwks_algorithms, Algorithm, Audience, AuthInfo,
AuthenticationError, IntrospectionResponse, JsonWebKeySet, OauthTokenVerifier,
},
mcp_http::error_message_from_response,
};
Expand Down Expand Up @@ -151,6 +151,8 @@ pub struct GenericOauthTokenVerifier {
validate_audience: Option<Audience>,
/// Optional issuer value to validate against the token's `iss` claim.
validate_issuer: Option<String>,
/// Signature algorithms accepted during JWKS verification.
allowed_algorithms: Vec<Algorithm>,
jwt_cache: Option<RwLock<JwtCache>>,
json_web_key_set: RwLock<Option<JwksCache>>,
introspection_uri: Option<Url>,
Expand Down Expand Up @@ -185,6 +187,7 @@ impl GenericOauthTokenVerifier {
Ok(Self {
validate_issuer,
validate_audience,
allowed_algorithms: default_jwks_algorithms(),
jwt_cache,
json_web_key_set: RwLock::new(None),
introspection_uri: strategy_options.introspection_uri,
Expand Down Expand Up @@ -371,6 +374,7 @@ impl GenericOauthTokenVerifier {
{
let token_info = cache.jwks.verify(
token.to_string(),
&self.allowed_algorithms,
self.validate_audience.as_ref(),
self.validate_issuer.as_ref(),
)?;
Expand All @@ -389,6 +393,7 @@ impl GenericOauthTokenVerifier {
if let Some(cache) = guard.as_ref() {
let token_info = cache.jwks.verify(
token.to_string(),
&self.allowed_algorithms,
self.validate_audience.as_ref(),
self.validate_issuer.as_ref(),
)?;
Expand Down
71 changes: 71 additions & 0 deletions crates/rust-mcp-sdk/src/auth/spec/jwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@ use http::StatusCode;
use jsonwebtoken::{decode, decode_header, jwk::Jwk, DecodingKey, TokenData, Validation};
use serde::{Deserialize, Serialize};

pub use jsonwebtoken::Algorithm;

/// Asymmetric signature algorithms accepted by default when verifying a JWT
/// against a JWKS.
///
/// HMAC algorithms (`HS*`) are intentionally excluded: a JWKS exposes public
/// keys, so accepting `HS*` would let an attacker sign a token with the public
/// key as the HMAC secret (the RS256 -> HS256 algorithm-confusion attack).
pub fn default_jwks_algorithms() -> Vec<Algorithm> {
vec![
Algorithm::RS256,
Algorithm::RS384,
Algorithm::RS512,
Algorithm::PS256,
Algorithm::PS384,
Algorithm::PS512,
Algorithm::ES256,
Algorithm::ES384,
Algorithm::EdDSA,
]
}

/// A JSON Web Key Set (JWKS) containing a list of JSON Web Keys.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonWebKeySet {
Expand All @@ -23,11 +45,20 @@ impl JsonWebKeySet {
pub fn verify(
&self,
token: String,
allowed_algorithms: &[Algorithm],
validate_audience: Option<&Audience>,
validate_issuer: Option<&String>,
) -> Result<TokenData<AuthClaims>, AuthenticationError> {
let header = decode_token_header(&token)?;

// Pin the verification algorithm to a configured allowlist instead of
// trusting the algorithm advertised in the token header.
if !allowed_algorithms.contains(&header.alg) {
return Err(AuthenticationError::InvalidToken {
description: "Token algorithm is not allowed",
});
}

let kid = header.kid.ok_or(AuthenticationError::InvalidToken {
description: "Missing kid in token header",
})?;
Expand All @@ -47,6 +78,8 @@ impl JsonWebKeySet {
}
})?;

// `header.alg` is now guaranteed to be in the allowlist, so pinning the
// validation to it cannot be downgraded to an HMAC algorithm.
let mut validation = Validation::new(header.alg);

let mut required_claims = vec![];
Expand Down Expand Up @@ -92,3 +125,41 @@ impl JsonWebKeySet {
Ok(token_data)
}
}

#[cfg(test)]
mod tests {
use super::*;
use jsonwebtoken::{encode, EncodingKey, Header};

#[test]
fn default_algorithms_exclude_hmac() {
let algs = default_jwks_algorithms();
assert!(!algs.contains(&Algorithm::HS256));
assert!(!algs.contains(&Algorithm::HS384));
assert!(!algs.contains(&Algorithm::HS512));
assert!(algs.contains(&Algorithm::RS256));
}

#[test]
fn rejects_token_with_disallowed_algorithm() {
// A token whose header advertises HS256 must be rejected up front,
// regardless of the keys in the set, so it can never be verified with
// a public key as an HMAC secret.
let token = encode(
&Header::new(Algorithm::HS256),
&serde_json::json!({ "sub": "attacker" }),
&EncodingKey::from_secret(b"public-key-as-secret"),
)
.unwrap();

let jwks = JsonWebKeySet { keys: vec![] };
let result = jwks.verify(token, &default_jwks_algorithms(), None, None);

assert!(matches!(
result,
Err(AuthenticationError::InvalidToken {
description: "Token algorithm is not allowed"
})
));
}
}
Loading