firebase / firebase-admin-node

Firebase Admin Node.js SDK
https://firebase.google.com/docs/admin/setup
Apache License 2.0
1.63k stars 371 forks source link

Error fetching access token. Not connecting to Firebase Auth Emulator if application_default_credentials.json not present #1077

Closed imarian97 closed 4 years ago

imarian97 commented 4 years ago

[READ] Step 1: Are you in the right place?

[REQUIRED] Step 2: Describe your environment

[REQUIRED] Step 3: Describe the problem

If the 'application_default_credentials.json' is not present in the gcloud config path I get the error from below when I try to call any auth method even though the emulator is started, the FIREBASE_AUTH_EMULATOR_HOST env var is correctly set and the initializeApp is correctly called.

Based on my research this problem comes from the getApplicationDefault function inside the src/credential/credential-internal.ts file. I solved this by running gcloud auth application-default login (found on internet, don't know exactly what it does) which generated a valid application_default_credentials.json file

I don't completely understand the underlying mechanisms but I think this behaviour is not great for my use case which is probably very similar with a CI/CD pipeline use case. Also, there's nothing mentioned in the Firebase Auth emulator about this and I really like to be able to run all emulators without a real project but, doing this gcloud config makes me worried not to interact by mistake with the real gcloud project.

 Error: Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth2 access token with the following error: "Error fetching access token: Error while making request: getaddrinfo ENOTFOUND metadata.google.internal metadata.google.internal:80. Error code: ENOTFOUND".
    at FirebaseAppError.FirebaseError [as constructor] (C:\Users\Marian\Desktop\test\tp\node_modules\firebase-admin\lib\utils\error.js:43:28)
    at FirebaseAppError.PrefixedFirebaseError [as constructor] (C:\Users\Marian\Desktop\test\tp\node_modules\firebase-admin\lib\utils\error.js:89:28)
    at new FirebaseAppError (C:\Users\Marian\Desktop\test\tp\node_modules\firebase-admin\lib\utils\error.js:124:28)
    at C:\Users\Marian\Desktop\test\tp\node_modules\firebase-admin\lib\firebase-app.js:124:23
    at process._tickCallback (internal/process/next_tick.js:68:7)

Steps to reproduce:

  1. Run the firebase emulators (especially the auth emulator)
  2. Set the FIREBASE_AUTH_EMULATOR_HOST env var as described in the docs
  3. Create a simple npm project that uses the firebase-admin
  4. Write a simple script (see detailed code below) that makes any call on the auth using the firebase-admin

Relevant Code:

I'm running a dev environment using the VSCode .devcontainer feature using the following files:

main.Dockerfile

ARG VARIANT="14-buster"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends openjdk-11-jre-headless
RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] http://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key --keyring /usr/share/keyrings/cloud.google.gpg  add - && apt-get update -y && apt-get install google-cloud-sdk -y
RUN sudo -u node npm install -g @nestjs/cli@7.4.0 firebase-tools@v8.14.0 nx@10.3.0

firebase_emulator.Dockerfile

ARG VARIANT="14-buster"
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}

RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
    && apt-get -y install --no-install-recommends openjdk-11-jre-headless

RUN sudo -u node npm install -g firebase-tools@v8.14.0

COPY ./firebase.json .
COPY ./.firebaserc .

RUN firebase setup:emulators:database
RUN firebase setup:emulators:firestore

ENTRYPOINT [ "firebase",  "emulators:start"]

docker-compose.yml

version: '3'
services:
  main:
    build: 
      context: .
      dockerfile: main.Dockerfile
      args:
        VARIANT: 10
    volumes:
      - ..:/workspace
    command: sleep infinity
    environment:
      FIRESTORE_EMULATOR_HOST: "firebase_emulator:8080"
      FIREBASE_DATABASE_EMULATOR_HOST: "firebase_emulator:9000"
      FIREBASE_AUTH_EMULATOR_HOST: "firebase_emulator:9099"
      GCLOUD_PROJECT: "local"
    links:
      - firebase_emulator
  firebase_emulator:
    build: 
      context: .
      dockerfile: firebase_emulator.Dockerfile
      args:
        VARIANT: 10
    ports:
      - "4000:4000"
      - "5001:5001"
      - "8080:8080"
      - "9000:9000"
      - "8085:8085"
      - "4500:4500"
      - "4400:4400"
      - "9005:9005"
      - "9099:9099"

devcontainer.json

