Firebase callable functions fail - firebase

Many functions have started to fail intermittently with the following error even before the first line of code is executed:
FIREBASE WARNING: {"code":"app/invalid-credential","message":"Credential implementation provided to initializeApp() via the "credential" property failed to fetch a valid Google OAuth2 access token with the following error: "Failed to parse access token response: SyntaxError: Unexpected token p in JSON at position 4"."}
It's been more than 5 five days since i had opened a bug issue with Firebase support and i still don't have any feedback.
Does anyone why this is happening and how it can be fixed?
Libraries used and their versions:
"firebase": "^8.2.9",
"firebase-admin": "^9.5.0",
"firebase-functions": "^3.1.0"
Initializations:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
var config = {
apiKey: "...",
authDomain: "...",
databaseURL: "..."
};
var firebase = require("firebase");
firebase.initializeApp(config);

In a Cloud Function, if you want to interact with the Firebase services (e.g. Firestore, Auth service, etc), you need to use the Admin SDK.
So, you need to load the firebase-functions and firebase-admin modules, and initialize an admin app instance from which you interact with the services, as follows:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const firestoreDB = admin.firestore();
const authService = admin.auth();
// ...
// Examples:
// In a Cloud Function
return firestoreDB.collection("cities").doc("LA").set({
name: "Los Angeles",
state: "CA",
country: "USA"
});
// In another Cloud Function
return authService.updateUser(uid, {
email: 'modifiedUser#example.com',
phoneNumber: '+11234567890',
})
.then((userRecord) => {
// ...
// return ...
})
In other words, you don't need to do:
var config = {
apiKey: "...",
authDomain: "...",
databaseURL: "..."
};
var firebase = require("firebase");
firebase.initializeApp(config);
Also note this note from the doc:
In many cases, new features and bug fixes are available only with the
latest version of the Firebase CLI and the firebase-functions SDK.
It's a good practice to frequently update both the Firebase CLI and
the SDK with these commands inside the functions folder of your
Firebase project:
npm install firebase-functions#latest firebase-admin#latest --save
npm install -g firebase-tools

Related

GitHub continuous deployment to Firebase hosting and env variables

I am currently working on a React application with Firebase initialised. I am initialising my React App with Firebase by doing the following:
import firebase from 'firebase/app'
import 'firebase/auth'
import 'firebase/firestore'
const app = firebase.initializeApp({
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID
})
const db = firebase.firestore()
export { db, app }
For obvious reasons, I do not want to push my env file to GitHub. But if I do not, my application build fails. I am wondering how I can configure GitHub to house my env variables and for my production build to use the GitHub env variables. I have attempted to set up a GitHub build workflow, which contains the env variables, but my build still seems to fail.
On a separate note, I am curious how much of a security risk it is for my Firebase config to be exposed. I read if my application is using an email/password sign-in method, I should protect these variables. Any advice, suggestions, critiques would be much appreciated.
Just use GitHub Actions with secrets and echo with the runner:
- name: 🗄️ Provide credentials
id: firebase-credentials
run: echo 'const app = firebase.initializeApp({apiKey: "${{ secrets.FIREBASE_API_KEY }}", ... })' > ./some.js
You can use Firebase Cloud Functions and set an environment variable in the terminal:
firebase functions:config:set someservice.key="API KEY" someservice.id="CLIENT ID"
For example, to set up the Cloud Function for Firebase:
firebase init functions
npm install --save firebase-functions#latest
npm install -g firebase-tools
You can then access the variables from a function:
const functions = require('firebase-functions');
const request = require('request-promise');
exports.userCreated = functions.database.ref('/users/{id}').onWrite(event => {
let email = event.data.child('email').val();
return request({
url: 'https://someservice.com/api/some/call',
headers: {
'X-Client-ID': functions.config().someservice.id,
'Authorization': `Bearer ${functions.config().someservice.key}`
},
body: {email: email}
});
});

How do I wire up the firestore emulator with my firebase functions tests?

