rs_firebase_admin_sdk/auth/token/
mod.rs

1#[cfg(test)]
2mod test;
3
4pub mod cache;
5pub mod crypto;
6pub mod error;
7pub mod jwt;
8
9use cache::KeyCache;
10use crypto::JwtRsaPubKey;
11use error::TokenVerificationError;
12use error_stack::{Report, ResultExt};
13use jwt::{JWTAlgorithm, JWToken};
14use std::future::Future;
15use time::{Duration, OffsetDateTime};
16
17const GOOGLE_ID_TOKEN_ISSUER_PREFIX: &str = "https://securetoken.google.com/";
18const GOOGLE_COOKIE_ISSUER_PREFIX: &str = "https://session.firebase.google.com/";
19
20#[cfg(feature = "tokens")]
21pub(crate) const GOOGLE_PUB_KEY_URI: &str =
22    "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com";
23#[cfg(feature = "tokens")]
24pub(crate) const GOOGLE_COOKIE_PUB_KEY_URI: &str =
25    "https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys";
26
27pub trait TokenVerifier: Sized + Sync + Send {
28    fn verify_token(
29        &self,
30        id_token: &str,
31    ) -> impl Future<Output = Result<JWToken, Report<TokenVerificationError>>> + Send;
32}
33
34pub struct EmulatedTokenVerifier {
35    _project_id: String,
36    _issuer: String,
37}
38
39impl EmulatedTokenVerifier {
40    pub fn new(project_id: String) -> Self {
41        Self {
42            _project_id: project_id.clone(),
43            _issuer: project_id,
44        }
45    }
46}
47
48impl TokenVerifier for EmulatedTokenVerifier {
49    async fn verify_token(
50        &self,
51        id_token: &str,
52    ) -> Result<JWToken, Report<TokenVerificationError>> {
53        let token = JWToken::from_encoded(id_token)
54            .change_context(TokenVerificationError::FailedParsing)?;
55
56        // TODO: implement claim checks for emulator
57
58        Ok(token)
59    }
60}
61
62pub struct LiveTokenVerifier<CacheT: KeyCache> {
63    project_id: String,
64    issuer: String,
65    key_cache: CacheT,
66}
67
68impl<CacheT: KeyCache + Send + Sync> TokenVerifier for LiveTokenVerifier<CacheT> {
69    async fn verify_token(
70        &self,
71        id_token: &str,
72    ) -> Result<JWToken, Report<TokenVerificationError>> {
73        let token = JWToken::from_encoded(id_token)
74            .change_context(TokenVerificationError::FailedParsing)?;
75
76        self.verify(&token).await?;
77
78        Ok(token)
79    }
80}
81
82impl<CacheT: KeyCache + Send + Sync> LiveTokenVerifier<CacheT> {
83    /// Create new ID token verifier
84    pub fn new_id_verifier(
85        project_id: String,
86        key_cache: CacheT,
87    ) -> Result<Self, Report<TokenVerificationError>> {
88        Ok(Self {
89            issuer: String::new() + GOOGLE_ID_TOKEN_ISSUER_PREFIX + &project_id,
90            project_id,
91            key_cache,
92        })
93    }
94
95    /// Create new cookie token verifier
96    pub fn new_cookie_verifier(
97        project_id: String,
98        key_cache: CacheT,
99    ) -> Result<Self, Report<TokenVerificationError>> {
100        Ok(Self {
101            issuer: String::new() + GOOGLE_COOKIE_ISSUER_PREFIX + &project_id,
102            project_id,
103            key_cache,
104        })
105    }
106
107    async fn verify_signature(
108        &self,
109        token: &JWToken,
110    ) -> Result<(), Report<TokenVerificationError>> {
111        let keys = self
112            .key_cache
113            .get_keys()
114            .await
115            .change_context(TokenVerificationError::FailedGettingKeys)?;
116
117        let key_id = token
118            .header
119            .kid
120            .as_ref()
121            .ok_or(TokenVerificationError::FailedGettingKeys)?;
122
123        let key = keys
124            .get(key_id)
125            .ok_or(Report::new(TokenVerificationError::InvalidSignatureKey))?;
126
127        let is_valid = key
128            .verify(token.payload.as_bytes(), &token.signature)
129            .change_context(TokenVerificationError::InvalidSignature)?;
130
131        if !is_valid {
132            return Err(Report::new(TokenVerificationError::InvalidSignature));
133        }
134
135        Ok(())
136    }
137
138    fn verify_header(&self, token: &JWToken) -> Result<(), Report<TokenVerificationError>> {
139        match token.header.alg {
140            JWTAlgorithm::RS256 => Ok(()),
141            _ => Err(Report::new(
142                TokenVerificationError::InvalidSignatureAlgorithm,
143            )),
144        }
145    }
146
147    fn verify_claims(&self, token: &JWToken) -> Result<(), Report<TokenVerificationError>> {
148        let now = OffsetDateTime::now_utc();
149
150        if token.critical_claims.exp <= now {
151            return Err(Report::new(TokenVerificationError::Expired));
152        }
153
154        // Firebase sometimes has wonky iat, pad with 10secs
155        if token.critical_claims.iat > now + Duration::seconds(10) {
156            return Err(Report::new(TokenVerificationError::IssuedInFuture));
157        }
158
159        if token.critical_claims.auth_time > now {
160            return Err(Report::new(TokenVerificationError::IssuedInFuture));
161        }
162
163        if token.critical_claims.aud != self.project_id {
164            return Err(Report::new(TokenVerificationError::InvalidAudience));
165        }
166
167        if token.critical_claims.iss != self.issuer {
168            return Err(Report::new(TokenVerificationError::InvalidIssuer));
169        }
170
171        if token.critical_claims.sub.is_empty() {
172            return Err(Report::new(TokenVerificationError::MissingSubject));
173        }
174
175        Ok(())
176    }
177
178    /// verify JWToken's attributes and signature
179    pub async fn verify(&self, token: &JWToken) -> Result<(), Report<TokenVerificationError>> {
180        self.verify_header(token)?;
181        self.verify_claims(token)?;
182        self.verify_signature(token).await
183    }
184}