Skip to main content

firebase_admin/core/
credentials.rs

1//! Service account credential loading.
2
3use crate::core::error::CoreError;
4use serde::Deserialize;
5
6/// A parsed Firebase/GCP service account key file (JSON).
7///
8/// The `Debug` implementation deliberately redacts [`Self::private_key`] so
9/// that logging or panicking with a `ServiceAccountKey` in scope cannot leak
10/// the private key into logs, error reports, or terminal output.
11#[derive(Clone, Deserialize)]
12pub struct ServiceAccountKey {
13    /// The service account's client email address.
14    pub client_email: String,
15    /// The PEM-encoded RSA private key used to sign tokens.
16    pub private_key: String,
17    /// The GCP project ID this service account belongs to.
18    pub project_id: String,
19    /// Unique key identifier assigned by Google, used as the JWT `kid` header.
20    pub private_key_id: String,
21}
22
23impl std::fmt::Debug for ServiceAccountKey {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("ServiceAccountKey")
26            .field("client_email", &self.client_email)
27            .field("private_key", &"[redacted]")
28            .field("project_id", &self.project_id)
29            .field("private_key_id", &self.private_key_id)
30            .finish()
31    }
32}
33
34impl ServiceAccountKey {
35    /// Parses a service account key from its raw JSON representation.
36    pub fn from_json(json: &str) -> Result<Self, CoreError> {
37        serde_json::from_str(json).map_err(CoreError::Deserialize)
38    }
39
40    /// Reads and parses a service account key from a file path.
41    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, CoreError> {
42        let contents = std::fs::read_to_string(path)
43            .map_err(|e| CoreError::Credentials(format!("failed to read key file: {e}")))?;
44        Self::from_json(&contents)
45    }
46}
47
48/// The source of credentials an [`crate::auth::AuthClientBuilder`] should use.
49#[derive(Debug, Clone)]
50pub enum Credentials {
51    /// A explicitly-provided service account key.
52    ServiceAccount(Box<ServiceAccountKey>),
53    /// Application Default Credentials, resolved at request time.
54    #[cfg(any(feature = "live-user-management", feature = "live-messaging"))]
55    ApplicationDefault,
56    /// No credentials; only valid when talking to the Firebase emulator.
57    Emulator,
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    const VALID_KEY_JSON: &str = r#"{
65        "client_email": "test@test-project.iam.gserviceaccount.com",
66        "private_key": "-----BEGIN PRIVATE KEY-----\nMIIB\n-----END PRIVATE KEY-----\n",
67        "project_id": "test-project",
68        "private_key_id": "abc123"
69    }"#;
70
71    #[test]
72    fn parses_a_valid_key_from_json() {
73        let key = ServiceAccountKey::from_json(VALID_KEY_JSON).unwrap();
74        assert_eq!(
75            key.client_email,
76            "test@test-project.iam.gserviceaccount.com"
77        );
78        assert_eq!(key.project_id, "test-project");
79        assert_eq!(key.private_key_id, "abc123");
80    }
81
82    #[test]
83    fn rejects_malformed_json() {
84        let err = ServiceAccountKey::from_json("not json").unwrap_err();
85        assert!(matches!(err, CoreError::Deserialize(_)));
86    }
87
88    #[test]
89    fn rejects_json_missing_required_fields() {
90        let err = ServiceAccountKey::from_json(r#"{"client_email": "only-this@example.com"}"#)
91            .unwrap_err();
92        assert!(matches!(err, CoreError::Deserialize(_)));
93    }
94
95    #[test]
96    fn from_file_surfaces_a_credentials_error_for_a_missing_path() {
97        let err = ServiceAccountKey::from_file("/does/not/exist.json").unwrap_err();
98        assert!(matches!(err, CoreError::Credentials(_)));
99    }
100
101    #[test]
102    fn debug_output_redacts_the_private_key() {
103        let key = ServiceAccountKey::from_json(VALID_KEY_JSON).unwrap();
104        let debug_output = format!("{key:?}");
105        assert!(!debug_output.contains("BEGIN PRIVATE KEY"));
106        assert!(debug_output.contains("[redacted]"));
107        assert!(debug_output.contains("test@test-project.iam.gserviceaccount.com"));
108    }
109}