Firestore admin in Node.js Missing or insufficient permissions - node.js

I am trying to access Firestore using Firebase Admin on Node.js v8.4.0 in a Firebase Cloud Function running locally using firebase functions:shell on Windows 10.
firebase -v 4.2.1
"dependencies": {
"firebase-admin": "^6.0.0",
"firebase-functions": "^2.0.5"
}
After attempting to use firebase admin from my apps code, I attempted to run the quick start example in https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase-firestore/functions/index.js.
This is the actual code run:
'use strict';
// [START all]
// [START import]
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp({})
// [END import]
// [START addMessage]
// Take the text parameter passed to this HTTP endpoint and insert it into the
// Realtime Database under the path /messages/:documentId/original
// [START addMessageTrigger]
exports.addMessage = functions.https.onRequest((req, res) => {
// [END addMessageTrigger]
// Grab the text parameter.
const original = req.query.text;
// [START adminSdkAdd]
// Push the new message into the Realtime Database using the Firebase Admin SDK.
return admin.firestore().collection('messages').add({original: original}).then((writeResult) => {
// Send back a message that we've succesfully written the message
return res.json({result: `Message with ID: ${writeResult.id} added.`});
});
// [END adminSdkAdd]
});
// [END addMessage]
// [START makeUppercase]
// Listens for new messages added to /messages/:documentId/original and creates an
// uppercase version of the message to /messages/:documentId/uppercase
// [START makeUppercaseTrigger]
exports.makeUppercase = functions.firestore.document('/messages/{documentId}')
.onCreate((snap, context) => {
// [END makeUppercaseTrigger]
// [START makeUppercaseBody]
// Grab the current value of what was written to the Realtime Database.
const original = snap.data().original;
console.log('Uppercasing', context.params.documentId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an 'uppercase' sibling in the Realtime Database returns a Promise.
return snap.ref.set({uppercase}, {merge: true});
// [END makeUppercaseBody]
});
// [END makeUppercase]
// [END all]
However, I still get the permission denied error.
This is the output I get:
firebase > makeUppercase({original:'alphabets'},{params:{documentId:'mydoc'}})
'Successfully invoked function.'
firebase > info: User function triggered, starting execution
info: Uppercasing mydoc alphabets
info: Function crashed
info: { Error: 7 PERMISSION_DENIED: Missing or insufficient permissions.
at Object.exports.createStatusError (C:\projects\myproject\functions\node_modules\grpc\src\common.js:87:15)
at Object.onReceiveStatus (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:1188:28)
at InterceptingListener._callNext (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:564:42)
at InterceptingListener.onReceiveStatus (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:614:8)
at callback (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:841:24)
code: 7,
metadata: Metadata { _internal_repr: {} },
details: 'Missing or insufficient permissions.' }
My security rules are completely open but that did not resolve the error.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
I also thought that this might be an authentication issue so I have tried the following to initialize the app:
1
admin.initializeApp()
2
admin.initializeApp(functions.config().firebase);
3
var serviceAccount = require('path/to/serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});
The last one with my own credentials file and project configuration. All of these attempts still give me the missing permissions error.
Update:
I deployed these functions to the cloud and they seem to be working perfectly but when running locally, I'm still getting a Error:7 Permission Denied.
Update 2:
Set application default credentials using gcloud auth application-default login as per suggestion by #Doug Stevenson. Ensured environment variable GOOGLE_APPLICATION_CREDENTIALS is not set. Attempted the code in 1,2 and 3 above as well as 4 below with no success. Encountered the same error.
4
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: "https://myapp-redacted.firebaseio.com"
});

I hope that by now you've already resolved your issue. The same thing just happened to me and I'm sharing what it was in my case with the hope that it will help others.
We manage several firebase projects - productions, dev, staging etc.
we init the admin sdk with this:
let serviceAccount = require("../serviceAccountKey.json");
const databaseUrl = functions.config().environment.databaseurl
const storageBucket = functions.config().environment.storagebucket
const isDev = "true" === functions.config().environment.dev
// if dev environment
if (isDev) {
serviceAccount = require("../serviceAccountKey-dev.json");
}
admin.initializeApp({
projectId: functions.config().environment.projectid,
credential: admin.credential.cert(serviceAccount),
databaseURL: databaseUrl,
storageBucket: storageBucket
});
You know how you need to do
firebase functions:config:get > .runtimeconfig.json
for the emulation to work. Well, my runtimeconfig was containing the wrong configuration. I was loading my serviceAccountKey-dev, but I was trying to access a different project. The second I fixed my runtimeconfig - it worked for me.

