firestore_db_and_auth/
jwt.rs

1//! # A Firestore Auth Session token is a Javascript Web Token (JWT). This module contains JWT helper functions.
2
3use super::credentials::Credentials;
4
5use serde::{Deserialize, Serialize};
6
7use chrono::{Duration, Utc};
8use std::collections::HashSet;
9use std::ops::Add;
10use std::slice::Iter;
11
12use crate::errors::FirebaseError;
13use biscuit::jwa::SignatureAlgorithm;
14use biscuit::{ClaimPresenceOptions, SingleOrMultiple, ValidationOptions};
15use cache_control::CacheControl;
16use std::ops::Deref;
17
18type Error = super::errors::FirebaseError;
19
20pub static JWT_AUDIENCE_FIRESTORE: &str = "https://firestore.googleapis.com/google.firestore.v1.Firestore";
21pub static JWT_AUDIENCE_IDENTITY: &str =
22    "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
23
24#[derive(Debug, Serialize, Deserialize, Clone, Default)]
25pub struct JwtOAuthPrivateClaims {
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub scope: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub client_id: Option<String>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub uid: Option<String>, // Probably the firebase User ID if set
32}
33
34pub(crate) type AuthClaimsJWT = biscuit::JWT<JwtOAuthPrivateClaims, biscuit::Empty>;
35
36#[derive(Serialize, Deserialize, Default, Clone)]
37pub struct JWSEntry {
38    #[serde(flatten)]
39    pub(crate) headers: biscuit::jws::RegisteredHeader,
40    #[serde(flatten)]
41    pub(crate) ne: biscuit::jwk::RSAKeyParameters,
42}
43
44#[derive(Serialize, Deserialize, Default, Clone)]
45pub struct JWKSet {
46    pub keys: Vec<JWSEntry>,
47}
48
49impl JWKSet {
50    /// Create a new JWKSetDTO instance from a given json string
51    /// You can use [`Credentials::add_jwks_public_keys`] to manually add more public keys later on.
52    /// You need two JWKS files for this crate to work:
53    /// * https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com
54    /// * https://www.googleapis.com/service_accounts/v1/jwk/{your-service-account-email}
55    pub fn new(jwk_content: &str) -> Result<JWKSet, Error> {
56        let jwk_set: JWKSet = serde_json::from_str(jwk_content).map_err(|e| FirebaseError::Ser {
57            doc: Option::from(format!("Failed to parse jwkset. Return value: {}", jwk_content)),
58            ser: e,
59        })?;
60        Ok(jwk_set)
61    }
62}
63
64/// Download the Google JWK Set for a given service account.
65/// Returns the JWKS alongside the maximum time the JWKS is valid for.
66/// The resulting set of JWKs need to be added to a credentials object
67/// for jwk verifications.
68pub async fn download_google_jwks(account_mail: &str) -> Result<(String, Option<Duration>), Error> {
69    let url = format!("https://www.googleapis.com/service_accounts/v1/jwk/{}", account_mail);
70    let resp = reqwest::Client::new().get(&url).send().await?;
71    let max_age = resp
72        .headers()
73        .get("cache-control")
74        .and_then(|cache_control| cache_control.to_str().ok())
75        .and_then(|cache_control| CacheControl::from_value(cache_control))
76        .and_then(|cache_control| cache_control.max_age)
77        .and_then(|max_age| Duration::from_std(max_age).ok());
78
79    let text = resp.text().await?;
80    Ok((text, max_age))
81}
82
83pub(crate) async fn create_jwt_encoded<S: AsRef<str>>(
84    credentials: &Credentials,
85    scope: Option<Iter<'_, S>>,
86    duration: chrono::Duration,
87    client_id: Option<String>,
88    user_id: Option<String>,
89    audience: &str,
90) -> Result<String, Error> {
91    let jwt = create_jwt(credentials, scope, duration, client_id, user_id, audience)?;
92    let secret_lock = credentials.keys.read().await;
93    let secret = secret_lock
94        .secret
95        .as_ref()
96        .ok_or(Error::Generic("No private key added via add_keypair_key!"))?;
97    Ok(jwt.encode(&secret.deref())?.encoded()?.encode())
98}
99
100/// Returns true if the access token (assumed to be a jwt) has expired
101///
102/// An error is returned if the given access token string is not a jwt
103pub(crate) fn is_expired(access_token: &str, tolerance_in_minutes: i64) -> Result<bool, FirebaseError> {
104    let token = AuthClaimsJWT::new_encoded(&access_token);
105    let claims = token.unverified_payload()?;
106    if let Some(expiry) = claims.registered.expiry.as_ref() {
107        let diff: Duration = Utc::now().signed_duration_since(expiry.deref().clone());
108        return Ok(diff.num_minutes() - tolerance_in_minutes > 0);
109    }
110
111    Ok(true)
112}
113
114/// Returns true if the jwt was updated and needs signing
115pub(crate) fn jwt_update_expiry_if(jwt: &mut AuthClaimsJWT, expire_in_minutes: i64) -> bool {
116    let ref mut claims = jwt.payload_mut().unwrap().registered;
117
118    let now = biscuit::Timestamp::from(Utc::now());
119    let now_plus_hour = biscuit::Timestamp::from(Utc::now().add(Duration::hours(1)));
120
121    if let Some(issued_at) = claims.issued_at.as_ref() {
122        let diff: Duration = Utc::now().signed_duration_since(issued_at.deref().clone());
123        if diff.num_minutes() > expire_in_minutes {
124            claims.issued_at = Some(now);
125            claims.expiry = Some(now_plus_hour);
126        } else {
127            return false;
128        }
129    } else {
130        claims.issued_at = Some(now);
131        claims.expiry = Some(now_plus_hour);
132    }
133
134    true
135}
136
137pub(crate) fn create_jwt<S>(
138    credentials: &Credentials,
139    scope: Option<Iter<S>>,
140    duration: chrono::Duration,
141    client_id: Option<String>,
142    user_id: Option<String>,
143    audience: &str,
144) -> Result<AuthClaimsJWT, Error>
145where
146    S: AsRef<str>,
147{
148    use biscuit::{
149        jws::{Header, RegisteredHeader},
150        ClaimsSet, Empty, RegisteredClaims, JWT,
151    };
152
153    let header: Header<Empty> = Header::from(RegisteredHeader {
154        algorithm: SignatureAlgorithm::RS256,
155        key_id: Some(credentials.private_key_id.to_owned()),
156        ..Default::default()
157    });
158    let expected_claims = ClaimsSet::<JwtOAuthPrivateClaims> {
159        registered: RegisteredClaims {
160            issuer: Some(credentials.client_email.clone()),
161            audience: Some(SingleOrMultiple::Single(audience.to_string())),
162            subject: Some(credentials.client_email.clone()),
163            expiry: Some(biscuit::Timestamp::from(Utc::now().add(duration))),
164            issued_at: Some(biscuit::Timestamp::from(Utc::now())),
165            ..Default::default()
166        },
167        private: JwtOAuthPrivateClaims {
168            scope: scope.and_then(|f| {
169                Some(f.fold(String::new(), |acc, x| {
170                    let x: &str = x.as_ref();
171                    return acc + x + " ";
172                }))
173            }),
174            client_id,
175            uid: user_id,
176        },
177    };
178    Ok(JWT::new_decoded(header, expected_claims))
179}
180
181#[derive(Debug)]
182pub struct TokenValidationResult {
183    pub claims: JwtOAuthPrivateClaims,
184    pub audience: String,
185    pub subject: String,
186}
187
188impl TokenValidationResult {
189    pub fn get_scopes(&self) -> HashSet<String> {
190        match self.claims.scope {
191            Some(ref v) => v.split(" ").map(|f| f.to_owned()).collect(),
192            None => HashSet::new(),
193        }
194    }
195}
196
197pub(crate) async fn verify_access_token(
198    credentials: &Credentials,
199    access_token: &str,
200) -> Result<TokenValidationResult, Error> {
201    let token = AuthClaimsJWT::new_encoded(&access_token);
202
203    let header = token.unverified_header()?;
204    let kid = header
205        .registered
206        .key_id
207        .as_ref()
208        .ok_or(FirebaseError::Generic("No jwt kid"))?;
209    let secret = credentials
210        .decode_secret(kid)
211        .await?
212        .ok_or(FirebaseError::Generic("No secret for kid"))?;
213
214    let token = token.into_decoded(&secret.deref(), SignatureAlgorithm::RS256)?;
215
216    use biscuit::Presence::*;
217
218    let o = ValidationOptions {
219        claim_presence_options: ClaimPresenceOptions {
220            issued_at: Required,
221            not_before: Optional,
222            expiry: Required,
223            issuer: Required,
224            audience: Required,
225            subject: Required,
226            id: Optional,
227        },
228        // audience: Validation::Validate(StringOrUri::from_str(JWT_SUBJECT)?),
229        ..Default::default()
230    };
231
232    let claims = token.payload()?;
233    claims.registered.validate(o)?;
234
235    let audience = match claims.registered.audience.as_ref().unwrap() {
236        SingleOrMultiple::Single(v) => v.to_string(),
237        SingleOrMultiple::Multiple(v) => v.get(0).unwrap().to_string(),
238    };
239
240    Ok(TokenValidationResult {
241        claims: claims.private.clone(),
242        subject: claims.registered.subject.as_ref().unwrap().to_string(),
243        audience,
244    })
245}
246
247pub mod session_cookie {
248    use super::*;
249    use std::ops::Add;
250
251    pub(crate) async fn create_jwt_encoded(
252        credentials: &Credentials,
253        duration: chrono::Duration,
254    ) -> Result<String, Error> {
255        let scope = [
256            "https://www.googleapis.com/auth/cloud-platform",
257            "https://www.googleapis.com/auth/firebase.database",
258            "https://www.googleapis.com/auth/firebase.messaging",
259            "https://www.googleapis.com/auth/identitytoolkit",
260            "https://www.googleapis.com/auth/userinfo.email",
261        ];
262
263        const AUDIENCE: &str = "https://accounts.google.com/o/oauth2/token";
264
265        use biscuit::{
266            jws::{Header, RegisteredHeader},
267            ClaimsSet, Empty, RegisteredClaims, JWT,
268        };
269
270        let header: Header<Empty> = Header::from(RegisteredHeader {
271            algorithm: SignatureAlgorithm::RS256,
272            key_id: Some(credentials.private_key_id.to_owned()),
273            ..Default::default()
274        });
275        let expected_claims = ClaimsSet::<JwtOAuthPrivateClaims> {
276            registered: RegisteredClaims {
277                issuer: Some(credentials.client_email.clone()),
278                audience: Some(SingleOrMultiple::Single(AUDIENCE.to_string())),
279                subject: Some(credentials.client_email.clone()),
280                expiry: Some(biscuit::Timestamp::from(Utc::now().add(duration))),
281                issued_at: Some(biscuit::Timestamp::from(Utc::now())),
282                ..Default::default()
283            },
284            private: JwtOAuthPrivateClaims {
285                scope: Some(scope.join(" ")),
286                client_id: None,
287                uid: None,
288            },
289        };
290        let jwt = JWT::new_decoded(header, expected_claims);
291
292        let secret_lock = credentials.keys.read().await;
293        let secret = secret_lock
294            .secret
295            .as_ref()
296            .ok_or(Error::Generic("No private key added via add_keypair_key!"))?;
297        Ok(jwt.encode(&secret.deref())?.encoded()?.encode())
298    }
299}