Currently we are using 'firebase-functions-test' in online mode to test our firebase functions (as described here https://firebase.google.com/docs/functions/unit-testing), which we setup like so:
//setupTests.ts
import * as admin from 'firebase-admin';
const serviceAccount = require('./../test-service-account.json');
export const testEnv = require('firebase-functions-test')({
projectId: 'projectId',
credential: admin.credential.cert(serviceAccount),
storageBucket: 'projectId.appspot.com'
});
const testConfig = {
dropbox: {
token: 'dropboxToken',
working_dir: 'someFolder'
}
};
testEnv.mockConfig(testConfig);
// ensure default firebase app exists:
try {
admin.initializeApp();
} catch (e) {}
We would like to move away from testing against an actual firestore instance in our tests, and use the emulator instead.
The docs, issues, and examples I've been able to find on the net are either outdated, or describe how to set up the emulator for testing security rules, or the web frontend.
Attempts using firebase.initializeAdminApp({ projectId: "my-test-project" }); did not do the trick.
I also tried setting FIRESTORE_EMULATOR_HOST=[::1]:8080,127.0.0.1:8080
So the question is: How can I initialise the firebaseApp in my tests, so that my functions are wired up to the firestore emulator?
I had another crack at it today, more than a year later, so some things have changed, which I can't all list out. Here is what worked for me:
1. Install and run the most recent version of firebase-tools and emulators:
$ npm i -g firebase-tools // v9.2.0 as of now
$ firebase init emulators
# You will be asked which emulators you want to install.
# For my purposes, I found the firestore and auth emulators to be sufficient
$ firebase -P <project-id> emulators:start --only firestore,auth
Take note of the ports at which your emulators are available:
2. Testsetup
The purpose of this file is to serve as a setup for tests which rely on emulators. This is where we let our app know where to find the emulators.
// setupFunctions.ts
import * as admin from 'firebase-admin';
// firebase automatically picks up on these environment variables:
process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080';
process.env.FIREBASE_AUTH_EMULATOR_HOST = 'localhost:9099';
admin.initializeApp({
projectId: 'project-id',
credential: admin.credential.applicationDefault()
});
export const testEnv = require('firebase-functions-test')();
3. Testing a simple function
For this, we setup a simple script which writes a document to firestore. In the test, we assert that the document exists within the emulator, only after we have run the function.
// myFunction.ts
import * as functions from 'firebase-functions';
import {firestore} from 'firebase-admin';
export const myFunction = functions
.region('europe-west1')
.runWith({timeoutSeconds: 540, memory: '2GB'})
.https.onCall(async () => {
await firestore()
.collection('myCollection')
.doc('someDoc')
.set({hello: 'world'});
return {result: 'success'};
});
// myTest.ts
// import testEnv first, to ensure that emulators are wired up
import {testEnv} from './setupFunctions';
import {myFunction} from './myFunction';
import * as admin from 'firebase-admin';
// wrap the function
const testee = testEnv.wrap(myFunction);
describe('myFunction', () => {
it('should add hello world doc', async () => {
// ensure doc does not exist before test
await admin
.firestore()
.doc('myCollection/someDoc')
.delete()
// run the function under test
const result = await testee();
// assertions
expect(result).toEqual({result: 'success'});
const doc = await admin
.firestore()
.doc('myCollection/someDoc')
.get();
expect(doc.data()).toEqual({hello: 'world'});
});
});
And sure enough, after running the tests, I can observe that the data is present in the firestore emulator. Visit http://localhost:4000/firestore while the emulator is running to get this view.

File upload with filestreams and Firebase cloud functions + cloud storage

I've written the following code for my QR file upload using firebase cloud functions
const functions = require('firebase-functions');
const qrcode = require('qrcode')
const admin = require('firebase-admin');
const spawn = require('child-process-promise').spawn;
const serviceAccount = require("./secret.json");
const gcs = require('#google-cloud/storage')();
admin.initializeApp(functions.config({
credential: admin.credential.cert(serviceAccount),
storageBucket: "https://SECRET.firebaseio.com"
}));
exports.qrcode = functions.https.onRequest((req, res) => {
const storage = admin.storage().bucket();
const dummyFile = storage.file('dummy.png')
new Promise ((resolve, reject) => qrcode.toFileStream(
dummyFile.createWriteStream()
.on('finish', resolve)
.on('error', reject),
'DummyQR'))
.then(console.log("success")) //Doing stuff here later
.catch(console.log)
res.send("<script>window.close();</script>")
});
According to the docs I should be able to connect to the bucket by simply calling admin.storage().bucket(); (https://firebase.google.com/docs/storage/admin/start) however I get the following error:
Error: Error occurred while parsing your function triggers.
Error: Bucket name not specified or invalid. Specify a valid bucket name via the storageBucket option when initializing the app, or specify the bucket name explicitly when calling the getBucket() method.
and so I'm stuck and not sure how to proceed. If I try to manually enter the bucket admin.storage().bucket("https://SECRET.firebaseio.com"); I get the error
{ ApiError: Not Found
at Object.parseHttpRespBody (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/storage/node_modules/#google-cloud/common/src/util.js:193:30)
at Object.handleResp (/user_code/node_modules/firebase-admin/node_modules/#google-cloud/storage/node_modules/#google-cloud/common/src/util.js:131:18)
at /user_code/node_modules/firebase-admin/node_modules/#google-cloud/storage/node_modules/#google-cloud/common/src/util.js:496:12
at Request.onResponse [as _callback] (/user_code/node_modules/firebase-admin/node_modules/retry-request/index.js:195:7)
at Request.self.callback (/user_code/node_modules/firebase-admin/node_modules/request/request.js:185:22)
at emitTwo (events.js:106:13)
at Request.emit (events.js:191:7)
at Request.<anonymous> (/user_code/node_modules/firebase-admin/node_modules/request/request.js:1157:10)
at emitOne (events.js:96:13)
at Request.emit (events.js:188:7)
code: 404,
errors: [ { domain: 'global', reason: 'notFound', message: 'Not Found' } ],
response: undefined,
message: 'Not Found' }
I had this same problem. I just added the storageBucket name when initializing the app, and if you are using the getSignedUrl method you need to include a service account, otherwise that url it's gonna get expired after a week (as it was in my case).
const serviceAccount = require('/your/service/account/key/path');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
storageBucket: "your-storage-bucket-name.appspot.com",
});
don't forget to update your firebase functions with
firebase deploy --only functions
on the command line
UPDATE 6 APRIL 2020
Signed URLs actually expire after 7 days. If you need URLs for more time than that, you should read this answer and this thread
It looks like you're not initializing the admin SDK correctly. Just call initializeApp with no parameters to get all the correct defaults:
admin.initializeApp();
This will use the default service account provided by Cloud Functions for your project. This account has permission to do most of what you need to do in your function without any additional configuration.
In my case it was similar issue to this one (Unable to use cloud storage without specifying storageBucket when initialising app with firebase-admin).
I had following setup:
const admin = require("firebase-admin")
admin.initializeApp()
export * from "./api/api"
export * from "./cron/cronJobs"
export * from "./posts/postsCloudFunctions"
Changed it to:
const admin = require("firebase-admin")
export * from "./api/api"
export * from "./cron/cronJobs"
export * from "./posts/postsCloudFunctions"
admin.initializeApp()
And it seems the issue is initializeApp() was not being called before accessing the storage bucket.
I have same issue while uploading to image to firebase storage:
Error: Bucket name not specified or invalid. Specify a valid bucket name via the storageBucket option when initializing the app, or specify the bucket name explicitly when calling the getBucket() method.
ndlers\users.js:130:8)
at Busboy.emit (events.js:189:13)
at Busboy.emit (E:\react-project\socialape-functions\functions\node_modules\busboy\lib\main.js:37:33)
at E:\react-project\socialape-functions\functions\node_modules\busboy\lib\types\multipart.js:52:13
at process._tickCallback (internal/process/next_tick.js:61:11)
Then i add storageBucket to admin function
var admin = require("firebase-admin");
var serviceAccount = require("your service account key file path);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://your-app-1234.firebaseio.com",
storageBucket: "your-storage-bucket-name given in firebase storage"
});
const db = admin.firestore();
module.exports = { admin, db };
After adding bucket to admin functions it works.
You should change
storageBucket: "your-storage-bucket-name given in firebase storage"
to
storageBucket: "<BUCKET_NAME>.appspot.com".
You can find BUCKET_NAME as the picture below:

