You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

手动实现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: 固定为RS256
  • iss/sub: 服务账号的client_email
  • aud: 固定为https://oauth2.googleapis.com/token
  • iat/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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 06:25:57