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

使用Firebase服务账户调用FCM API时OAuth2认证失败求助

Firebase Messaging API 认证错误:Request had invalid authentication credentials

问题描述

调用Firebase Messaging API时触发认证错误:

Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid credential

尝试通过服务账户认证,调用FCM发送推送通知,但在获取访问令牌环节失败。

相关代码

引入的脚本

<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-messaging-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.6.1/firebase-firestore-compat.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script src="https://kjur.github.io/jsrsasign/jsrsasign-all-min.js"></script>

核心逻辑代码

function getAccessToken() {
  return new Promise(async function(resolve, reject) {
    try {
      const key = {
        client_email: "firebase-adminsdk-lmaet@****.iam.gserviceaccount.com",
        private_key: "-----BEGIN PRIVATE KEY-----\nKey\n-----END PRIVATE KEY-----\n"
      };

      const SCOPES = ['https://www.googleapis.com/auth/firebase.messaging'];
      
      const now = Math.floor(Date.now() / 1000);
      const payload = {
        iss: key.client_email,
        scope: SCOPES.join(' '),
        aud: 'https://oauth2.googleapis.com/token',
        exp: now + 3600,
        iat: now
      };

      const header = {
        alg: 'RS256',
        typ: 'JWT'
      };

      const encodeBase64Url = (input) => {
        return btoa(JSON.stringify(input))
          .replace(/=/g, '')
          .replace(/\+/g, '-')
          .replace(/\//g, '_');
      };

      const encodedHeader = encodeBase64Url(header);
      const encodedPayload = encodeBase64Url(payload);

      const signContent = `${encodedHeader}.${encodedPayload}`;

      const pemToArrayBuffer = (pem) => {
        const base64 = pem
          .replace(/-----BEGIN PRIVATE KEY-----/, '')
          .replace(/-----END PRIVATE KEY-----/, '')
          .replace(/\s/g, '');
        const binary = atob(base64);
        const buffer = new ArrayBuffer(binary.length);
        const bytes = new Uint8Array(buffer);
        for (let i = 0; i < binary.length; i++) {
          bytes[i] = binary.charCodeAt(i);
        }
        return buffer;
      };

      const privateKeyArrayBuffer = pemToArrayBuffer(key.private_key);

      const privateKey = await crypto.subtle.importKey(
        'pkcs8',
        privateKeyArrayBuffer,
        {
          name: 'RSASSA-PKCS1-v1_5',
          hash: { name: 'SHA-256' },
        },
        false,
        ['sign']
      );

      const encoder = new TextEncoder();
      const signatureArrayBuffer = await crypto.subtle.sign(
        { name: 'RSASSA-PKCS1-v1_5' },
        privateKey,
        encoder.encode(signContent)
      );

      const encodedSignature = btoa(String.fromCharCode.apply(null, new Uint8Array(signatureArrayBuffer)))
        .replace(/=/g, '')
        .replace(/\+/g, '-')
        .replace(/\//g, '_');

      const jwt = `${signContent}.${encodedSignature}`;

      const response = await fetch('https://oauth2.googleapis.com/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
          grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
          assertion: jwt
        })
      });

      const data = await response.json();

      if (data.access_token) {
        resolve(data.access_token);
      } else {
        reject(new Error('Failed to obtain access token'));
      }
    } catch (error) {
      reject(error);
    }
  });
}

async function sendNotification(token, tkid, serviceAccount) {
  try {
    const jwtToken = await getAccessToken(serviceAccount);

    const response = await fetch(
      "https://fcm.googleapis.com/v1/projects/silent-fuze-42760-t5/messages:send",
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${jwtToken}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          message: {
            token: token,
            notification: {
              title: "Profile View Alert",
              body: "Someone viewed your profile. Check now to see who viewed you!",
            },
            data: {
              tkid: tkid,
            },
          },
        }),
      }
    );

    const responseData = await response.json();
    console.log("Notification sent:", responseData);
  } catch (error) {
    console.error("Error sending notification:", error);
  }
}

已完成的操作和验证项

  • 在Firebase控制台为项目silent-fuze-427608-t5创建服务账户,并下载私钥JSON文件
  • 验证服务账户邮箱正确:firebase-adminsdk-lmaet@silent-fuze-427608-t5.iam.gserviceaccount.com
  • 项目ID确认无误:silent-fuze-427608-t5
  • 已在Google Cloud Console启用Firebase Cloud Messaging API、IAM API
  • 服务账户已分配Firebase Admin SDK Administrator Service Agent角色
  • 尝试生成新私钥,问题依旧

解决方案

1. 修正JWT Base64URL编码逻辑

原encodeBase64Url函数未处理Unicode字符,会导致JSON字符串编码错误,替换为以下实现:

const encodeBase64Url = (input) => {
  return btoa(unescape(encodeURIComponent(JSON.stringify(input))))
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');
};

2. 修正项目ID匹配问题

sendNotification函数中请求URL的项目ID为silent-fuze-42760-t5,与实际项目IDsilent-fuze-427608-t5不符(少了数字8),这会导致令牌权限与目标项目不匹配,修正后URL应为:

"https://fcm.googleapis.com/v1/projects/silent-fuze-427608-t5/messages:send"

3. 确保私钥格式完整

私钥中的换行符必须完整保留,不能省略或替换,示例格式:

private_key: "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...\n...\n-----END PRIVATE KEY-----\n"

4. 迁移认证逻辑到后端

关键提醒:当前在前端浏览器中处理服务账户私钥会导致密钥泄露,严重威胁项目安全。必须将FCM推送逻辑迁移到后端环境,推荐使用Firebase Admin SDK自动处理认证流程,示例代码(Node.js):

const admin = require('firebase-admin');
const serviceAccount = require('./path-to-service-account-key.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});

async function sendNotification(token, tkid) {
  const message = {
    token: token,
    notification: {
      title: "Profile View Alert",
      body: "Someone viewed your profile. Check now to see who viewed you!",
    },
    data: {
      tkid: tkid,
    },
  };

  try {
    const response = await admin.messaging().send(message);
    console.log("Notification sent:", response);
  } catch (error) {
    console.error("Error sending notification:", error);
  }
}

内容的提问来源于stack exchange,提问作者Kiran

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 07:54:55