1use std::{collections::HashMap, time::SystemTime};
2
3use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use thiserror::Error;
7
8const IAP_ISSUER: &str = "https://cloud.google.com/iap";
9
10#[cfg(all(not(feature = "aws_lc_rs"), not(feature = "rust_crypto")))]
11compile_error!("Either aws_lc_rs or rust_crypto feature must be enabled");
12
13#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
15pub struct Claims {
16 pub exp: u64,
17 pub iat: u64,
18 pub aud: String,
19 pub iss: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub hd: Option<String>,
22 pub sub: String,
23 pub email: String,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub google: Option<Value>,
26}
27
28#[derive(Debug, Error)]
30#[non_exhaustive]
31pub enum Error {
32 #[error("Invalid kid: {0}")]
33 InvalidKid(String),
34 #[error("Invalid alg: {0}, must be ES256")]
35 InvalidAlgorithm(String),
36 #[error("Invalid aud: {actual}, expected {expected}")]
37 InvalidAudience { actual: String, expected: String },
38 #[error("Invalid issuer: {0}, expected {IAP_ISSUER}")]
39 InvalidIssuer(String),
40 #[error("Invalid key format")]
41 InvalidKeyFormat,
42 #[error(transparent)]
43 JsonWebToken(#[from] jsonwebtoken::errors::Error),
44 #[error("Request failed: {0}")]
45 RequestFailed(Box<dyn std::error::Error + Send + 'static>),
46 #[error("Kid not available in header")]
47 KidNotAvailable,
48 #[error("Invalid iat: {actual} > {expected}")]
49 FutureIat { actual: u64, expected: u64 },
50 #[error("Invalid hosted domain: {actual}, expected {expected:?}")]
51 InvalidHostedDomain {
52 actual: String,
53 expected: Vec<String>,
54 },
55 #[error("hd claim is missing, expected {expected:?}")]
56 HdClaimMissing { expected: Vec<String> },
57 #[error("Insufficient access level, expected {0}")]
58 InsufficientAccessLevel(String),
59 #[error("Access levels claim is missing")]
60 AccessLevelsMissing,
61 #[error("Invalid google claims")]
62 InvalidGoogleClaims,
63 #[error("Token lifetime too long: iat: {iat}, exp: {exp}, max_lifetime: {max_lifetime}")]
64 TokenLifetimeTooLong {
65 iat: u64,
66 exp: u64,
67 max_lifetime: u64,
68 },
69}
70
71pub struct ValidationConfig {
79 audience: Vec<String>,
80 google_hosted_domain: Option<Vec<String>>,
82 access_levels: Option<Vec<String>>,
83 skew: u64,
85 max_token_lifetime: u64,
87}
88
89impl ValidationConfig {
90 pub fn new<A, I>(audience: I) -> Self
94 where
95 A: Into<String>,
96 I: IntoIterator<Item = A>,
97 {
98 Self {
99 audience: audience.into_iter().map(Into::into).collect(),
100 google_hosted_domain: None,
101 access_levels: None,
102 skew: 30, max_token_lifetime: 600 + 2 * 30, }
105 }
106
107 pub fn with_google_hosted_domain<H, I>(mut self, google_hosted_domain: I) -> Self
111 where
112 H: Into<String>,
113 I: IntoIterator<Item = H>,
114 {
115 self.google_hosted_domain =
116 Some(google_hosted_domain.into_iter().map(Into::into).collect());
117 self
118 }
119
120 pub fn with_access_levels<T: Into<String>>(
122 mut self,
123 access_levels: impl IntoIterator<Item = T>,
124 ) -> Self {
125 self.access_levels = Some(access_levels.into_iter().map(Into::into).collect());
126 self
127 }
128
129 pub async fn decode_and_validate<E: std::error::Error + Send + 'static>(
131 &self,
132 token: &str,
133 client: &impl PublicKeySource<Error = E>,
134 ) -> Result<Claims, Error> {
135 let header = decode_header(token)?;
136 let kid = header.kid.ok_or(Error::KidNotAvailable)?;
137 let public_key = client
138 .get_public_key(&kid)
139 .await
140 .map_err(|e| Error::RequestFailed(Box::new(e)))?
141 .ok_or_else(|| Error::InvalidKid(kid))?;
142 let mut validation = Validation::new(Algorithm::ES256);
143 validation.set_audience(&self.audience);
144 validation.set_issuer(&[IAP_ISSUER]);
145 validation.leeway = self.skew;
146
147 let token = decode::<Claims>(
148 token,
149 &DecodingKey::from_ec_pem(public_key.as_bytes())
150 .map_err(|_| Error::InvalidKeyFormat)?,
151 &validation,
152 )?;
153
154 let now = SystemTime::now()
155 .duration_since(std::time::UNIX_EPOCH)
156 .unwrap()
157 .as_secs();
158
159 if token.claims.iat > now + self.skew {
161 return Err(Error::FutureIat {
162 actual: token.claims.iat,
163 expected: now,
164 });
165 }
166
167 if token.claims.exp > token.claims.iat + self.max_token_lifetime {
169 return Err(Error::TokenLifetimeTooLong {
170 iat: token.claims.iat,
171 exp: token.claims.exp,
172 max_lifetime: self.max_token_lifetime,
173 });
174 }
175
176 if let Some(expected_hd) = &self.google_hosted_domain {
177 let Some(hd) = &token.claims.hd else {
178 return Err(Error::HdClaimMissing {
179 expected: expected_hd.clone(),
180 });
181 };
182 if !expected_hd.contains(hd) {
183 return Err(Error::InvalidHostedDomain {
184 actual: hd.clone(),
185 expected: expected_hd.clone(),
186 });
187 }
188 }
189
190 if let Some(expected_access_levels) = &self.access_levels {
191 #[derive(Deserialize)]
192 struct GoogleClaims {
193 access_levels: Vec<String>,
194 }
195 let google: GoogleClaims = serde_json::from_value(
196 token
197 .claims
198 .google
199 .as_ref()
200 .ok_or(Error::AccessLevelsMissing)?
201 .clone(),
202 )
203 .map_err(|_| Error::InvalidGoogleClaims)?;
204 for access_level in expected_access_levels {
205 if !google.access_levels.contains(access_level) {
206 return Err(Error::InsufficientAccessLevel(access_level.clone()));
207 }
208 }
209 }
210
211 Ok(token.claims)
212 }
213}
214
215pub trait PublicKeySource {
216 type Error: std::error::Error + Send + 'static;
217
218 fn get_public_key(
219 &self,
220 key: &str,
221 ) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send;
222}
223
224#[cfg(feature = "reqwest")]
225impl PublicKeySource for reqwest::Client {
226 type Error = reqwest::Error;
227
228 async fn get_public_key(&self, key: &str) -> Result<Option<String>, Self::Error> {
229 Ok(self
230 .get("https://www.gstatic.com/iap/verify/public_key")
231 .send()
232 .await?
233 .error_for_status()?
234 .json::<HashMap<String, String>>()
235 .await?
236 .remove(key))
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use jsonwebtoken::{EncodingKey, Header};
243 use serde_json::json;
244
245 use super::*;
246
247 const VALID_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
248MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrpu756TO0uDuesyS
2491S1jL/6u/X5TUTfnSscBq6sVLTihRANCAATnZzElTUxsOkFb6AhJ2vRUy3uSuRy/
250JX8+CfoH13EhLv+gIqtL8ooDGQKktq9fd/yo89wv3Ut8CVDxET2h34jE
251-----END PRIVATE KEY-----
252";
253 const VALID_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----
254MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE52cxJU1MbDpBW+gISdr0VMt7krkc
255vyV/Pgn6B9dxIS7/oCKrS/KKAxkCpLavX3f8qPPcL91LfAlQ8RE9od+IxA==
256-----END PUBLIC KEY-----
257";
258
259 const TEST_AUD: &str = "/projects/1234567890/global/backendServices/test-service-id";
260 const TEST_KID: &str = "test-kid";
261
262 fn test_header() -> Header {
263 let mut header = Header::new(Algorithm::ES256);
264 header.kid = Some(TEST_KID.to_string());
265 header
266 }
267
268 fn test_claims() -> Claims {
269 let now = SystemTime::now()
270 .duration_since(std::time::UNIX_EPOCH)
271 .unwrap()
272 .as_secs();
273 Claims {
274 exp: now + 600, iat: now,
276 aud: TEST_AUD.to_string(),
277 iss: "https://cloud.google.com/iap".into(),
278 hd: Some("example.com".to_string()),
279 google: Some(json!({
280 "access_levels": ["OWNER", "EDITOR"],
281 })),
282 sub: "1234567890".into(),
283 email: "test@example.com".into(),
284 }
285 }
286
287 #[derive(Debug, Error)]
288 #[error("Mock error: {0}")]
289 struct MockError(String);
290
291 fn mock_client(
292 f: impl Fn(&str) -> Result<Option<String>, MockError> + Send + Sync + 'static,
293 ) -> impl PublicKeySource {
294 type MockFn = Box<dyn Fn(&str) -> Result<Option<String>, MockError> + Send + Sync>;
295 struct MockClient(MockFn);
296 impl PublicKeySource for MockClient {
297 type Error = MockError;
298
299 async fn get_public_key(&self, key: &str) -> Result<Option<String>, Self::Error> {
300 self.0(key)
301 }
302 }
303 MockClient(Box::new(f))
304 }
305
306 #[tokio::test]
307 async fn test_decode_with_public_key() {
308 let claims = test_claims();
309 let token = jsonwebtoken::encode(
310 &test_header(),
311 &claims,
312 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
313 )
314 .unwrap();
315 let client = mock_client(|key| {
316 assert_eq!(key, TEST_KID);
317 Ok(Some(VALID_PUBLIC_KEY.to_string()))
318 });
319 let decoded = ValidationConfig::new([TEST_AUD])
320 .with_google_hosted_domain(["example.com"])
321 .with_access_levels(["OWNER", "EDITOR"])
322 .decode_and_validate(&token, &client)
323 .await
324 .unwrap();
325 assert_eq!(decoded.exp, claims.exp);
326 assert_eq!(decoded.iat, claims.iat);
327 assert_eq!(decoded.aud, claims.aud);
328 assert_eq!(decoded.iss, claims.iss);
329 assert_eq!(decoded.hd, claims.hd);
330 assert_eq!(decoded.sub, claims.sub);
331 assert_eq!(decoded.email, claims.email);
332 }
333
334 #[tokio::test]
335 async fn test_decode_with_public_key_not_found() {
336 let claims = test_claims();
337 let token = jsonwebtoken::encode(
338 &test_header(),
339 &claims,
340 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
341 )
342 .unwrap();
343 let client = mock_client(|key| {
344 assert_eq!(key, TEST_KID);
345 Ok(None)
346 });
347 let error = ValidationConfig::new([TEST_AUD])
348 .decode_and_validate(&token, &client)
349 .await
350 .unwrap_err();
351 assert_eq!(error.to_string(), "Invalid kid: test-kid");
352 }
353
354 #[tokio::test]
355 async fn test_decode_with_private_key_is_invalid() {
356 const TEST_INVALID_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
357MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgx1I+Ljp/UOxUumfg
358kp1T9PFpY8RklbMF1SHmFaB1OXihRANCAAQnLQRg6fL2pgJYUPKdl6DFVsKtda3i
359sDlX34kd5D0tFCdaZ5LH7MRtf5ptFCWouh7JDyOcAucHHwz0Z20PKFmu
360-----END PRIVATE KEY-----
361";
362 let claims = test_claims();
363 let token = jsonwebtoken::encode(
364 &test_header(),
365 &claims,
366 &EncodingKey::from_ec_pem(TEST_INVALID_PRIVATE_KEY.as_bytes()).unwrap(),
367 )
368 .unwrap();
369 let client = mock_client(|key| {
370 assert_eq!(key, TEST_KID);
371 Ok(Some(VALID_PUBLIC_KEY.to_string()))
372 });
373 let error = ValidationConfig::new([TEST_AUD])
374 .decode_and_validate(&token, &client)
375 .await
376 .unwrap_err();
377 assert_eq!(error.to_string(), "InvalidSignature");
378 }
379
380 #[tokio::test]
381 async fn test_decode_invalid_audience() {
382 let mut claims = test_claims();
383 claims.aud = "invalid-aud".to_string();
384 let token = jsonwebtoken::encode(
385 &test_header(),
386 &claims,
387 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
388 )
389 .unwrap();
390 let client = mock_client(|key| {
391 assert_eq!(key, TEST_KID);
392 Ok(Some(VALID_PUBLIC_KEY.to_string()))
393 });
394 let error = ValidationConfig::new([TEST_AUD])
395 .decode_and_validate(&token, &client)
396 .await
397 .unwrap_err();
398 assert_eq!(error.to_string(), "InvalidAudience");
399 }
400
401 #[tokio::test]
402 async fn test_decode_aud_not_found() {
403 let claims = test_claims();
404 let mut json = serde_json::to_value(&claims).unwrap();
405 json.as_object_mut().unwrap().remove("aud");
406 let token = jsonwebtoken::encode(
407 &test_header(),
408 &json,
409 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
410 )
411 .unwrap();
412 let client = mock_client(|key| {
413 assert_eq!(key, TEST_KID);
414 Ok(Some(VALID_PUBLIC_KEY.to_string()))
415 });
416 let error = ValidationConfig::new([TEST_AUD])
417 .decode_and_validate(&token, &client)
418 .await
419 .unwrap_err();
420 assert!(
421 error.to_string().contains("missing field `aud`"),
422 "{} does not contain `aud`",
423 error
424 );
425 }
426
427 #[tokio::test]
428 async fn test_decode_invalid_algorithm() {
429 const TEST_RSA_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----
430MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDbWZjcOnBP8XB1
431OJIitW3Nc7/YpI+rNni1JBCSngMN/PVnOWrmkEj3ObsvYPwVpZ+FF+tKYNdcCU3S
432Z6ntUkd6404tB/QeutCtRFF1m9jtLa1bayP3dnsnhrrD/qsV/BWxw1BynJ8a1HLe
433303E0jBKNa+kVxf0OMccHyyoAAnaOtCvJmY+xFV/Z9ai584Rs8aKWoiL+vg28tEt
434LGu/YgWIbEibVbQCpy8w8kuYFraU92uuXlCDJ6SMIfWmaj4yob/YiGDqwYTT+jyF
435EZIXsHJzuUXCsgshfG4cAOKjYpqR+bbRwPVznINxQLWgs4SCisX8xMXuAumyVLta
436K330dWXbAgMBAAECgf994+gVzockCXpcI/Yrqor2frBkeorbChDfdTEUB6imaxFa
437hHpyMPMA2BTt15yX7yVgKuOObvNplETXao6ZKWeq+mZgnZoghcZ2pNgbuoIjeEWv
438H2rJu1vHnp5/hvs/+k7QmByaDtlz3b87iFfuRvKC3RTes8AFfXF39w684B0SZlpp
439rayQY2eXNQnVZXxORLls2NEkqFByZmIuNd0yIuvnYaYICtvzL8Er7xPkgLREvmzH
440rivbjhL2IpFrlH7Hye9kKSrQ0nwmuKAZutOYP1PPq1YrK/dEkJhy6NdqAcMlBhib
441PH4WQ2wCFtADb74mI3vfEKDHFMdaEf0XxBc0BEECgYEA8CkZbejMFcuIrWuXJ61V
442EtFEKZQVr8LKvGhlE41vEqfqBt7Gj6VHz9fJEVo/nrqz9lZz6lKFZMF0TLkEHRFc
443jtXaLoJQ9OVcqiA4cwUFoHE99rjql8oDZ2/ccOaHnmxlXfByGUTDuI+CxbuEh8AR
444GfFk7HR8h6rR2jlMDQrox4cCgYEA6dEhN5LNIHY0lpLx9rhDEb/ecsn64bo4vOTk
445KiSwcw/VDiYQeua6JnVpY1c4Ir9us5+NNCs/As00LJeNdWkFDLO5dnxud0mqquKd
446SSG1tS4L7CIi7IqQYeOSDB3idFxj8u/8M6FgMVXuDui+inxkd+0Yb1ZVmtC9vTCD
447oEPLnA0CgYBLpXZ4E0Ltfo3PqjsTaVqJsdbZjeaC1UWMsQldbkhVRQTHIzbCGlqT
448UjHoQFgXxFFZP4QFg/a2dOUQIZr1GPnhl+TAj5W2feSBReLh/+v0zJaq9zYVl7EY
449zLhP650+PoBzZYBbCzjnEnUrmVQ2ej4owMt8W3i6Nwkgxrl4xj3qUwKBgQCipt5q
450oG6dxFz02igEL05IzKZcR/GEkVzi2n92aattf3gAra4NMPARzN+RQZ1FXtINllJO
451Fj9xHXrMAmlfYb0nhubfa9QUm2RkF9y+gPq8nNmiXGTbE9E4p2xzjV54/8RvvU4+
452RGZ8K4C9Ul8qSzpAyuiSmwZV+hvjvhnypPbBCQKBgFNo/R0p0O9GLAPe6aF5dJVx
453mXTqfrk1BmXxkSS16EgoZC8D8N6EnAQC8tI6FFYWYsgD7kaUnKG1lNIXQjw63PDx
454NksAFp0qJeVLilaFwTXrR1RNIoWzWrZkWzsZ3IHpE1WHgrH3hzVZ0O3X94ns4vT6
455W8BYg+xGjeeybwjKuiVc
456-----END PRIVATE KEY-----
457";
458 const TEST_RSA_PUBLIC_KEY: &str = "-----BEGIN PUBLIC KEY-----
459MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA21mY3DpwT/FwdTiSIrVt
460zXO/2KSPqzZ4tSQQkp4DDfz1Zzlq5pBI9zm7L2D8FaWfhRfrSmDXXAlN0mep7VJH
461euNOLQf0HrrQrURRdZvY7S2tW2sj93Z7J4a6w/6rFfwVscNQcpyfGtRy3t9NxNIw
462SjWvpFcX9DjHHB8sqAAJ2jrQryZmPsRVf2fWoufOEbPGilqIi/r4NvLRLSxrv2IF
463iGxIm1W0AqcvMPJLmBa2lPdrrl5QgyekjCH1pmo+MqG/2Ihg6sGE0/o8hRGSF7By
464c7lFwrILIXxuHADio2Kakfm20cD1c5yDcUC1oLOEgorF/MTF7gLpslS7Wit99HVl
4652wIDAQAB
466-----END PUBLIC KEY-----
467";
468 let mut header = test_header();
469 header.alg = Algorithm::RS256;
470 let claims = test_claims();
471 let token = jsonwebtoken::encode(
472 &header,
473 &claims,
474 &EncodingKey::from_rsa_pem(TEST_RSA_PRIVATE_KEY.as_bytes()).unwrap(),
475 )
476 .unwrap();
477 let client = mock_client(|key| {
478 assert_eq!(key, TEST_KID);
479 Ok(Some(TEST_RSA_PUBLIC_KEY.to_string()))
480 });
481 let error = ValidationConfig::new([TEST_AUD])
482 .decode_and_validate(&token, &client)
483 .await
484 .unwrap_err();
485 assert!(error.to_string().contains("Invalid key format"));
486 }
487
488 #[tokio::test]
489 async fn test_decode_request_failure_handling() {
490 let claims = test_claims();
491 let token = jsonwebtoken::encode(
492 &test_header(),
493 &claims,
494 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
495 )
496 .unwrap();
497 let client = mock_client(|key| {
498 assert_eq!(key, TEST_KID);
499 Err(MockError("test-error".to_string()))
500 });
501 let error = ValidationConfig::new([TEST_AUD])
502 .decode_and_validate(&token, &client)
503 .await
504 .unwrap_err();
505 assert_eq!(error.to_string(), "Request failed: Mock error: test-error");
506 }
507
508 #[tokio::test]
509 async fn test_docode_kid_not_available() {
510 let mut header = test_header();
511 header.kid = None;
512 let claims = test_claims();
513 let token = jsonwebtoken::encode(
514 &header,
515 &claims,
516 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
517 )
518 .unwrap();
519 let client = mock_client(|key| {
520 assert_eq!(key, TEST_KID);
521 Err(MockError("test-error".to_string()))
522 });
523 let error = ValidationConfig::new([TEST_AUD])
524 .decode_and_validate(&token, &client)
525 .await
526 .unwrap_err();
527 assert_eq!(error.to_string(), "Kid not available in header");
528 }
529
530 #[tokio::test]
531 async fn test_decode_future_iat() {
532 let mut claims = test_claims();
533 claims.iat += 120;
534 let token = jsonwebtoken::encode(
535 &test_header(),
536 &claims,
537 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
538 )
539 .unwrap();
540 let client = mock_client(|key| {
541 assert_eq!(key, TEST_KID);
542 Ok(Some(VALID_PUBLIC_KEY.to_string()))
543 });
544 let error = ValidationConfig::new([TEST_AUD])
545 .decode_and_validate(&token, &client)
546 .await
547 .unwrap_err();
548 assert!(error.to_string().contains("Invalid iat"));
549 }
550
551 #[tokio::test]
552 async fn test_decode_without_iat() {
553 let claims = test_claims();
554 let mut json = serde_json::to_value(&claims).unwrap();
555 json.as_object_mut().unwrap().remove("iat");
556 let token = jsonwebtoken::encode(
557 &test_header(),
558 &json,
559 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
560 )
561 .unwrap();
562 let client = mock_client(|key| {
563 assert_eq!(key, TEST_KID);
564 Ok(Some(VALID_PUBLIC_KEY.to_string()))
565 });
566 let error = ValidationConfig::new([TEST_AUD])
567 .decode_and_validate(&token, &client)
568 .await
569 .unwrap_err();
570 assert!(error.to_string().contains("missing field `iat`"));
571 }
572
573 #[tokio::test]
574 async fn test_decode_expired() {
575 let mut claims = test_claims();
576 claims.exp -= 4000;
577 claims.iat -= 4000;
578 let token = jsonwebtoken::encode(
579 &test_header(),
580 &claims,
581 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
582 )
583 .unwrap();
584 let client = mock_client(|key| {
585 assert_eq!(key, TEST_KID);
586 Ok(Some(VALID_PUBLIC_KEY.to_string()))
587 });
588 let error = ValidationConfig::new([TEST_AUD])
589 .decode_and_validate(&token, &client)
590 .await
591 .unwrap_err();
592 assert!(error.to_string().contains("Expired"));
593 }
594
595 #[tokio::test]
598 async fn test_decode_exp_within_skew_tolerance() {
599 let now = SystemTime::now()
600 .duration_since(std::time::UNIX_EPOCH)
601 .unwrap()
602 .as_secs();
603
604 let mut claims = test_claims();
605 claims.iat = now - 600; claims.exp = now - 30; let token = jsonwebtoken::encode(
610 &test_header(),
611 &claims,
612 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
613 )
614 .unwrap();
615 let client = mock_client(|key| {
616 assert_eq!(key, TEST_KID);
617 Ok(Some(VALID_PUBLIC_KEY.to_string()))
618 });
619
620 let decoded = ValidationConfig::new([TEST_AUD])
622 .decode_and_validate(&token, &client)
623 .await
624 .expect("Token with exp at skew boundary should be valid");
625
626 assert_eq!(decoded.exp, claims.exp);
627 }
628
629 #[tokio::test]
633 async fn test_decode_exp_beyond_skew_tolerance() {
634 let now = SystemTime::now()
635 .duration_since(std::time::UNIX_EPOCH)
636 .unwrap()
637 .as_secs();
638
639 let mut claims = test_claims();
640 claims.iat = now - 600; claims.exp = now - 31; let token = jsonwebtoken::encode(
645 &test_header(),
646 &claims,
647 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
648 )
649 .unwrap();
650 let client = mock_client(|key| {
651 assert_eq!(key, TEST_KID);
652 Ok(Some(VALID_PUBLIC_KEY.to_string()))
653 });
654
655 let error = ValidationConfig::new([TEST_AUD])
657 .decode_and_validate(&token, &client)
658 .await
659 .unwrap_err();
660
661 assert!(
662 error.to_string().contains("Expired"),
663 "Token expired 31 seconds ago should be rejected, but got: {}",
664 error
665 );
666 }
667
668 #[tokio::test]
669 async fn test_decode_without_exp() {
670 let claims = test_claims();
671 let mut json = serde_json::to_value(&claims).unwrap();
672 json.as_object_mut().unwrap().remove("exp");
673 let token = jsonwebtoken::encode(
674 &test_header(),
675 &json,
676 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
677 )
678 .unwrap();
679 let client = mock_client(|key| {
680 assert_eq!(key, TEST_KID);
681 Ok(Some(VALID_PUBLIC_KEY.to_string()))
682 });
683 let error = ValidationConfig::new([TEST_AUD])
684 .decode_and_validate(&token, &client)
685 .await
686 .unwrap_err();
687 assert!(error.to_string().contains("missing field `exp`"));
688 }
689
690 #[tokio::test]
691 async fn test_decode_without_iss() {
692 let claims = test_claims();
693 let mut json = serde_json::to_value(&claims).unwrap();
694 json.as_object_mut().unwrap().remove("iss");
695 let token = jsonwebtoken::encode(
696 &test_header(),
697 &json,
698 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
699 )
700 .unwrap();
701 let client = mock_client(|key| {
702 assert_eq!(key, TEST_KID);
703 Ok(Some(VALID_PUBLIC_KEY.to_string()))
704 });
705 let error = ValidationConfig::new([TEST_AUD])
706 .decode_and_validate(&token, &client)
707 .await
708 .unwrap_err();
709 assert!(error.to_string().contains("missing field `iss`"));
710 }
711
712 #[tokio::test]
713 async fn test_decode_invalid_iss() {
714 let mut claims = test_claims();
715 claims.iss = "invalid-iss".to_string();
716 let token = jsonwebtoken::encode(
717 &test_header(),
718 &claims,
719 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
720 )
721 .unwrap();
722 let client = mock_client(|key| {
723 assert_eq!(key, TEST_KID);
724 Ok(Some(VALID_PUBLIC_KEY.to_string()))
725 });
726 let error = ValidationConfig::new([TEST_AUD])
727 .decode_and_validate(&token, &client)
728 .await
729 .unwrap_err();
730 assert_eq!(error.to_string(), "InvalidIssuer");
731 }
732
733 #[tokio::test]
734 async fn test_decode_validate_hd() {
735 let claims = test_claims();
736 let token = jsonwebtoken::encode(
737 &test_header(),
738 &claims,
739 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
740 )
741 .unwrap();
742 let client = mock_client(|key| {
743 assert_eq!(key, TEST_KID);
744 Ok(Some(VALID_PUBLIC_KEY.to_string()))
745 });
746 let error = ValidationConfig::new([TEST_AUD])
747 .with_google_hosted_domain(["another.example.com"])
748 .decode_and_validate(&token, &client)
749 .await
750 .unwrap_err();
751 assert_eq!(
752 error.to_string(),
753 "Invalid hosted domain: example.com, expected [\"another.example.com\"]"
754 );
755 }
756
757 #[tokio::test]
758 async fn test_decode_hd_not_found() {
759 let mut claims = test_claims();
760 claims.hd = None;
761 let token = jsonwebtoken::encode(
762 &test_header(),
763 &claims,
764 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
765 )
766 .unwrap();
767 let client = mock_client(|key| {
768 assert_eq!(key, TEST_KID);
769 Ok(Some(VALID_PUBLIC_KEY.to_string()))
770 });
771 let error = ValidationConfig::new([TEST_AUD])
772 .with_google_hosted_domain(["example.com"])
773 .decode_and_validate(&token, &client)
774 .await
775 .unwrap_err();
776 assert_eq!(
777 error.to_string(),
778 "hd claim is missing, expected [\"example.com\"]"
779 );
780 }
781
782 #[tokio::test]
783 async fn test_decode_validate_access_levels() {
784 let claims = test_claims();
785 let token = jsonwebtoken::encode(
786 &test_header(),
787 &claims,
788 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
789 )
790 .unwrap();
791 let client = mock_client(|key| {
792 assert_eq!(key, TEST_KID);
793 Ok(Some(VALID_PUBLIC_KEY.to_string()))
794 });
795 let error = ValidationConfig::new([TEST_AUD])
796 .with_access_levels(["EDITOR", "ADMIN"])
797 .decode_and_validate(&token, &client)
798 .await
799 .unwrap_err();
800 assert_eq!(
801 error.to_string(),
802 "Insufficient access level, expected ADMIN"
803 );
804 }
805
806 #[tokio::test]
807 async fn test_decode_validate_access_levels_missing() {
808 let claims = test_claims();
809 let mut json = serde_json::to_value(&claims).unwrap();
810 json.as_object_mut().unwrap().remove("google");
811 let token = jsonwebtoken::encode(
812 &test_header(),
813 &json,
814 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
815 )
816 .unwrap();
817 let client = mock_client(|key| {
818 assert_eq!(key, TEST_KID);
819 Ok(Some(VALID_PUBLIC_KEY.to_string()))
820 });
821 let error = ValidationConfig::new([TEST_AUD])
822 .with_access_levels(["EDITOR"])
823 .decode_and_validate(&token, &client)
824 .await
825 .unwrap_err();
826 assert_eq!(error.to_string(), "Access levels claim is missing");
827 }
828
829 #[tokio::test]
830 async fn test_decode_super_set_access_levels() {
831 let claims = test_claims();
832 let token = jsonwebtoken::encode(
833 &test_header(),
834 &claims,
835 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
836 )
837 .unwrap();
838 let client = mock_client(|key| {
839 assert_eq!(key, TEST_KID);
840 Ok(Some(VALID_PUBLIC_KEY.to_string()))
841 });
842 let decoded = ValidationConfig::new([TEST_AUD])
843 .with_google_hosted_domain(["example.com"])
844 .with_access_levels(["EDITOR"])
845 .decode_and_validate(&token, &client)
846 .await
847 .unwrap();
848 assert_eq!(decoded.sub, claims.sub);
849 }
850
851 #[tokio::test]
852 async fn test_decode_token_lifetime_too_long() {
853 let now = SystemTime::now()
854 .duration_since(std::time::UNIX_EPOCH)
855 .unwrap()
856 .as_secs();
857
858 let mut claims = test_claims();
859 claims.iat = now - 60;
860 claims.exp = claims.iat + 661;
861
862 let token = jsonwebtoken::encode(
863 &test_header(),
864 &claims,
865 &EncodingKey::from_ec_pem(VALID_PRIVATE_KEY.as_bytes()).unwrap(),
866 )
867 .unwrap();
868
869 let client = mock_client(|kid| {
870 assert_eq!(kid, TEST_KID);
871 Ok(Some(VALID_PUBLIC_KEY.to_string()))
872 });
873
874 let error = ValidationConfig::new([TEST_AUD])
875 .decode_and_validate(&token, &client)
876 .await
877 .unwrap_err();
878
879 match error {
880 Error::TokenLifetimeTooLong {
881 iat,
882 exp,
883 max_lifetime,
884 } => {
885 assert_eq!(iat, claims.iat);
886 assert_eq!(exp, claims.exp);
887 assert_eq!(max_lifetime, 600 + 2 * 30); }
889 _ => panic!("Expected TokenLifetimeTooLong error, got: {:?}", error),
890 }
891 }
892}