Firebase: Update Firestore Fails If Document Does Not Exist [duplicate]

This is my very basic Cloud Function:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore()
exports.createdNewAccount = functions.auth.user().onCreate(event => {
return db.collection('users').doc(event.data.uid).update({
"creationDate" : Date.now()
})
})
And I get the error
Error: no entity to update: app
What is wrong with my code?
Most likely, the document for event.data.uid does not exist. The documentation for update() states:
The update will fail if applied to a document that does not exist.
Use set() instead.
I faced a similar error when testing my app locally with the Firebase emulator. My firebase config file looked like:
import firebase from "firebase";
import "firebase/firestore";
const firebaseConfig = {
apiKey: <FIREBASE_API_KEY>,
authDomain: <FIREBASE_AUTH_DOMAIN>,
databaseURL: <FIREBASE_DB_URL>,
projectId: <FIREBASE_PROJECT_ID>,
storageBucket: <FIREBASE_STORAGE_BUCKET>,
messagingSenderId: <FIREBASE_MSG_SENDER_ID>,
appId: <FIREBASE_APP_ID>,
measurementId: <FIREBASE_MEASUREMENT_ID>,
};
// Initialize Firebase
const firebaseApp = firebase.initializeApp(firebaseConfig);
// for local instances, use the emulator to connect the frontend to firestore & functions
if (location.hostname === "localhost") {
firebase.firestore().settings({
host: "localhost:8080",
ssl: false,
});
firebase.functions().useFunctionsEmulator("http://localhost:5001");
}
Turns out my local Firestore database (expected to be running at localhost:8080) wasn't being hit. So my cloud functions were trying to write to a non-existent db. The underlying issue was that I also had my backend initializing to a different database:
// functions/index.js
const admin = require("firebase-admin");
var serviceAccount = require("./serviceAccount.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://other-firebase-project-id.firebaseio.com",
});
The solution (originally adapted from a Fireship tutorial) was to remove this [incorrect] re-initialization of the database altogether:
// functions/index.js
...
admin.initializeApp();
...
After all, according to the docs, we can initialize the Firebase Admin SDK without parameters since the FIREBASE_CONFIG environment variable is included automatically in Cloud Functions for Firebase functions that are deployed via the Firebase CLI.
FWIW, also be sure to set the correct Firebase project on the CLI. Doing so with this allows the Firebase CLI to hook Functions/Firestore to the right project:
firebase use <desired-firebase-project-id>
If you have multiple Firebase projects, you can list them out with: firebase projects:list

