错误:无法加载默认凭据(Firebase function 到 firestore)

70

我正在尝试为Firebase Cloud Functions编写onCall函数,以对firestore数据库执行高级查询任务(例如将文本查询与AutoML自然语言进行比对以获取类别等),但我在尝试从函数查询数据库时遇到了问题:

Error getting documents ::  Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/srv/node_modules/google-auth-library/build/src/auth/googleauth.js:161:19)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:229:7)

功能:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();

exports.query = functions.https.onCall((data, context) => {
    const text = data.text;
    var results = [];
    const promise = db.collection('providers').get()
    promise.then((snapshot) => {
        console.log('marker');
        snapshot.forEach((doc) => {
            results.push({id: doc.id, data: doc.data()});
        });
        console.log('yessir');
        return {results: results};
    }).catch((err) => {
        console.log('Error getting documents :: ', err)
        console.log('nosir');
        return {results: "no results"};
    });
});

更长的输出:

Function execution started
Function execution took 8ms, finished with status code: 200
Error getting documents :: (etc, same error)
nosir

例2(运行中未改变):

Function execution started
Function execution took 1200 ms, finished with status code: 200
marker
yessir

我想不出这个问题从何而来,也不知道如何解决。有人能帮忙吗?

谢谢。


当您初始化应用程序时,是否尝试获取默认凭据?请在您的代码中指定此内容:admin.initializeApp({ credential:admin.credential.applicationDefault() }); - Pablo Almécija Rodríguez
@PabloAlmécijaRodríguez 遇到了同样的问题...我尝试了那个,但没有成功。 - Fernando Rocha
我刚刚尝试了一下,可惜没有成功。 - BurnDownTheIgloo
只需运行 firebase login 命令,问题就解决了。 - Cristhian Fernández
20个回答

73

我首先做的是将我的Firebase管理SDK密钥添加到我的项目中。

我在以下网址下载了SDK密钥:

https://console.firebase.google.com/u/0/project/**YOUR_PROJECT_ID**/settings/serviceaccounts/adminsdk

Admin SDK Key Download Page

接着,在admin.initializeApp();处将其更改为:

admin.initializeApp({
    credential: admin.credential.cert(require('../keys/admin.json'))
});

我的文件夹结构是

├── key
│   ├── admin.json
├── src
│   ├── index.ts

然而,更好的做法和更安全的方法,就像一些人已经提到的那样:

您可以使用环境变量来存储您的凭据,这样您就不会将它提交到诸如Github之类的存储库中,从而使其更安全,并且不会使其硬编码。

根据您的项目和部署位置,有不同的方法可以实现。

有很多教程可以让您了解如何创建和访问环境变量(像这样的教程),但您可以像下面的示例一样命名:

GOOGLE_APPLICATION_CREDENTIALS="/home/admin.json"

1
在TypeScript项目中,您如何做到这一点?我该如何指示tsc将证书放入转译后的文件夹树中? - Sergio
1
在我的情况下,默认凭据在某些机器上有效,但在其他机器上无效,但添加服务帐户当然是更稳定的解决方案。非常好的答案和非常详细的解释。 - Snorre
6
这不是一个合适的答案,因为它将可撤销的服务账号凭据嵌入到函数中。在你的源代码仓库中存放秘密信息是不合适的,而且如果在服务账号凭据轮换时部署了秘密信息,也是不合适的,因为这会强制重新部署云函数。 - Tetsujin no Oni
1
@TetsujinnoOni,你应该提出一个合适的答案。 - Fernando Rocha
2
我赞同的所有答案并没有采用这种可怕的安全实践,它们都是合适的,因此我添加答案是不合适的。 - Tetsujin no Oni
显示剩余5条评论

30

我遇到了同样的错误“Could not load the default credentials”。

这个错误是在我使用 npm update 更新项目依赖包,特别是 firebase-adminfirebase-functions 后发生的。

更新前:

"dependencies": {
    "@google-cloud/firestore": "^1.3.0",
    "firebase-admin": "~7.0.0",
    "firebase-functions": "^2.2.0"
}

更新后:

