1#![forbid(unsafe_code)]
27#![warn(clippy::pedantic)]
28#![deny(missing_docs)]
29
30use std::collections::HashMap;
31
32use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
33use serde::{Deserialize, Serialize};
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Claims {
39 pub sub: String,
41 pub email: String,
43 pub name: String,
45 pub iss: String,
47 pub aud: String,
49 pub exp: i64,
51 pub iat: i64,
53 pub jti: String,
55}
56
57#[derive(Debug, thiserror::Error)]
59pub enum VerifyError {
60 #[error("malformed jwks: {0}")]
62 Jwks(String),
63 #[error("token header has no kid")]
65 MissingKid,
66 #[error("no verification key for kid {0:?}")]
69 UnknownKid(String),
70 #[error("token verification failed: {0}")]
72 Jwt(#[from] jsonwebtoken::errors::Error),
73 #[cfg(feature = "fetch")]
75 #[error("jwks fetch failed: {0}")]
76 Fetch(String),
77}
78
79pub struct Verifier {
84 keys: HashMap<String, DecodingKey>,
85 validation: Validation,
86}
87
88impl Verifier {
89 pub fn from_jwks_value(
104 jwks: &serde_json::Value,
105 issuer: &str,
106 audience: &str,
107 ) -> Result<Self, VerifyError> {
108 let entries = jwks
109 .get("keys")
110 .and_then(serde_json::Value::as_array)
111 .ok_or_else(|| VerifyError::Jwks("missing \"keys\" array".to_string()))?;
112
113 let mut keys = HashMap::new();
114 for jwk in entries {
115 let kty = jwk.get("kty").and_then(serde_json::Value::as_str);
116 if kty != Some("RSA") {
117 continue;
118 }
119 let kid = jwk
120 .get("kid")
121 .and_then(serde_json::Value::as_str)
122 .ok_or_else(|| VerifyError::Jwks("rsa jwk missing \"kid\"".to_string()))?;
123 let n = jwk
124 .get("n")
125 .and_then(serde_json::Value::as_str)
126 .ok_or_else(|| VerifyError::Jwks(format!("jwk {kid} missing \"n\"")))?;
127 let e = jwk
128 .get("e")
129 .and_then(serde_json::Value::as_str)
130 .ok_or_else(|| VerifyError::Jwks(format!("jwk {kid} missing \"e\"")))?;
131 let key = DecodingKey::from_rsa_components(n, e)
132 .map_err(|err| VerifyError::Jwks(format!("jwk {kid}: {err}")))?;
133 keys.insert(kid.to_string(), key);
134 }
135
136 let mut validation = Validation::new(Algorithm::RS256);
137 validation.set_issuer(&[issuer]);
138 validation.set_audience(&[audience]);
139 Ok(Self { keys, validation })
140 }
141
142 #[must_use]
144 pub fn key_count(&self) -> usize {
145 self.keys.len()
146 }
147
148 pub fn verify(&self, token: &str) -> Result<Claims, VerifyError> {
157 let header = decode_header(token)?;
158 let kid = header.kid.ok_or(VerifyError::MissingKid)?;
159 let key = self.keys.get(&kid).ok_or(VerifyError::UnknownKid(kid))?;
160 let data = decode::<Claims>(token, key, &self.validation)?;
161 Ok(data.claims)
162 }
163}
164
165#[cfg(feature = "fetch")]
166impl Verifier {
167 pub async fn from_jwks_url(
177 url: &str,
178 issuer: &str,
179 audience: &str,
180 ) -> Result<Self, VerifyError> {
181 let body = reqwest::get(url)
182 .await
183 .map_err(|e| VerifyError::Fetch(e.to_string()))?
184 .error_for_status()
185 .map_err(|e| VerifyError::Fetch(e.to_string()))?
186 .json::<serde_json::Value>()
187 .await
188 .map_err(|e| VerifyError::Fetch(e.to_string()))?;
189 Self::from_jwks_value(&body, issuer, audience)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use base64::Engine;
197 use jsonwebtoken::{EncodingKey, Header};
198 use rsa::pkcs8::DecodePublicKey;
199 use rsa::traits::PublicKeyParts;
200 use sha2::{Digest, Sha256};
201
202 const TEST_PRIVATE_PEM: &str = "-----BEGIN PRIVATE KEY-----\n\
206MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCb8A4bMels7eEL\n\
207TCFMcXyJTMdQ0k2klAxkPjPRHdAtwzV+6uLjKBlI2NTubkUl0BcKLKpX1G9mDjci\n\
208Qiga5frQ/QX9IedKGdU3aLo0BvpewAK2TmRwDe48i3d+uGWPbDcDd9DG0hEz6ugd\n\
209CXeCcXekdcsX8e8bF9E4FYK+2DaI9xiVhMQS8xSY7Mt3Os/T8GBT27/JdKkzoX2Z\n\
210ntYiOOhxwcwztS+W8y1qVD6b39hPCo2vbl+nyFdN6tz00L/qakJMV4AJjXOYhT5N\n\
211XBYII92vmlznYNWNTtBymmaGhes7RbIb9AvgC2PJZdk57Sxl0tP71CsIX2P/RjPD\n\
212AoExpOfDAgMBAAECggEAKwLNGUIslM+OJZwTiS66P3KufUPsh4sQWevwRes3uw+f\n\
213Z0jpWOd8BeRM4xEGQJZDbJqCR6SAL4GXQntF7Zlmk5NevgHGdmFmtphL18Le9xh2\n\
214BwvbVy74ebmsNYct+B/MksfPDa/ub8gIys2MKa4bZoDZCltAbNQmcJY6UGJ5tFAp\n\
215ItKFgoA8wnxJroUGcw1r7B8WRyxBGxSqjYVmwtRyUcbea1gCVfCXGwkkVgv42Miu\n\
216YY6C7y9e0zlwcAXdkZGTgfYlz/hiBWd2xAg+tGV2bwVBRlX3Io1qcNUEphOHDp+l\n\
217iTf/E8DkLvln3J9DsBD6jscWE8lK8HDzLHPpT1EkSQKBgQDQ7KW2so/ZMPMvLo+R\n\
218j61XUqvRW9sJgsQvZsukGt+dbq/YDJho6J0mu2OC8Sag2ZvLezGmGdjMkUHIvza/\n\
2193KpHau4vPG0LByqZv2sF+9XAOLo5YAVPZlZFb8egYH7X2bOmVaupyGKSsmcMUsHa\n\
220x7jZf84vIY033RcLhirjDDg6ywKBgQC/Evw1CXby8WVKBPNB8pr7VKE6oy6DK76d\n\
221TESJiirq1Z8am1WKxOzvo9OlZPibfuZKeY/XiF2fj5YHWc1xR7S7+OPMlwZdVx64\n\
222h7Iv3j9yFS7jwX1AVxmOB/b48ki+QEItTEiQJqEQXiFEGr0MgI6fTj/opKEltv5L\n\
2236cfdqOCP6QKBgQCu4LchQzvnV/Lm1nl0JSi6REfvuYyR3HRtHQVuOtRcig8EsB5P\n\
224Cg6pIgd8znBACYY//8GiQFZZfWjsKSoh1QpvN1FiFplLttbw1Oo3mwHjoVg3uGkZ\n\
225ehbSjmsxkjP6Z47ZtzI2rrXcBxr8lLURdUYEQNeMWfBEB3tHuSli3ZKfmwKBgQCn\n\
226YfdEcuUbz7H+lLWQqPlxgGK5HmhJilGyNDS6FCqii76ULU1Tgk1ZZLesZPaQKSuO\n\
227RE1o71GszLkN+XJKcRl3rYHJIOf3brE/z8edvWDxDHOGG2MgsOx3Cq0kygJFf785\n\
228NWE/vkdMMlmL8qx3vkqybXb40vdENbkxQTvQBvepuQKBgGHKxTj314X4xObeQ5BY\n\
229yteRm758Id7MoieW0dYZC62a05STaiyCB5ulVCijNa66616uxyAMhquKru9xT1Bt\n\
230zGUi4GKlyqqAH7webJPHDR58z5Jj4XqAblYzRFY3nqSAd6lxdjMdIftQi0xrG6+x\n\
231UOsbyJ6S44rLeDtZ9KGxR0gS\n\
232-----END PRIVATE KEY-----\n";
233
234 const TEST_PUBLIC_PEM: &str = "-----BEGIN PUBLIC KEY-----\n\
235MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm/AOGzHpbO3hC0whTHF8\n\
236iUzHUNJNpJQMZD4z0R3QLcM1furi4ygZSNjU7m5FJdAXCiyqV9RvZg43IkIoGuX6\n\
2370P0F/SHnShnVN2i6NAb6XsACtk5kcA3uPIt3frhlj2w3A3fQxtIRM+roHQl3gnF3\n\
238pHXLF/HvGxfROBWCvtg2iPcYlYTEEvMUmOzLdzrP0/BgU9u/yXSpM6F9mZ7WIjjo\n\
239ccHMM7UvlvMtalQ+m9/YTwqNr25fp8hXTerc9NC/6mpCTFeACY1zmIU+TVwWCCPd\n\
240r5pc52DVjU7QcppmhoXrO0WyG/QL4AtjyWXZOe0sZdLT+9QrCF9j/0YzwwKBMaTn\n\
241wwIDAQAB\n\
242-----END PUBLIC KEY-----\n";
243
244 const ISSUER: &str = "authentication-service";
245 const AUDIENCE: &str = "main-x-service";
246
247 fn test_jwks() -> (serde_json::Value, String) {
250 let pub_key = rsa::RsaPublicKey::from_public_key_pem(TEST_PUBLIC_PEM).expect("parse pub");
251 let n_bytes = pub_key.n().to_bytes_be();
252 let e_bytes = pub_key.e().to_bytes_be();
253 let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
254 let n = b64.encode(&n_bytes);
255 let e = b64.encode(&e_bytes);
256 let kid = b64.encode(Sha256::digest(&n_bytes));
257 let jwks = serde_json::json!({
258 "keys": [{
259 "kty": "RSA", "use": "sig", "alg": "RS256",
260 "kid": kid, "n": n, "e": e,
261 }]
262 });
263 (jwks, kid)
264 }
265
266 fn sign(kid: &str, claims: &Claims) -> String {
267 let mut header = Header::new(Algorithm::RS256);
268 header.kid = Some(kid.to_string());
269 let key = EncodingKey::from_rsa_pem(TEST_PRIVATE_PEM.as_bytes()).expect("encoding key");
270 jsonwebtoken::encode(&header, claims, &key).expect("encode")
271 }
272
273 fn claims(exp_offset: i64) -> Claims {
274 let now = 1_900_000_000; Claims {
278 sub: "11111111-1111-1111-1111-111111111111".to_string(),
279 email: "alice@example.com".to_string(),
280 name: "Alice".to_string(),
281 iss: ISSUER.to_string(),
282 aud: AUDIENCE.to_string(),
283 exp: now + exp_offset,
284 iat: now,
285 jti: "22222222-2222-2222-2222-222222222222".to_string(),
286 }
287 }
288
289 #[test]
290 fn valid_token_round_trips_claims() {
291 let (jwks, kid) = test_jwks();
292 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
293 assert_eq!(verifier.key_count(), 1);
294
295 let token = sign(&kid, &claims(3600));
296 let got = verifier.verify(&token).expect("verify");
297 assert_eq!(got.sub, "11111111-1111-1111-1111-111111111111");
298 assert_eq!(got.email, "alice@example.com");
299 assert_eq!(got.iss, ISSUER);
300 assert_eq!(got.aud, AUDIENCE);
301 }
302
303 #[test]
304 fn expired_token_is_rejected() {
305 let (jwks, kid) = test_jwks();
306 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
307 let token = sign(&kid, &claims(-10_000_000_000));
309 assert!(matches!(verifier.verify(&token), Err(VerifyError::Jwt(_))));
310 }
311
312 #[test]
313 fn wrong_audience_is_rejected() {
314 let (jwks, kid) = test_jwks();
315 let verifier =
316 Verifier::from_jwks_value(&jwks, ISSUER, "some-other-service").expect("build");
317 let token = sign(&kid, &claims(3600));
318 assert!(matches!(verifier.verify(&token), Err(VerifyError::Jwt(_))));
319 }
320
321 #[test]
322 fn wrong_issuer_is_rejected() {
323 let (jwks, kid) = test_jwks();
327 let verifier =
328 Verifier::from_jwks_value(&jwks, "some-other-issuer", AUDIENCE).expect("build");
329 let token = sign(&kid, &claims(3600));
330 assert!(matches!(verifier.verify(&token), Err(VerifyError::Jwt(_))));
331 }
332
333 #[test]
334 fn unknown_kid_is_rejected() {
335 let (jwks, kid) = test_jwks();
336 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
337 let token = sign("not-a-known-kid", &claims(3600));
339 assert!(matches!(
340 verifier.verify(&token),
341 Err(VerifyError::UnknownKid(_))
342 ));
343 let _ = kid;
344 }
345
346 #[test]
347 fn tampered_signature_is_rejected() {
348 let (jwks, kid) = test_jwks();
349 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
350 let token = sign(&kid, &claims(3600));
351 let mut bytes = token.into_bytes();
352 let last = bytes.len() - 1;
353 bytes[last] = if bytes[last] == b'A' { b'B' } else { b'A' };
355 let tampered = String::from_utf8(bytes).unwrap();
356 assert!(verifier.verify(&tampered).is_err());
357 }
358
359 #[test]
360 fn garbage_token_is_rejected() {
361 let (jwks, _) = test_jwks();
362 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
363 assert!(verifier.verify("not.a.jwt").is_err());
364 assert!(verifier.verify("").is_err());
365 }
366
367 #[test]
368 fn empty_jwks_builds_but_rejects_everything() {
369 let jwks = serde_json::json!({ "keys": [] });
370 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
371 assert_eq!(verifier.key_count(), 0);
372 let (_, kid) = test_jwks();
373 let token = sign(&kid, &claims(3600));
374 assert!(matches!(
375 verifier.verify(&token),
376 Err(VerifyError::UnknownKid(_))
377 ));
378 }
379
380 #[test]
381 fn jwks_without_keys_array_errors() {
382 let jwks = serde_json::json!({ "not_keys": [] });
383 assert!(matches!(
384 Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE),
385 Err(VerifyError::Jwks(_))
386 ));
387 }
388
389 #[test]
390 fn non_rsa_keys_are_skipped() {
391 let jwks = serde_json::json!({
392 "keys": [{ "kty": "EC", "kid": "ec-1", "crv": "P-256", "x": "a", "y": "b" }]
393 });
394 let verifier = Verifier::from_jwks_value(&jwks, ISSUER, AUDIENCE).expect("build");
395 assert_eq!(verifier.key_count(), 0);
396 }
397}