Skip to main content

rust_mcp_sdk/auth/spec/
jwk.rs

1use 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
8/// Asymmetric signature algorithms accepted by default when verifying a JWT
9/// against a JWKS.
10///
11/// HMAC algorithms (`HS*`) are intentionally excluded: a JWKS exposes public
12/// keys, so accepting `HS*` would let an attacker sign a token with the public
13/// key as the HMAC secret (the RS256 -> HS256 algorithm-confusion attack).
14pub 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/// A JSON Web Key Set (JWKS) containing a list of JSON Web Keys.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct JsonWebKeySet {
31    /// List of JSON Web Keys.
32    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        // Pin the verification algorithm to a configured allowlist instead of
55        // trusting the algorithm advertised in the token header.
56        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        // `header.alg` is now guaranteed to be in the allowlist, so pinning the
83        // validation to it cannot be downgraded to an HMAC algorithm.
84        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        // A token whose header advertises HS256 must be rejected up front,
147        // regardless of the keys in the set, so it can never be verified with
148        // a public key as an HMAC secret.
149        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}