I had the same issue and was resolved by going to the cloud console then granting the role Firebase Admin SDK admin service agent to the app engine service account which is in the following format {PROJECT_ID}#appspot.gserviceaccount.com.

Ultimately I just had to run gcloud auth application-default login to make sure I was logged in with the correct Google account.

It is required that the client, the firebase function, to have access to the resource, firebase firestore. Following the least privilege principle you would need to:
Create a role within Google Cloud IAM with the following permissions:
datastore.entities.get, datastore.entities.update.
Also in IAM, create a service account and assign it the recently created role.
Update your firebase function selecting the new service account.
I have not found a way to assign the service account while deploying with firebase-cli. Here a guide for configuring permissions of the functions https://cloud.google.com/functions/docs/securing/function-identity.

Related

Firebase Scheduled Functions Not Fully Deploying

I have been trying to deploy the following function for days. This is the code and the errors. It appears on my google cloud platform but it is not working properly.
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
const database = admin.firestore();
// prettier-ignore
exports.schFunc = functions.region("us-east1").pubsub
.schedule("every 1 minutes")
.onRun((context) => {
database
.doc("timers/timer1")
.update({time: admin.firestore.Timestamp.now()});
return console.log("succesful timer update");
});
These are the errors.
<<< HTTP RESPONSE BODY {"error":{"code":400,"message":"Any resource that needs App Engine can only be created/updated in the App Engine region. Location must equal us-east1 because the App Engine app that is associated with this project is located in us-east1.","status":"INVALID_ARGUMENT"}}
<<< HTTP RESPONSE BODY {"error":{"code":404,"message":"Job not found.","status":"NOT_FOUND"}}
And this is what is showing when I deploy.
Error: Failed to upsert schedule function schFunc in region us-east1
Error: There was an error deploying functions

retrieve secret firebase service account json

I have this code in nextjs that is supposed to check if a token is valid then sign in the user.
const firebaseAdmin = require("firebase-admin");
const serviceAccount = require ('../secret.json');
export const verifyIdToken = async (token) => {
if (!firebaseAdmin.apps.length) {
firebaseAdmin.initializeApp({
// credential: firebaseAdmin.credential.cert(serviceAccount),
credential: firebaseAdmin.credential.applicationDefault(),
databaseURL: "rtdb.firebaseio.com",
});
}
return await firebaseAdmin
.auth()
.verifyIdToken(token)
.catch((error) => {
throw error;
});
};
I have the windows environment variables set as firebase recommends and switched to using the applicationDefault() since as I understand,
ADC can automatically find your credentials
Problem is the application works only locally. When I deploy the website, the token is not verified and creates errors. I am serving the NextJs app through a cloud function. How can I solve this.
The error is
auth/invalid-credential
Must initialize app with a cert credential or set your Firebase project
ID as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().
What the app is supposed to do is do a check server side to determine if a token is valid.
As below
export async function getServerSideProps(ctx) {
try {
const cookies = nookies.get(ctx);
const token = await verifyIdToken(cookies.token);
// the user is authenticated!
const { uid, email } = token;
return {
props: {
userData: {
email: email,
uid: uid,
},
},
};
} catch (err) {
console.log(err.code)
console.log(err.message)
return { props: {
} };
}
}
The auth/invalid-credential error message means that the Admin SDK needs to be initialized, as we can see in the Official Documentation.
The credential used to authenticate the Admin SDKs cannot be used to
perform the desired action. Certain Authentication methods such as
createCustomToken() and verifyIdToken() require the SDK to be
initialized with a certificate credential as opposed to a refresh
token or Application Default credential.
And for the ID token verification, a project ID is required. The Firebase Admin SDK attempts to obtain a project ID via one of the following methods:
If the SDK was initialized with an explicit projectId app option, the SDK uses the value of that option.
If the SDK was initialized with service account credentials, the SDK uses the project_id field of the service account JSON object.
If the GOOGLE_CLOUD_PROJECT environment variable is set, the SDK uses its value as the project ID. This environment variable is available for code running on Google infrastructure such as App Engine and Compute Engine.
So, we can initialize the Admin SDK with a service (and fulfill the second option); but, the first thing to do is authenticate a service account and authorize it to access Firebase services, you must generate a private key file in JSON format.
To generate a private key file for your service account you can do the following:
In the Firebase console, open Settings > Service Accounts.
Click Generate New Private Key, then confirm by clicking Generate Key.
Securely store the JSON file containing the key.
Once you have your JSON file, you can set a environment variable to hold your private key.
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"
And then, use it in your code like this:
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});
In the end I downloaded Gcloud tool and setting the GOOGLE_APPLICATION_CREDENTIALS environment variable from the tool worked. The function could then work with credential: firebaseAdmin.credential.applicationDefault(),

