使用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
相关产品推荐
相关产品推荐

