firebase_admin/auth/id_token/
verifier.rs1use crate::auth::error::TokenVerificationError;
7use crate::auth::id_token::claims::IdTokenClaims;
8use crate::auth::id_token::jwks::JwksCache;
9use crate::core::ProjectId;
10use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
11
12pub struct IdTokenVerifier {
15 project_id: ProjectId,
16 jwks: JwksCache,
17}
18
19impl IdTokenVerifier {
20 pub fn new(project_id: ProjectId, jwks: JwksCache) -> Self {
22 Self { project_id, jwks }
23 }
24
25 pub async fn verify(&self, token: &str) -> Result<IdTokenClaims, TokenVerificationError> {
28 let header = decode_header(token)?;
29
30 if header.alg != Algorithm::RS256 {
31 return Err(TokenVerificationError::InvalidSignature);
32 }
33
34 let kid = header.kid.ok_or(TokenVerificationError::InvalidSignature)?;
35 let (n, e) = self.jwks.public_key(&kid).await?;
36 let decoding_key = DecodingKey::from_rsa_components(&n, &e)
37 .map_err(|_| TokenVerificationError::InvalidSignature)?;
38
39 verify_with_key(
40 token,
41 &decoding_key,
42 self.project_id.as_str(),
43 "https://securetoken.google.com/",
44 )
45 }
46}
47
48pub(crate) fn verify_with_key(
60 token: &str,
61 decoding_key: &DecodingKey,
62 project_id: &str,
63 issuer_prefix: &str,
64) -> Result<IdTokenClaims, TokenVerificationError> {
65 let expected_issuer = format!("{issuer_prefix}{project_id}");
66 let mut validation = Validation::new(Algorithm::RS256);
67 validation.set_audience(&[project_id]);
68 validation.set_issuer(&[expected_issuer]);
69 validation.validate_exp = true;
70
71 let token_data =
72 decode::<IdTokenClaims>(token, decoding_key, &validation).map_err(|e| match e.kind() {
73 jsonwebtoken::errors::ErrorKind::ExpiredSignature => TokenVerificationError::Expired,
74 jsonwebtoken::errors::ErrorKind::InvalidAudience => {
75 TokenVerificationError::AudienceMismatch
76 }
77 jsonwebtoken::errors::ErrorKind::InvalidIssuer => {
78 TokenVerificationError::IssuerMismatch
79 }
80 jsonwebtoken::errors::ErrorKind::InvalidSignature => {
81 TokenVerificationError::InvalidSignature
82 }
83 _ => TokenVerificationError::Malformed(e),
84 })?;
85
86 let claims = token_data.claims;
87
88 if claims.sub.trim().is_empty() {
89 return Err(TokenVerificationError::MissingSubject);
90 }
91
92 let now = std::time::SystemTime::now()
93 .duration_since(std::time::UNIX_EPOCH)
94 .expect("system clock is before the Unix epoch")
95 .as_secs() as i64;
96
97 if claims.auth_time > now {
98 return Err(TokenVerificationError::NotYetValid);
99 }
100
101 Ok(claims)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use jsonwebtoken::{encode, EncodingKey, Header};
108 use serde_json::json;
109
110 const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../tests/fixtures/test_private_key.pem");
111 const TEST_PROJECT_ID: &str = "test-project";
112
113 fn sign(claims: &serde_json::Value, alg: Algorithm) -> String {
114 let mut header = Header::new(alg);
115 header.kid = Some("test-key".to_string());
116 let key = EncodingKey::from_rsa_pem(TEST_PRIVATE_KEY_PEM.as_bytes()).unwrap();
117 encode(&header, claims, &key).unwrap()
118 }
119
120 fn decoding_key() -> DecodingKey {
121 DecodingKey::from_rsa_pem(include_bytes!(
122 "../../../tests/fixtures/test_public_key.pem"
123 ))
124 .unwrap()
125 }
126
127 fn valid_claims(now: i64) -> serde_json::Value {
128 json!({
129 "iss": format!("https://securetoken.google.com/{TEST_PROJECT_ID}"),
130 "aud": TEST_PROJECT_ID,
131 "iat": now - 10,
132 "exp": now + 3600,
133 "auth_time": now - 10,
134 "sub": "test-uid",
135 })
136 }
137
138 fn now() -> i64 {
139 std::time::SystemTime::now()
140 .duration_since(std::time::UNIX_EPOCH)
141 .unwrap()
142 .as_secs() as i64
143 }
144
145 #[test]
146 fn accepts_a_valid_token() {
147 let token = sign(&valid_claims(now()), Algorithm::RS256);
148 let claims = verify_with_key(
149 &token,
150 &decoding_key(),
151 TEST_PROJECT_ID,
152 "https://securetoken.google.com/",
153 )
154 .unwrap();
155 assert_eq!(claims.sub, "test-uid");
156 }
157
158 #[test]
159 fn rejects_expired_token() {
160 let mut claims = valid_claims(now());
161 claims["exp"] = json!(now() - 100);
162 let token = sign(&claims, Algorithm::RS256);
163 let err = verify_with_key(
164 &token,
165 &decoding_key(),
166 TEST_PROJECT_ID,
167 "https://securetoken.google.com/",
168 )
169 .unwrap_err();
170 assert!(matches!(err, TokenVerificationError::Expired));
171 }
172
173 #[test]
174 fn rejects_wrong_audience() {
175 let mut claims = valid_claims(now());
176 claims["aud"] = json!("some-other-project");
177 let token = sign(&claims, Algorithm::RS256);
178 let err = verify_with_key(
179 &token,
180 &decoding_key(),
181 TEST_PROJECT_ID,
182 "https://securetoken.google.com/",
183 )
184 .unwrap_err();
185 assert!(matches!(err, TokenVerificationError::AudienceMismatch));
186 }
187
188 #[test]
189 fn rejects_wrong_issuer() {
190 let mut claims = valid_claims(now());
191 claims["iss"] = json!("https://securetoken.google.com/some-other-project");
192 let token = sign(&claims, Algorithm::RS256);
193 let err = verify_with_key(
194 &token,
195 &decoding_key(),
196 TEST_PROJECT_ID,
197 "https://securetoken.google.com/",
198 )
199 .unwrap_err();
200 assert!(matches!(err, TokenVerificationError::IssuerMismatch));
201 }
202
203 #[test]
204 fn rejects_missing_subject() {
205 let mut claims = valid_claims(now());
206 claims["sub"] = json!("");
207 let token = sign(&claims, Algorithm::RS256);
208 let err = verify_with_key(
209 &token,
210 &decoding_key(),
211 TEST_PROJECT_ID,
212 "https://securetoken.google.com/",
213 )
214 .unwrap_err();
215 assert!(matches!(err, TokenVerificationError::MissingSubject));
216 }
217
218 #[test]
219 fn rejects_not_yet_valid_auth_time() {
220 let mut claims = valid_claims(now());
221 claims["auth_time"] = json!(now() + 3600);
222 let token = sign(&claims, Algorithm::RS256);
223 let err = verify_with_key(
224 &token,
225 &decoding_key(),
226 TEST_PROJECT_ID,
227 "https://securetoken.google.com/",
228 )
229 .unwrap_err();
230 assert!(matches!(err, TokenVerificationError::NotYetValid));
231 }
232
233 #[test]
234 fn rejects_tampered_signature() {
235 let token = sign(&valid_claims(now()), Algorithm::RS256);
236 let mut parts: Vec<&str> = token.split('.').collect();
237 let tampered_payload = parts[1]
238 .chars()
239 .map(|c| if c == 'A' { 'B' } else { 'A' })
240 .collect::<String>();
241 parts[1] = &tampered_payload;
242 let tampered = parts.join(".");
243 let err = verify_with_key(
244 &tampered,
245 &decoding_key(),
246 TEST_PROJECT_ID,
247 "https://securetoken.google.com/",
248 )
249 .unwrap_err();
250 assert!(matches!(
251 err,
252 TokenVerificationError::InvalidSignature | TokenVerificationError::Malformed(_)
253 ));
254 }
255}