Skip to main content

iap_jwt/
lib.rs

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/// The claims in a JWT issued by Google IAP.
14#[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/// The error returned by the `decode_and_validate` method.
29#[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
71/// Configures validation options for JWT issued by Google IAP.
72///
73/// # Validation Options
74///
75/// - By default, validates the audience claim against the provided list.
76/// - `with_google_hosted_domain`: Additionally validates the `hd` (hosted domain) claim.
77/// - `with_access_levels`: Additionally validates the access levels claim in the Google-specific payload.
78pub struct ValidationConfig {
79    audience: Vec<String>,
80    /// "If an account belongs to a hosted domain, the hd claim is provided to differentiate the domain the account is associated with." - https://cloud.google.com/iap/docs/signed-headers-howto
81    google_hosted_domain: Option<Vec<String>>,
82    access_levels: Option<Vec<String>>,
83    /// Time skew tolerance in seconds
84    skew: u64,
85    /// Maximum token lifetime in seconds
86    max_token_lifetime: u64,
87}
88
89impl ValidationConfig {
90    /// Creates a new validation config with the given audience.
91    ///
92    /// By default, validates the audience claim against the provided list.
93    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,                         // Default value: 30 seconds
103            max_token_lifetime: 600 + 2 * 30, // Default value: 10 minutes + 2 * skew
104        }
105    }
106
107    /// Validates that the hd claim is in the list of google hosted domains.
108    ///
109    /// "If an account belongs to a hosted domain, the hd claim is provided to differentiate the domain the account is associated with." - https://cloud.google.com/iap/docs/signed-headers-howto
110    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    /// Validates that the access levels claim contains all the access levels in the config.
121    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    /// Decode and validate a jwt with respect to the IAP documentation: https://cloud.google.com/iap/docs/signed-headers-howto
130    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        // Validate iat (considering skew)
160        if token.claims.iat > now + self.skew {
161            return Err(Error::FutureIat {
162                actual: token.claims.iat,
163                expected: now,
164            });
165        }
166
167        // Validate maximum token lifetime
168        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, // 10 minutes after iat (maximum is 10 minutes + 2*skew)
275            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    /// Test that tokens with exp at the skew boundary are accepted.
596    /// According to IAP documentation: "exp - Must be in the future. Allow 30 seconds for skew."
597    #[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        // Set exp to 30 seconds in the past - at the 30 second skew boundary
606        claims.iat = now - 600; // issued 10 minutes ago
607        claims.exp = now - 30; // expired 30 seconds ago, at 30s skew boundary
608
609        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        // This should succeed because exp is at the 30 second skew boundary
621        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    /// Test that tokens with exp beyond the skew tolerance are rejected.
630    /// According to IAP documentation: "exp - Must be in the future. Allow 30 seconds for skew."
631    /// A token that expired 31 seconds ago should be rejected (beyond 30s skew).
632    #[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        // Set exp to 31 seconds in the past - beyond the 30 second skew tolerance
641        claims.iat = now - 600; // issued 10 minutes ago
642        claims.exp = now - 31; // expired 31 seconds ago, beyond 30s skew
643
644        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        // This should fail because exp is beyond the 30 second skew tolerance
656        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); // 10 minutes + 2 * skew(30 seconds)
888            }
889            _ => panic!("Expected TokenLifetimeTooLong error, got: {:?}", error),
890        }
891    }
892}