{
    "name": "Node.js & TypeScript & GCP & Firebase & Nest",
    "dockerComposeFile":"docker-compose.yml",
    "service": "main",
    "workspaceFolder": "/workspace",
    "settings": { 
        "terminal.integrated.shell.linux": "/bin/zsh",
        "files.autoSave": "afterDelay",
        "git.autofetch": true,
        "workbench.iconTheme": "material-icon-theme"
    },
    "extensions": [
        "dbaeumer.vscode-eslint",
        "ms-vscode.vscode-typescript-tslint-plugin",
        "eg2.vscode-npm-script",
        "redhat.vscode-yaml",
        "abhijoybasak.nestjs-files",
        "nrwl.angular-console",
        "esbenp.prettier-vscode",
        "firsttris.vscode-jest-runner",
        "github.vscode-pull-request-github",
        "pkief.material-icon-theme",
        "humao.rest-client"
    ],
    "remoteEnv": {
        "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
        "FIREBASE_TOKEN": "${localEnv:FIREBASE_TOKEN}"
    },
    "postCreateCommand": "npm install && npm install --only=dev",
    "remoteUser": "node"
}

firebase.json

{
    "emulators": {
      "firestore": {
        "port": 8080,
        "host": "0.0.0.0"
      },
      "database": {
        "port": 9000,
        "host": "0.0.0.0"
      },
      "auth": {
        "port": 9099,
        "host": "0.0.0.0"
      },
      "ui": {
        "enabled": true,
        "port": 4000,
        "host": "0.0.0.0"
      }
    }
 }

.firebaserc

{
  "projects": {
    "default": "local"
  }
}

The minimal code required is to call some auth function using the firebase-admin package. For example:

// The test script make sure the FIREBASE_AUTH_EMULATOR_HOST environment var is set
const process=require("process")
process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9099";

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

admin.initializeApp({
    projectId: 'local'
})

admin.auth().createUser({});
IsaiahByDayah commented 4 years ago

Also running into this issue after trying to use the admin SDK as mentioned by @yuchenshi here: https://github.com/firebase/firebase-tools/issues/2749#issuecomment-718141997

One thing I did notice as well (likely separate ticket but maybe could be an easier workaround?). In @firebase/rules-unit-testing you can call initializeAdminApp and get what is suppose to be an admin app, but it's Auth doesn't have all the same methods as firebase-admin's Auth, namely createUser() which is what I specifically was trying to use to create users with specific uid's for testing

hiranya911 commented 4 years ago

@imarian97 you mentioned: Firebase SDK version: v8.14.0. Note that the emulator support is only available on latest version (v9.3.0) of the SDK.

imarian97 commented 4 years ago

@imarian97 you mentioned: Firebase SDK version: v8.14.0. Note that the emulator support is only available on latest version (v9.3.0) of the SDK.

Sorry! I have confused firebase-tools with firebase-admin-node. So, to clarify, I'm using the latest versions at the moment of writing and these versions are:

akauppi commented 4 years ago

@IsaiahByDayah On your side comment on rules testing and initializeAdminApp: I think it's a completely different abstraction from the admin client, only meant for bypassing rules restrictions. Anyways, out of scope of this issue.

IsaiahByDayah commented 4 years ago

Haha, that would explain things wouldn't it 😅. My bad. I guess I just saw "admin" and made assumptions

samtstern commented 4 years ago

@IsaiahByDayah ok there's a lot going on here so I have some questions.

  1. Where do you get this error Error: Credential implementation provided to initializeApp(), is this from code running within the Functions emulator? A script running elsewhere? Do you have to do anything besides 'emulators:start` to trigger this error?
  2. It seems like you have the FIREBASE_TOKEN environment variable set, does the behavior change with/without that?
  3. Is there a simpler way I could reproduce this behavior? Are you able to get this to happen outside of docker / docker compose?
imarian97 commented 4 years ago

@IsaiahByDayah ok there's a lot going on here so I have some questions.

  1. Where do you get this error Error: Credential implementation provided to initializeApp(), is this from code running within the Functions emulator? A script running elsewhere? Do you have to do anything besides 'emulators:start` to trigger this error?
  2. It seems like you have the FIREBASE_TOKEN environment variable set, does the behavior change with/without that?
  3. Is there a simpler way I could reproduce this behavior? Are you able to get this to happen outside of docker / docker compose?

Hi, Considering the questions I suppose they are also addressed to me so:

  1. The script is not running in the emulator. In my real use case, I get the error inside a Jest test and in a Nest.js app but, I also tested with a simple script ( the last script from the initial comment) in a clean npm project and I still get the error.
  2. I have removed the environment variable and the behaviour is the same, I still get the same error.
  3. I was able to reproduce the same issue in my host machine (running windows) so, it is not docker related. To reproduce the issue, follow the steps in the initial comment. I will rewrite them here in a simpler form:

    Assuming you have the specified versions of node, npm and firebase-tools:

  4. Create a folder
  5. Run npm init -y in the folder
  6. Run npm i firebase-admin in the folder
  7. Create the firebase.json file in the folder with the content from the initial comment
  8. Create the .firebaserc file in the folder with the content from the initial comment
  9. Create the index.js file with the following content:
    
    // The test script make sure the FIREBASE_AUTH_EMULATOR_HOST environment var is set
    const process=require("process")
    process.env.FIREBASE_AUTH_EMULATOR_HOST = "localhost:9099";

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