How to access a Google Firestore collection from within a Cloud Function referencing a different Firestore collection?

The following (from a React site) is not working . I've been in the docs for hours without success. Any ideas?
import firebase = require("../../node_modules/firebase");
import * as functions from "firebase-functions";
exports.onSomeCollectionCreate = functions
.firestore
.document("some-collection/{someCollectionId}")
.onCreate(async(snap, context) => {
firebase
.firestore()
.collection("another-collection/{anotherCollectionId}")
.add({ some: "data" });
}
);
Some terminal feedback:
⚠ functions[onSomeCollectionCreate(region)]: Deployment error.
Thank you for reading.
In a Cloud Function, you should use the Admin SDK in order to interact with the Firebase services, see the doc for more details.
The following should therefore work:
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access Cloud Firestore.
const admin = require('firebase-admin');
admin.initializeApp();
exports.onSomeCollectionCreate = functions
.firestore
.document("some-collection/{someCollectionId}")
.onCreate(async(snap, context) => {
return admin. // note the return
.firestore()
.collection("another-collection")
.add({ some: "data" });
}
);
Note two additional points:
You should not pass to the collection() method a string with a slash (/), since Collection references must have an odd number of segments.
Note that we return the Promise returned by the add() method. See the doc here for more details on this key point.

initializeApp when adding firebase to app and to server

I'm using firebase for auth and db, and AWS lambda for cloud functions.
To add firebase to my JS project, I initializeApp with the firebase config as parameter, as documented here : https://firebase.google.com/docs/web/setup.
As documented here : https://firebase.google.com/docs/admin/setup, I also need to initializeApp in my lambda function.
Something as follows here :
const admin = require('firebase-admin');
const serviceAccount = require('../path/to/service-account.json');
const firebaseAdmin = admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "dB_URL"
});
The credentials come from the firebase-admin library, so I cannot add this to my web firebase config. So, I need to initialize twice.
However, if I proceed like this, the server will throw an error :
The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once.But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.
Am I missing something here ? What's the best practice ? I'm confused.
Someone faced the same issue before it seems : Use Firebase SDK with Netlify Lambda Functions
What worked for this user was to use the REST API as documented here : https://firebase.google.com/docs/projects/api/workflow_set-up-and-manage-project
The documentation says it's in beta though.
Thanks for your kind help
It seems that lambda may load the script file that calls admin.initializeApp({...}) multiple times. To prevent the Admin SDK from initializing multiple times, you can for example detect if that already happened:
if (!admin.apps.length) {
const firebaseAdmin = admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "dB_URL"
});
}
Have similar problem. Based on answer from #Frank van Puffelen ( Thanks to him ), Now a days, Using ES6 ( Firebase admin V >= 9 )
import { initializeApp, applicationDefault, getApps } from "firebase-admin/app";
...
if ( !getApps().length ) {
initializeApp({
credential: applicationDefault(),
databaseURL: 'https://XXXXXXX.firebaseio.com'
});
}
...
An alternative in the simplest case might be something like this:
import { initializeApp, getApp } from "firebase-admin/app";
function appSingleton () {
try {
return getApp()
} catch (err) {
return initializeApp()
}
}
try removing const firebaseAdmin = ... since you can already use admin to call firebase services. Just use
admin.initializeApp(
{
credential: admin.credential.cert(serviceAccount),
databaseURL: "dB_URL"
});
Then, you are good to go.

How to include the Google Cloud's JSON Project Key file in a Google Cloud Function?

I have a Google Cloud Function that works well and I want, once executed before the callback, to connect to Firestore to add a document to my Notifications collection.
const Firestore = require('#google-cloud/firestore');
const firestore = new Firestore({
projectId: 'my-firebase-project',
keyFilename: 'thekey.json',
});
var fsDocument = {
'username': 'theuser',
'organization': 'theorg',
'attr': [
{k:'key1', v:'val1'},
{k:'key2', v:'val2'}
]
};
firestore.collection('Notifications').add(fsDocument).then(documentReference => {
console.log('Added document with name' + documentReference.id);
});
How can I include the key file to my google cloud function? So far, I am creating them in console.cloud.google.com.
All files in your functions directory will be sent to Cloud Functions when you deploy. You could put your credentials in a file under functions, then refer to it with a relative path like this:
const firestore = new Firestore({
projectId: 'my-firebase-project',
keyFilename: './thekey.json',
});
You should only do this if your credentials are for a project different from the one running your Cloud Function. If you're trying to access Firestore in the same project as the one running your function, just use the default credentials using the Admin SDK. There are lots of examples of this in functions-samples.

Resources