firebase_admin/auth/custom_token/
signer.rs1use crate::core::ServiceAccountKey;
8use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
9use serde::Serialize;
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12const CUSTOM_TOKEN_AUDIENCE: &str =
13 "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit";
14const CUSTOM_TOKEN_TTL: Duration = Duration::from_secs(60 * 60);
15
16#[derive(Debug, Serialize)]
17struct CustomTokenClaims {
18 iss: String,
19 sub: String,
20 aud: String,
21 iat: i64,
22 exp: i64,
23 uid: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 claims: Option<serde_json::Map<String, serde_json::Value>>,
26}
27
28pub struct CustomTokenSigner {
30 service_account: ServiceAccountKey,
31}
32
33impl CustomTokenSigner {
34 pub fn new(service_account: ServiceAccountKey) -> Self {
36 Self { service_account }
37 }
38
39 pub fn create_custom_token(
42 &self,
43 uid: &str,
44 claims: Option<serde_json::Map<String, serde_json::Value>>,
45 ) -> Result<String, jsonwebtoken::errors::Error> {
46 let now = SystemTime::now()
47 .duration_since(UNIX_EPOCH)
48 .expect("system clock is before the Unix epoch")
49 .as_secs() as i64;
50
51 let payload = CustomTokenClaims {
52 iss: self.service_account.client_email.clone(),
53 sub: self.service_account.client_email.clone(),
54 aud: CUSTOM_TOKEN_AUDIENCE.to_string(),
55 iat: now,
56 exp: now + CUSTOM_TOKEN_TTL.as_secs() as i64,
57 uid: uid.to_string(),
58 claims,
59 };
60
61 let mut header = Header::new(Algorithm::RS256);
62 header.kid = Some(self.service_account.private_key_id.clone());
63
64 let encoding_key = EncodingKey::from_rsa_pem(self.service_account.private_key.as_bytes())?;
65
66 encode(&header, &payload, &encoding_key)
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use jsonwebtoken::{decode, decode_header, DecodingKey, Validation};
74 use serde::Deserialize;
75
76 const TEST_PRIVATE_KEY_PEM: &str = include_str!("../../../tests/fixtures/test_private_key.pem");
77 const TEST_PUBLIC_KEY_PEM: &[u8] =
78 include_bytes!("../../../tests/fixtures/test_public_key.pem");
79
80 #[derive(Debug, Deserialize)]
81 struct DecodedClaims {
82 iss: String,
83 sub: String,
84 aud: String,
85 iat: i64,
86 exp: i64,
87 uid: String,
88 claims: Option<serde_json::Map<String, serde_json::Value>>,
89 }
90
91 fn test_service_account() -> ServiceAccountKey {
92 ServiceAccountKey {
93 client_email: "test@test-project.iam.gserviceaccount.com".to_string(),
94 private_key: TEST_PRIVATE_KEY_PEM.to_string(),
95 project_id: "test-project".to_string(),
96 private_key_id: "test-key-id".to_string(),
97 }
98 }
99
100 fn independently_decode(token: &str) -> DecodedClaims {
105 let header = decode_header(token).unwrap();
106 assert_eq!(header.alg, Algorithm::RS256);
107 assert_eq!(header.kid.as_deref(), Some("test-key-id"));
108
109 let decoding_key = DecodingKey::from_rsa_pem(TEST_PUBLIC_KEY_PEM).unwrap();
110 let mut validation = Validation::new(Algorithm::RS256);
111 validation.set_audience(&[CUSTOM_TOKEN_AUDIENCE]);
112 validation.set_issuer(&["test@test-project.iam.gserviceaccount.com"]);
113 validation.validate_exp = false; decode::<DecodedClaims>(token, &decoding_key, &validation)
116 .unwrap()
117 .claims
118 }
119
120 #[test]
121 fn creates_a_token_with_the_documented_claim_shape() {
122 let signer = CustomTokenSigner::new(test_service_account());
123 let token = signer.create_custom_token("some-uid", None).unwrap();
124
125 let claims = independently_decode(&token);
126 assert_eq!(claims.uid, "some-uid");
127 assert_eq!(claims.iss, "test@test-project.iam.gserviceaccount.com");
128 assert_eq!(claims.sub, "test@test-project.iam.gserviceaccount.com");
129 assert_eq!(claims.aud, CUSTOM_TOKEN_AUDIENCE);
130 assert!(claims.exp > claims.iat);
131 assert!(claims.exp - claims.iat <= CUSTOM_TOKEN_TTL.as_secs() as i64);
132 assert!(claims.claims.is_none());
133 }
134
135 #[test]
136 fn embeds_custom_claims_when_provided() {
137 let signer = CustomTokenSigner::new(test_service_account());
138 let mut extra = serde_json::Map::new();
139 extra.insert("admin".to_string(), serde_json::Value::Bool(true));
140
141 let token = signer
142 .create_custom_token("some-uid", Some(extra.clone()))
143 .unwrap();
144
145 let claims = independently_decode(&token);
146 assert_eq!(claims.claims, Some(extra));
147 }
148}