admin.initializeApp({ projectId: 'local' })

admin.auth().createUser({});


 7. Run `node index.js`
samtstern commented 4 years ago

So I think this is working as intended, although I'd like to do some experiments myself to try. Setting the FIREBASE_AUTH_EMULATOR_HOST doesn't really change how firebase.initializeApp() works, since that initializes things for much more than Auth. And initializeApp() needs to be able to find a credential from somewhere. Some ways it can find a credential:

  1. You could manually pass a credential param with projectId
  2. You can point the GOOGLE_APPLICATION_CREDENTIALS env var at a service account JSON file, thus authorizing as a service account.
  3. You can use gcloud auth application-default login to put user credentials on your machine, thus authorizing as yourself.

Now we try to hide this from you in a few cases:

  1. When you run initializeApp() inside Google Cloud, like in a Cloud Function or on App Engine or on Compute Engine, we can automatically discover the necessary credentials due to the trusted environment.
  2. When you run initializeApp() inside the Cloud Functions Emulator, we simulate (1) by morphing your firebase login credentials into something the Admin SDK recognizes.

So when running a script in a completely unauthorized docker container, I'd expect this error.

imarian97 commented 4 years ago

So I think this is working as intended, although I'd like to do some experiments myself to try. Setting the FIREBASE_AUTH_EMULATOR_HOST doesn't really change how firebase.initializeApp() works, since that initializes things for much more than Auth. And initializeApp() needs to be able to find a credential from somewhere. Some ways it can find a credential:

  1. You could manually pass a credential param with projectId
  2. You can point the GOOGLE_APPLICATION_CREDENTIALS env var at a service account JSON file, thus authorizing as a service account.
  3. You can use gcloud auth application-default login to put user credentials on your machine, thus authorizing as yourself.

Now we try to hide this from you in a few cases:

  1. When you run initializeApp() inside Google Cloud, like in a Cloud Function or on App Engine or on Compute Engine, we can automatically discover the necessary credentials due to the trusted environment.
  2. When you run initializeApp() inside the Cloud Functions Emulator, we simulate (1) by morphing your firebase login credentials into something the Admin SDK recognizes.

So when running a script in a completely unauthorized docker container, I'd expect this error.

I forgot to mention an important step but I suppose you already noticed it. The step was to remove or rename the application_default_credentials.json file which on Windows is located in %appdata%\gcloud

I understand that the initializeApp() needs the credentials but I don't really like to have to provide as a requirement a service account or some other form of credential to my CI/CD pipeline or, in my case, (at least in this stage of the development) to the local development environment, as long as I'm using only the emulators. For Firestore and RTDB emulators, based on my experience, the credentials are not required and I feel like the firebase-admin code can be updated (I have not spent enough time on it to understand how) to also not require the credentials for Auth Emulator.

I don't really have a problem with the credentials but I can't "fit the moment where you generate/obtain the credentials". For the CI/CD I know there are some ways to store a secret (the service account) and then retrieve it but that's extra work and configs considering that only emulators are used. But, for the local development scenario, it is even worse. The ideal scenario for local development which I managed to achieve using the method described in the first comment is to have any developer of my team clone the repo and immediately start working on code (because I've containerized both the dev env and the Firebase Emulators so, everything is configured and starts automatically and the developer does not have to do anything). In this scenario, obtaining the credentials can be a bit annoying considering that including the service account in the repo is not a good practice and the developer might not even have access to a GCP/Firebase project (extremely possible for public projects)

I hope the scenarios and my issue are a bit clear now and I hope to find together (or, maybe, it already exists but I'm missing it) a good solution for completely local development and easy to use CI/CD.

dbanisimov commented 4 years ago

Facing a similar issues here. Admin SDK cannot be used with the Auth emulator without providing valid application credentials.

The particular use case for me is a using the Admin SDK to seed/reset the emulators in a script. And while Admin SDK can successfully interact with the Firestore emulator without any application credentials, the Auth part of the Admin SDK requires application credentials with the Auth emulator.

nVitius commented 4 years ago

I am also running into this issue. The documentation for the auth emulator recommends writing a script to create users, but I am not able to get admin-firebase to work this way unless I provide it with credentials to a real environment.

samtstern commented 4 years ago

@dbanisimov @nVitius thanks for your feedback! According to @yuchenshi we may be able to fix this by lazily requiring credentials in firebase-admin-node when using Auth with the emulator.

samtstern commented 4 years ago

This is fixed and released in version 9.4.0 of firebase-admin