firebase_rs_sdk/app_check/
token_provider.rs

1use std::sync::atomic::{AtomicBool, Ordering};
2
3use crate::app_check::errors::AppCheckError;
4use crate::app_check::FirebaseAppCheckInternal;
5use crate::firestore::error::{
6    internal_error, invalid_argument, unauthenticated, unavailable, FirestoreError, FirestoreResult,
7};
8use crate::firestore::remote::datastore::{TokenProvider, TokenProviderArc};
9
10/// Bridges App Check token retrieval into Firestore's [`TokenProvider`] trait.
11pub struct AppCheckTokenProvider {
12    app_check: FirebaseAppCheckInternal,
13    force_refresh: AtomicBool,
14}
15
16impl AppCheckTokenProvider {
17    /// Creates a new provider backed by the given App Check instance.
18    pub fn new(app_check: FirebaseAppCheckInternal) -> Self {
19        Self {
20            app_check,
21            force_refresh: AtomicBool::new(false),
22        }
23    }
24
25    /// Converts the provider into a reference-counted [`TokenProviderArc`].
26    pub fn into_arc(self) -> TokenProviderArc {
27        std::sync::Arc::new(self)
28    }
29}
30
31/// Convenience helper to expose an App Check instance as a [`TokenProviderArc`].
32pub fn app_check_token_provider_arc(app_check: FirebaseAppCheckInternal) -> TokenProviderArc {
33    AppCheckTokenProvider::new(app_check).into_arc()
34}
35
36impl Clone for AppCheckTokenProvider {
37    fn clone(&self) -> Self {
38        Self {
39            app_check: self.app_check.clone(),
40            force_refresh: AtomicBool::new(self.force_refresh.load(Ordering::SeqCst)),
41        }
42    }
43}
44
45impl TokenProvider for AppCheckTokenProvider {
46    fn get_token(&self) -> FirestoreResult<Option<String>> {
47        let force_refresh = self.force_refresh.swap(false, Ordering::SeqCst);
48        let result = self
49            .app_check
50            .get_token(force_refresh)
51            .map_err(map_app_check_error)?;
52
53        if let Some(error) = result.error {
54            return Err(map_app_check_error(error));
55        }
56
57        if let Some(error) = result.internal_error {
58            return Err(internal_error(error.to_string()));
59        }
60
61        if result.token.is_empty() {
62            Ok(None)
63        } else {
64            Ok(Some(result.token))
65        }
66    }
67
68    fn invalidate_token(&self) {
69        self.force_refresh.store(true, Ordering::SeqCst);
70    }
71}
72
73fn map_app_check_error(error: AppCheckError) -> FirestoreError {
74    match error {
75        AppCheckError::AlreadyInitialized { .. }
76        | AppCheckError::UseBeforeActivation { .. }
77        | AppCheckError::InvalidConfiguration { .. } => invalid_argument(error.to_string()),
78        AppCheckError::TokenFetchFailed { .. } | AppCheckError::ProviderError { .. } => {
79            unavailable(error.to_string())
80        }
81        AppCheckError::TokenExpired => unauthenticated(error.to_string()),
82        AppCheckError::Internal(message) => internal_error(message),
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::app::{FirebaseApp, FirebaseAppConfig, FirebaseOptions};
90    use crate::app_check::api::{initialize_app_check, token_with_ttl};
91    use crate::app_check::types::{AppCheckOptions, AppCheckProvider, AppCheckToken};
92    use crate::component::ComponentContainer;
93    use std::sync::Arc;
94    use std::time::Duration;
95
96    #[derive(Clone)]
97    struct StaticTokenProvider {
98        token: String,
99    }
100
101    impl AppCheckProvider for StaticTokenProvider {
102        fn get_token(&self) -> crate::app_check::AppCheckResult<AppCheckToken> {
103            token_with_ttl(self.token.clone(), Duration::from_secs(60))
104        }
105    }
106
107    #[derive(Clone)]
108    struct ErrorProvider;
109
110    impl AppCheckProvider for ErrorProvider {
111        fn get_token(&self) -> crate::app_check::AppCheckResult<AppCheckToken> {
112            Err(AppCheckError::TokenFetchFailed {
113                message: "network".into(),
114            })
115        }
116    }
117
118    fn test_app(name: &str) -> FirebaseApp {
119        FirebaseApp::new(
120            FirebaseOptions::default(),
121            FirebaseAppConfig::new(name.to_owned(), false),
122            ComponentContainer::new(name.to_owned()),
123        )
124    }
125
126    #[test]
127    fn returns_token_string() {
128        let provider = Arc::new(StaticTokenProvider {
129            token: "app-check-123".into(),
130        });
131        let options = AppCheckOptions::new(provider);
132        let app_check = initialize_app_check(Some(test_app("app-check-ok")), options).unwrap();
133        let internal = FirebaseAppCheckInternal::new(app_check);
134        let provider = AppCheckTokenProvider::new(internal);
135
136        let token = provider.get_token().unwrap();
137        assert_eq!(token.as_deref(), Some("app-check-123"));
138    }
139
140    #[test]
141    fn propagates_errors() {
142        let provider = Arc::new(ErrorProvider);
143        let options = AppCheckOptions::new(provider);
144        let app_check = initialize_app_check(Some(test_app("app-check-err")), options).unwrap();
145        let internal = FirebaseAppCheckInternal::new(app_check);
146        let provider = AppCheckTokenProvider::new(internal);
147
148        let error = provider.get_token().unwrap_err();
149        assert_eq!(
150            error.code,
151            crate::firestore::error::FirestoreErrorCode::Unavailable
152        );
153    }
154}