"dependencies": {
    "@google-cloud/firestore": "^1.3.0",
    "firebase-admin": "^8.6.0",
    "firebase-functions": "^3.3.0"
}

我将serviceAccountKey.json文件添加到我的项目中,并使用Firebase项目的服务账户设置提供的代码更改了导入。

改为:

我将serviceAccountKey.json添加到我的项目中,并根据Firebase项目的服务账户设置提供的代码更改了导入。

var admin = require('firebase-admin')

admin.initializeApp()

致:

var admin = require('firebase-admin');    
var serviceAccount = require('path/to/serviceAccountKey.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: 'https://my-project.firebaseio.com'
});

请查看下面@Fernando Rocha的答案,以访问您的Firebase项目的帐户设置。


你如何在Typescript项目中实现它?我该如何指示tsc将证书放置在转译后的文件夹树中? - Sergio
@Sergio,我刚把它手动放到lib文件夹里,然后它就可以工作了。 - Karl Hofmann
13
这个答案是极其不好的做法,因为它涉及将服务帐号密钥放入云函数源代码中。虽然这将使密钥对项目可用,但实践中这样做是不适当的。 - Tetsujin no Oni

23

@aldobaie的回答帮助我理解了我的使用情况。对于那些不想在所有调用中添加异步/等待的人来说,记住Firestore调用返回承诺,因此在它们之前加上return具有相同的效果。

在我的情况下:

function doSomething(...) {
    return admin.firestore().collection(...).doc(...).get()
        .then((doc) => {...})
        .catch(err => {...})
}

module.exports = functions.firestore.document('collection/{docId}').onWrite((change, context) => {
    return doSomething()
})

我认为被接受的答案违反了Firebase的推荐配置。函数环境已经可以访问管理员凭据,因此在代码中传递密钥并不是推荐的做法。

我的做法是这样的:


const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp(functions.config().firebase)

3
这个方法对我很有用——返回一个 Promise 解决了我的身份验证问题! - shankie_san
2
这也是我的确切问题。当我将一个云函数转换为async/await时,其中一个函数开始失败,我忘记了返回结果的promise。你可以在Firebase日志中看到该函数成功返回,然后几秒钟后记录身份验证问题。异步代码试图在云函数终止后执行,而auth信息现在已经消失了。这完全合理。 - Tim Philip

17

我自己也遇到了同样的问题。有时函数运行正常,但很多时候会抛出错误:Error: Could not load the default credentials

我认为这个问题已经通过观察回调函数来解决了。你必须使用 awaitasync 前缀,使函数一直运行,直到回调函数被调用。

Firebase Cloud Functions不允许在终止后通过回调访问处理器! 这就是为什么我们会得到 Error: Could not load the default credentials 错误的原因。

所以,每当你有一个 .then() 函数,请在前面加上 await ,并将其内部的函数添加 async 前缀,并在对函数的任何调用前添加 await

async function registerUser(..) {
    ...
    await admin.firestore().collection(..)...
    ...
}

我希望这能帮助你!


3
这个方法很好用。对于其他遇到这个问题的人,可能函数代码中有多个promise。每一个promise都需要被等待,不仅仅是主要的那一个。 - Adrian Marinica
我曾经遇到过同样的问题,最初我认为我的问题与这个问题有关:https://github.com/googleapis/google-auth-library-nodejs/issues/798 但实际上问题在于承诺没有正确等待,正如这个答案中所提到的。 - aragalie

9
另一种选择是将服务帐号密钥设置为环境变量,而不是通过调用firebaseAdmin.initializeApp({ credential })进行设置。此处有相关说明。
Linux操作系统。
export GOOGLE_APPLICATION_CREDENTIALS="[PATH]"
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/[FILE_NAME].json"

Windows PowerShell

$env:GOOGLE_APPLICATION_CREDENTIALS="[PATH]"
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\Users\username\Downloads\[FILE_NAME].json"

附言:一个更好的选择可能是使用本地仿真套件


