手动实现Firebase服务角色认证遇权限拒绝问题排查
Firebase服务角色认证权限问题(Rust手动实现)
我正在为Rust编写Firebase库(未使用现有SDK),手动通过JWT实现service-role认证。已成功用服务角色JSON生成JWT完成认证,但访问非公开权限的集合时出现“permission denied”错误。使用官方SDK的服务角色账号则无此问题,怀疑生成OAuth令牌时缺少额外声明,当前仅获得用户级权限而非服务角色的完整权限。
以下是当前生成refresh token的Rust代码:
const BASE_URL: &str = "https://identitytoolkit.googleapis.com/v1"; pub async fn get_refresh_token(&mut self) -> Result<(), GetRefreshTokenError> { let service_role_file_contents_string = std::fs::read_to_string(&self.service_role_key_path); if service_role_file_contents_string.is_err() { return Err(GetRefreshTokenError::UnableToReadServiceRoleFile); } let service_role_file_contents = serde_json::from_str::<HashMap<String, String>>(&service_role_file_contents_string.unwrap()); if service_role_file_contents.is_err() { return Err(GetRefreshTokenError::UnableToReadServiceRoleFile); } let service_role_file_contents = service_role_file_contents.unwrap(); let service_role_key = service_role_file_contents.get("private_key"); if service_role_key.is_none() { return Err(GetRefreshTokenError::InvalidServiceRoleFileFormat); } let service_role_key = service_role_key.unwrap(); let service_role_email = service_role_file_contents.get("client_email"); if service_role_email.is_none() { return Err(GetRefreshTokenError::InvalidServiceRoleFileFormat); } let service_role_email = service_role_email.unwrap(); let current_time = SystemTime::now().duration_since(UNIX_EPOCH); if current_time.is_err() { return Err(GetRefreshTokenError::UnableToGetSystemTime); } let current_time = current_time.unwrap().as_secs(); let current_time_str = current_time.to_string(); let expire_time = current_time + 3000; let expire_time_str = expire_time.to_string(); let uuid_str = Uuid::new_v4().to_string(); let claims = Claims { alg: "RS256".to_string(), iss: service_role_email.to_string(), sub: service_role_email.to_string(), aud: "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit".to_string(), iat: current_time_str, exp: expire_time_str, uid: uuid_str, admin: "true".to_string() }; let key = encode(&Header::new(Algorithm::RS256), &claims, &EncodingKey::from_rsa_pem(service_role_key.as_bytes()).unwrap()); if key.is_err() { return Err(GetRefreshTokenError::UnableToEncodeKey); } let key = key.unwrap(); let url = format!("{}/accounts:signInWithCustomToken?key={}", BASE_URL, self.web_api_key); let init_client = reqwest::Client::builder().build().unwrap(); let mut body = HashMap::new(); body.insert("token", key); body.insert("returnSecureToken", "true".to_string()); let response = init_client .post(&url) .json(&body) .send() .await; if response.is_err() { return Err(GetRefreshTokenError::UnableToMakeWebRequest); } let response = response.unwrap(); let res_json = response.json::<serde_json::Value>().await.unwrap(); println!("{:#?}", res_json); self.jwt_expiration_time = res_json.get("expiresIn").unwrap().to_string().replace('"',"").parse::<u64>().unwrap() + current_time; self.id_token = Some(res_json.get("idToken").unwrap().to_string().replace('"',"")); self.refresh_token = Some(res_json.get("refreshToken").unwrap().to_string().replace('"',"")); Ok(()) }
问题根源
当前实现调用的signInWithCustomToken接口是模拟普通用户登录的流程,生成的是用户级ID令牌,而非服务角色专属的Admin API访问令牌。服务角色需要通过Google OAuth 2.0的服务账号认证流程获取权限令牌,才能拥有完整的Admin级权限。
修正方案
1. 更换认证端点
放弃identitytoolkit.googleapis.com的登录接口,改用Google OAuth令牌端点https://oauth2.googleapis.com/token获取服务角色令牌。
2. 调整JWT声明字段
服务角色JWT必须符合Google服务账号规范,核心声明如下:
alg: 固定为RS256iss/sub: 服务账号的client_emailaud: 固定为https://oauth2.googleapis.com/tokeniat/exp: 数值类型的时间戳(不要转成字符串)scope: 需指定Firebase API的权限范围,比如https://www.googleapis.com/auth/firestore https://www.googleapis.com/auth/database(根据使用的服务调整)
3. 修改后的Rust代码示例
const OAUTH_TOKEN_URL: &str = "https://oauth2.googleapis.com/token"; pub async fn get_service_account_token(&mut self) -> Result<(), GetTokenError> { // 读取服务角色密钥文件 let service_role_file = std::fs::read_to_string(&self.service_role_key_path)?; let service_role: HashMap<String, String> = serde_json::from_str(&service_role_file)?; let private_key = service_role.get("private_key").ok_or(GetTokenError::InvalidFormat)?; let client_email = service_role.get("client_email").ok_or(GetTokenError::InvalidFormat)?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); let expire_time = current_time + 3600; // 令牌最长有效期1小时 // 构造符合规范的服务账号JWT声明 let claims = json!({ "alg": "RS256", "typ": "JWT", "iss": client_email, "sub": client_email, "aud": OAUTH_TOKEN_URL, "iat": current_time, "exp": expire_time, "scope": "https://www.googleapis.com/auth/firestore https://www.googleapis.com/auth/database" }); // 编码JWT let header = Header::new(Algorithm::RS256); let jwt = encode(&header, &claims, &EncodingKey::from_rsa_pem(private_key.as_bytes())?)?; // 请求OAuth访问令牌 let client = reqwest::Client::new(); let body = reqwest::multipart::Form::new() .text("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer") .text("assertion", jwt); let response = client.post(OAUTH_TOKEN_URL) .multipart(body) .send() .await?; let res_json = response.json::<serde_json::Value>().await?; self.access_token = Some(res_json["access_token"].as_str().unwrap().to_string()); self.token_expiration = current_time + res_json["expires_in"].as_u64().unwrap(); Ok(()) } // 自定义错误类型示例 #[derive(Debug)] enum GetTokenError { UnableToReadFile, InvalidFormat, JwtEncodeError, RequestError, JsonParseError, } impl From<std::io::Error> for GetTokenError { fn from(_: std::io::Error) -> Self { GetTokenError::UnableToReadFile } } // 补充其他错误转换实现...
4. 使用令牌调用Firebase API
获取到access_token后,调用Firebase服务时在请求头中添加:
Authorization: Bearer {access_token}
内容的提问来源于stack exchange,提问作者Kyle Kingsberry
相关产品推荐
相关产品推荐