Use firebase auto SDK setup with Webpack

I am creating a web app that uses Vue webpack with firebase. I would like to have my firebase credentials automatically change when i use firebase use <some_alias> on the firebase cli. In other projects, this simply meant including the /__/firebase/init.js file of firebase hosting. In this project, I am using the npm firebase library and can load in a specific firebase set of credentials with
import firebase from 'firebase'
var config = {
apiKey: '...',
authDomain: '...',
databaseURL: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...'
}
firebase.initializeApp(config)
export default {
database: firebase.database,
storage: firebase.storage,
auth: firebase.auth
}
However, this does not get my credentials based on my current firebase workspace. Instead, I would like something like
import firebase from 'firebase'
const fbcli = require('firebase-tools');
export const getFirebaseInstance = () => {
return fbcli.setup.web().then(config => {
firebase.initializeApp(config)
return firebase
});
}
though synchronous. Is there any way to synchronously load in my firebase credentials?
This was solved by checking window.location.host when in the prod environment and having a production config object if the host was our production hostname and reading from the values of a configuration file otherwise.
Try using fs.writeFileSync as described in this example from a firebase blog post about reading credentials:
const fbcli = require('firebase-tools');
const fs = require('fs');
// by default, uses the current project and logged in user
fbcli.setup.web().then(config => {
fs.writeFileSync(
'build/initFirebase.js',
`firebase.initializeApp(${JSON.stringify(config)});`
);
});

Resources