1
在部署到Firebase的Google Cloud Functions时,您如何设置环境变量? - Sergio
4
据我所记,当我们部署到Firebase时,不需要那些凭证。@Sergio - Shaun Luttin
正确。当您部署到 Firebase 时,您的函数调用将来自云端,因此您已经通过身份验证。这是因为您不是在本地调用函数(如果您使用 firebase serve 而不是 firebase deploy),在本地您没有登录和身份验证,但在云端您已经登录和身份验证。因此不会出现身份验证错误! - C.Gadd

3

好的,我也遇到了这个问题,并花了几天时间在多个网站、文章、视频等上面寻找原因,以便为自己和其他人提供一个足够的答案。

这个问题已经有答案了。然而,我试着按照大部分答案操作,但都没有成功。有些答案存在安全问题,而其他一些答案则过于模糊难懂。我决定发布一个详细的答案,同时解决其他答案中可能存在的安全问题。

好了,既然说完了,我们开始吧!

首先你需要访问这个链接 - Getting started with authentication

在屏幕中央你应该会看到类似这样的内容 - Getting Started with Authentication 接下来,点击我标记为绿色的按钮。这将带你到创建服务账户密钥页面。

你应该会看到类似以下图像的屏幕 - enter image description here

  • 对于 服务账户 选项,选择新的服务账户。

  • 创建一个名字为您的服务帐户。这一点不重要,可以随意命名。

  • 对于角色选项,选择 项目 -> 所有者

  • 最后,选择 JSON 选项作为密钥类型,然后点击创建。

这将创建并下载一个 .json 文件。将此文件放在一个聪明而安全的地方。我在项目根目录中创建了一个名为“credentials”的文件夹,并将其放置在其中。

另外,我还将文件重命名为更易读的名称。虽然这不是必需的,但遵循良好的文件/文件夹命名和结构化实践非常重要,我建议你将其重命名为更易读的名称。

(需要注意的是,该文件是个人文件,不应包含在任何 github 存储库/firebase 生产环境等中。此文件仅供您自己使用!)

接下来打开命令提示符窗口,输入以下命令 -

set GOOGLE_APPLICATION_CREDENTIALS=C:\Users\Username\Path\To\File\filename.json

这将创建一个与你的凭据链接安全的环境变量,当你进行身份验证调用时 firebase 将识别和使用它。

(注意 - 这是 Windows 的命令。如果使用 mac/linux,请转到上面提到的 'Getting started with Authentication' 页面获取适合您操作系统的命令)

问题应该已经解决了。如果有任何进一步的问题或困难,请随时在下方评论,我会尽力帮助您。我知道被这样的错误卡住是多么令人沮丧。

我希望这能对至少一个人有所帮助。祝编程愉快。 C.Gadd


2
我不想使用@Fernando的解决方案,尽管没有什么问题。
我有prd和non-prd环境。我使用firebase use命令将更改推送到正确的环境。当我部署时,firebase使用默认服务帐户。此外,我不想在项目文件夹或git存储库中拥有密钥。
我解决问题的方式可能对其他人无效,但我想在这里分享。
当我更新firebase项目的权限以授予具有编辑权限的查看器时,问题就出现了。我让那个人成为所有者并回滚到编辑器。它消失了。虽然这不是一个合理的解决方法,但对我起作用,而且我不需要下载密钥。

3
这个答案指引了我正确的方向。我之前使用了不同的用户账户进行部署,导致出现了错误。使用默认账户重新部署后,问题得到了解决。无需进行显式凭据配置。 - lijo

2

在文档中有一件比较难找到的事情是,firebase-admin SDK 只有在 环境变量 告诉它时才使用模拟器。如果您像这里某些答案所描述的那样使用服务帐户 JSON 密钥,则 firebase-admin 将与生产环境(在 Google Cloud 上)通信,而不是模拟版本,即使您正在进行的其他所有操作都在模拟器上。

由于大多数情况下您可能更愿意使用模拟器进行本地测试,因此以下是我在 Mac 的 ~/.zshrc 中设置环境变量的方法:

export GCLOUD_PROJECT="your-project-id"
export FIRESTORE_EMULATOR_HOST=localhost:8080
export FIREBASE_AUTH_EMULATOR_HOST=localhost:9099
export FIREBASE_DATABASE_EMULATOR_HOST=localhost:9000

