Functions emulator should not requires authenticated secret manager calls if .secret.local is defined
[REQUIRED] Environment info
firebase-tools: 13.12.0
Platform: macOS
[REQUIRED] Test case
A firebase function that depends on a secret. E.g.
export const processUploadedFile = functions
.region(FUNCTION_REGION)
.runWith({ secrets: FUNCTION_SECRETS })
.storage.object()
.onFinalize(processUploadedFileHandler)
Also have a .secret.local file that defines all required secrets
[REQUIRED] Steps to reproduce
With no firebase or application default credentials in place (or being offline)
firebase --project demo-my-project-id emulators:start
[REQUIRED] Expected behavior
Since .secret.local is defined, we should not worry if secrets manager is inaccessible (due to lack of credentials or being offline) since we assume user has populated .secret.local properly. I expect any issues to be handled at runtime with
⬢ functions: Unable to access secret environment variables from Google Cloud Secret Manager. Make sure the credential used for the Functions Emulator have access or provide override values in /.../functions/.secret.local:
as correctly handled by resolveSecretEnvs
[REQUIRED] Actual behavior
⬢ functions: Failed to load function definition from source: FirebaseError: HTTP Error: 401, Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Through debugging with local firebase-tools, this is happening in the functions deploy phase, where the issue is at
resolveParams which has the following code
const [needSecret, needPrompt] = partition(outstanding, (param) => param.type === "secret");
for (const param of needSecret) {
await handleSecret(param as SecretParam, firebaseConfig.projectId);
}
which requires caller to have access to the secret manager APIs to succeed.
Commenting out the call to handleSecret gives me the expected behaviour - if I have no credentials or even if I'm offline, the emulator starts with functions and my full stack operates as expected.
Background
The background is that I'm trying to get the emulator to work while fully offline as I'll be travelling without internet for a while and have been following advice from #3916 and elsewhere. This is the only issue that needs to be resolved for me to work offline.
Workaround
I have built a local copy of firebase-tools and commented out the await handleSecret line and am okay now.
Proposed fix
The best fix is for handleSecret in params.ts to check for the existence of the secret in .secret.local before making the call out to secret manager. Centralise the code in resolveSecretEnvs in functionsEmulator for reuse.
Or if it were me doing the PR, after line 385 in params.ts, I'd load .secret.local there, then for any matched secret, ignore the call to handleSecret.
I am open to making a PR for this patch if requested (since I'm using a local copy of firebase-tools anyway and can test easily).
Hey @glorat, thanks for the detailed report and for sharing your observations. I’ve been trying to reproduce this issue, but so far I haven’t been able to replicate the behavior mentioned. Here’s my current setup for reproducing the issue:
functions/index.js
import * as functions from "firebase-functions/v1"
const FUNCTION_SECRETS = ["FIRST_SECRET"]
export const helloWorld1 = functions
.runWith({ secrets: FUNCTION_SECRETS }).https.onRequest((request, response) => {
response.send("Hello from Firebase!" + process.env.FIRST_SECRET);
});
export const processUploadedFile = functions
.runWith({ secrets: FUNCTION_SECRETS })
.storage.object()
.onFinalize(() => {
functions.logger.debug(`The first secret is ${process.env.FIRST_SECRET}`)
})
functions/.secret.local
FIRST_SECRET=local-secret-first
No errors were raised running firebase emulators:start --project demo-project as well as when invoking the functions.
Just to verify, are you using defineSecret in your codebase? I found a similar issue that could be related #5520, and I tried adding defineSecret to my codebase and I was able to get the same error message you shared:
functions: Failed to load function definition from source: FirebaseError: HTTP Error: 401, Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Just to verify, are you using
defineSecretin your codebase? I found a similar issue that could be related #5520, and I tried addingdefineSecretto my codebase and I was able to get the same error message you shared:functions: Failed to load function definition from source: FirebaseError: HTTP Error: 401, Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.
Hi, yes you need to have defineSecrets in the codebase and a function that depends on that secret to cause this issue (e.g. .runWith({ secrets: FUNCTION_SECRETS })
My workaround also addresses #5520 , per my local testing. My proposed fix should also be good
Thanks for the additional information @glorat. I’ll raise this to our engineering team so they can take a look. Also, it looks like you have a potential fix for this, feel free to make a PR if you feel up for it! Contributions are always welcome!
Thanks for the additional information @glorat. I’ll raise this to our engineering team so they can take a look. Also, it looks like you have a potential fix for this, feel free to make a PR if you feel up for it! Contributions are always welcome!
Unfortunately, I couldn't figure out how to make my hardcoded approach generic and I can't quite infer the design so not confident to PR it. My direction to the dev team is to look at resolveParams in params.ts. It already excludes .env related params from the secrets manager check so one just needs to extend that the secrets.local exclusion too. However, it's not clear how to do that. Options include
- Just try to read it right there in that function
- Get it passed it like UserEnvs already does
- Refactor and reuse the existing .secret.local loading code in the main emulator
I think I'll need to leave that with the devs to decide.
@glorat I am also facing this issue, can you share your hardcoded solution? I would like to see it if possible, I am currently adding a TEST env var to firebase command, TEST=true firebase emulators:exec vitest then added a break in the loop in params.js:
for (const param of needSecret) {
if (process.env.TEST) {
break;
}
await handleSecret(param, firebaseConfig.projectId);
}
Works for now for me but keen to see another solution to compare. Thanks
@glorat I am also facing this issue, can you share your hardcoded solution? I would like to see it if possible, I am currently adding a TEST env var to firebase command,
TEST=true firebase emulators:exec vitestthen added a break in the loop in params.js:for (const param of needSecret) { if (process.env.TEST) { break; } await handleSecret(param, firebaseConfig.projectId); }Works for now for me but keen to see another solution to compare. Thanks
That's the way to go. This is best since you're happy to modify a local copy.
An alternative is to set secrets through environment but if you do this be careful not to deploy functions from command line and send the environment to the cloud.