rust_mcp_sdk/auth/spec/
jwk.rs1use crate::auth::{Audience, AuthClaims, AuthenticationError};
2use http::StatusCode;
3use jsonwebtoken::{decode, decode_header, jwk::Jwk, DecodingKey, TokenData, Validation};
4use serde::{Deserialize, Serialize};
5
6pub use jsonwebtoken::Algorithm;
7
8pub fn default_jwks_algorithms() -> Vec<Algorithm> {
15 vec![
16 Algorithm::RS256,
17 Algorithm::RS384,
18 Algorithm::RS512,
19 Algorithm::PS256,
20 Algorithm::PS384,
21 Algorithm::PS512,
22 Algorithm::ES256,
23 Algorithm::ES384,
24 Algorithm::EdDSA,
25 ]
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct JsonWebKeySet {
31 pub keys: Vec<Jwk>,
33}
34
35pub fn decode_token_header(token: &str) -> Result<jsonwebtoken::Header, AuthenticationError> {
36 let header =
37 decode_header(token).map_err(|err| AuthenticationError::TokenVerificationFailed {
38 description: err.to_string(),
39 status_code: Some(StatusCode::UNAUTHORIZED.as_u16()),
40 })?;
41 Ok(header)
42}
43
44impl JsonWebKeySet {
45 pub fn verify(
46 &self,
47 token: String,
48 allowed_algorithms: &[Algorithm],
49 validate_audience: Option<&Audience>,
50 validate_issuer: Option<&String>,
51 ) -> Result<TokenData<AuthClaims>, AuthenticationError> {
52 let header = decode_token_header(&token)?;
53
54 if !allowed_algorithms.contains(&header.alg) {
57 return Err(AuthenticationError::TokenVerificationFailed {
58 description: format!("Token algorithm {:?} is not allowed", header.alg),
59 status_code: Some(StatusCode::UNAUTHORIZED.as_u16()),
60 });
61 }
62
63 let kid = header.kid.ok_or(AuthenticationError::InvalidToken {
64 description: "Missing kid in token header",
65 })?;
66
67 let jwk = self
68 .keys
69 .iter()
70 .find(|key| key.common.key_id == Some(kid.clone()))
71 .ok_or(AuthenticationError::InvalidToken {
72 description: "No matching key found in JWKS",
73 })?;
74
75 let decoding_key = DecodingKey::from_jwk(jwk).map_err(|err| {
76 AuthenticationError::TokenVerificationFailed {
77 description: err.to_string(),
78 status_code: None,
79 }
80 })?;
81
82 let mut validation = Validation::new(header.alg);
85
86 let mut required_claims = vec![];
87 if let Some(validate_audience) = validate_audience {
88 let vec_audience = match validate_audience {
89 Audience::Single(aud) => &vec![aud.to_owned()],
90 Audience::Multiple(auds) => auds,
91 };
92 validation.set_audience(vec_audience);
93 required_claims.push("aud");
94 } else {
95 validation.validate_aud = false;
96 }
97
98 if let Some(validate_issuer) = validate_issuer {
99 validation.set_issuer(&[validate_issuer]);
100 required_claims.push("iss");
101 }
102 if !required_claims.is_empty() {
103 validation.set_required_spec_claims(&required_claims);
104 }
105
106 let token_data =
107 decode::<AuthClaims>(token, &decoding_key, &validation).map_err(|err| {
108 match err.kind() {
109 jsonwebtoken::errors::ErrorKind::InvalidToken => {
110 AuthenticationError::InvalidToken {
111 description: "Invalid token",
112 }
113 }
114 jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
115 AuthenticationError::InvalidToken {
116 description: "Expired token",
117 }
118 }
119 _ => AuthenticationError::TokenVerificationFailed {
120 description: err.to_string(),
121 status_code: Some(StatusCode::BAD_REQUEST.as_u16()),
122 },
123 }
124 })?;
125
126 Ok(token_data)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use jsonwebtoken::{encode, EncodingKey, Header};
134
135 #[test]
136 fn default_algorithms_exclude_hmac() {
137 let algs = default_jwks_algorithms();
138 assert!(!algs.contains(&Algorithm::HS256));
139 assert!(!algs.contains(&Algorithm::HS384));
140 assert!(!algs.contains(&Algorithm::HS512));
141 assert!(algs.contains(&Algorithm::RS256));
142 }
143
144 #[test]
145 fn rejects_token_with_disallowed_algorithm() {
146 let token = encode(
150 &Header::new(Algorithm::HS256),
151 &serde_json::json!({ "sub": "attacker" }),
152 &EncodingKey::from_secret(b"public-key-as-secret"),
153 )
154 .unwrap();
155
156 let jwks = JsonWebKeySet { keys: vec![] };
157 let result = jwks.verify(token, &default_jwks_algorithms(), None, None);
158
159 assert!(matches!(
160 result,
161 Err(AuthenticationError::TokenVerificationFailed { .. })
162 ));
163 }
164
165 #[test]
166 fn error_message_reports_rejected_algorithm() {
167 let token = encode(
168 &Header::new(Algorithm::HS256),
169 &serde_json::json!({ "sub": "attacker" }),
170 &EncodingKey::from_secret(b"public-key-as-secret"),
171 )
172 .unwrap();
173
174 let jwks = JsonWebKeySet { keys: vec![] };
175 let result = jwks.verify(token, &default_jwks_algorithms(), None, None);
176
177 match result {
178 Err(AuthenticationError::TokenVerificationFailed { description, .. }) => {
179 assert!(
180 description.contains("HS256"),
181 "Error description should mention the rejected algorithm, got: {}",
182 description
183 );
184 }
185 other => panic!("Expected TokenVerificationFailed, got {:?}", other),
186 }
187 }
188}