GCLOUD_PROJECT id可以是您的项目id,但显然只要它是一个格式良好的Firebase项目id,任何id都可以正常工作,因此这些同样的环境变量可用于在Firebase模拟器上测试所有项目。在尝试其他解决方案之前,请先设置这些环境变量以供模拟器使用。

另一个奇怪的地方是firebase emulators:start需要设置这些环境变量,但firebase emulators:exec会自动设置它们。当您处于CI场景中时,:exec是更好的选择,但当您编写代码并积极运行测试时,通过:start保持模拟器处于运行状态的速度更快,您将需要这些环境变量才能正常工作。通过在环境变量中设置这些内容,您的代码在部署到Cloud时将不需要进行任何更改。


我处于非常相似的情况下,我希望我的模拟器能够在启动时使用“:start”,这样每次运行测试时就不必重新启动。然而奇怪的是,当我尝试在.env文件中设置GCLOUD_PROJECT时,我只会得到以下错误信息:functions: Failed to load function definition from source: FirebaseError: Failed to load environment variables from .env. 一旦我将其移除或者更改一个字符的名称,模拟器就可以正常启动了,但我遇到了与OP相同的错误。你是否也遇到过这个问题?同样地,当我使用“:exec”时,我没有遇到任何错误。 - Lucas Rahn
我的错哈哈。我在云函数中使用了相同的环境文件,并且它为运行环境保留了这些环境变量。在测试设置文件中设置了环境变量并从实际的.env文件中删除后,您的解决方案起作用了。虽然我甚至认为您不需要设置GCLOUD_PROJECT,只需要设置其他的就可以了。https://firebase.google.com/docs/emulator-suite/connect_auth#admin_sdks - Lucas Rahn

2

不需要设置 serviceAccountKey.json 文件,您可以先从中设置 .env 值,然后再使用它们:

import * as firebaseAdmin from "firebase-admin";

const adminCredentials = {
  credential: firebaseAdmin.credential.cert({
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
    privateKey: JSON.parse(process.env.FIREBASE_PRIVATE_KEY || ""),
  }),
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
};

if (!firebaseAdmin.apps.length) {
  firebaseAdmin.initializeApp(adminCredentials);
}

const firestore = firebaseAdmin.firestore();

新回答:
这是Firebase中已知的一个bug。在此处查看进展情况:https://github.com/firebase/firebase-tools/issues/1940

然而,与此同时有几个解决方案:

1. 通过代码显式传递

var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://your-app.firebaseio.com"
});

不建议硬编码这个json文件,因为它在服务器上无法访问。

2 通过GOOGLE_APPLICATION_CREDENTIALS传递

我建议使用这种方式,设置环境变量:

GOOGLE_APPLICATION_CREDENTIALS=path/to/serviceAccountKey.json

对于Windows系统:(假设json文件位于项目的根目录。)
使用PowerShell:
$env:GOOGLE_APPLICATION_CREDENTIALS='serviceAccountKey.json'

使用NPM脚本:(注意在&&之前没有空格)

"serve": "set GOOGLE_APPLICATION_CREDENTIALS=serviceAccountKey.json&& npm run start",

由于某些原因,cross-env无法工作。

3 由于安装了gcloud sdk并运行gcloud auth application-default login,可在众所周知的文件系统路径中使用

4 在GCP上运行时,可以从Compute Engine元数据API中获取


1
我在 Firebase 上遇到了同样的问题,错误信息为:“无法获取默认凭据。” 然后前往 Firebase 控制台,进入项目设置,可以找到“服务帐户”选项。点击进入,你会看到在项目设置下有“生成新的私钥”的选项。 将适用于你的项目语言的代码复制并添加到你的项目文件中。

var admin = require("firebase-admin");

var serviceAccount = require("path/to/serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://your-database-url-that-is-given-under-admin-sdk-snippets"
});

生成密钥后,您将有下载选项。然后将其放入项目文件夹中。还需设置路径:var serviceAccount = require("path/to/serviceAccountKey.json");完成后,您已准备就绪。

2
这是一个重复的答案。 - Fernando Rocha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接