Generating a custom auth token with a cloud function for firebase using the new 1.0 SDK - firebase

As of firebase-admin#5.11.0 and firebase-functions#1.0.0 firebase-admin no longer takes in an application config when the app initializes.
I had a firestore function that would generate a custom token using firebase-admin’s createCustomToken. Calling that function would generate a credential that I would pass into initializeApp in the credential attribute. How would I go about doing that now?
Do I need to edit process.env.FIREBASE_CONFIG somehow and put the serialized credential there before calling initializeApp?

Based on this issue in Github, it still works.
https://github.com/firebase/firebase-admin-node/issues/224
The following example worked for me:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://yourapplication.firebaseio.com/'
});
exports.createToken = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
return admin.auth()
.createCustomToken(uid)
.then(customToken => {
console.log(`The customToken is: ${customToken}`);
return {status: 'success', customToken: customToken};
})
.catch(error => {
console.error(`Something happened buddy: ${error}`)
return {status: 'error'};
});
});

Michael Chen's cloud function appears to trigger from a HTTP request from somewhere (an external server?). My employee wrote a cloud function that triggers when the user logs in:
// this watches for any updates to the user document in the User's collection (not subcollections)
exports.userLogin = functions.firestore.document('Users/{userID}').onUpdate((change, context) => {
// save the userID ubtained from the wildcard match, which gets put into context.params
let uid = context.params.userID;
// initialize basic values for custom claims
let trusted = false;
let teaches = [];
// check the Trusted_Users doc
admin.firestore().collection('Users').doc('Trusted_Users').get()
.then(function(doc) {
if (doc.data().UIDs.includes(uid)) {
// if the userID is in the UIDs array of the document, set trusted to true.
trusted = true;
}
// Get docs for each language in our dictionary
admin.firestore().collection('Dictionaries').get()
.then(function(docs) {
// for each of those language docs
docs.forEach(function(doc) {
// check if the userID is included in the trustedUIDs array in the doc
if (doc.data().trustedUIDs.includes(uid)) {
// if it is, we push the 2-letter language abbreviation onto the array of what languages this user teaches
teaches.push(doc.data().shortLanguage);
}
});
// finally, set custom claims as we've parsed
admin.auth().setCustomUserClaims(uid, {'trusted': trusted, 'teaches': teaches}).then(() => {
console.log("custom claims set.");
});
});
});
});
First, we put in a lastLogin property on the user object, which runs Date.now when a user logs in and writes the time to the database location, triggering the cloud function.
Next, we get the userID from the cloud function response context.params.userID.
Two variables are then initialized. We assume that the user is not trusted until proven otherwise. The other variable is an array of subjects the user teaches. In a roles-based data security system, these are the collections that the user is allowed to access.
Next, we access a document listing the userIDs of trusted users. We then check if the recently logged in userID is in this array. If so, we set trusted to true.
Next, we go to the database and traverse a collection Dictionaries whose documents include arrays of trusted userIDs (i.e., users allowed to read and write those documents). If the user is in one or more of these arrays, he or she gets that document added to the teaches property on his or her user data, giving the user access to that document.
Finally, we're ready to run setCustomUserClaims to customize the token claims.

Here's a variation for a Callable Cloud Function, thanks to Thomas's answer
Once the custom claim is set, you can access the field in/from .. say, a firebase storage rule.
For example:
allow write: if request.auth.token.isAppAdmin == true;
With a Callable Cloud Function, as long as the admin.auth().setCustomUserClaims(..) function is returned somewhere along the promise chain, the claim field will be added to the request.auth.token object:
const functions = require('firebase-functions');
exports.setIsAdminClaim = functions.https.onCall((data, context) => {
var uid = context.auth.uid;
return admin.auth().setCustomUserClaims(
uid, {
isAppAdmin: true
}
)
.then(() => {
var msg = 'isAppAdmin custom claim set';
console.log(msg);
return new Promise(function (resolve, reject) {
var resolveObject = {
message : msg
};
resolve(resolveObject);
});
});
});

Related

Firebase: Firestore query on onAuthStateChanged() works only on page reload

In a Vue app, in a separate JS file in my SRC folder, I have a function that does 3 things: first it listens to auth changes using onAuthStateChanged(), then it takes the user id from the signed-in user and queries the related Firestore user document, and finally it send the user document as an object to the Vuex store (as described in the 3 steps below).
const listenToAuthStateAndChanges = () => {
const auth = getAuth();
//STEP 1, listen to auth changes
onAuthStateChanged(auth, (user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
const uid = user.uid;
//STEP 2, retrieve user doc from firestore based on the id above
const q = query(collection(db, "users"), where("userid", "==", uid));
async function getUserDoc() {
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
//STEP 3, store user doc info in Vuex store
store.state.userInfo = doc.data();
});
}
getUserDoc();
} else {
//user not signed in
}
});
}
This function above is then imported in the mounted hook of my Main-Header component:
export default {
mounted() {
//imported from auth.js
listenToAuthStateAndChanges()
},
My problem is that, when I sign a new user up (in a different signup component), the code from the function above stops running right after const q is declared. This means it detects the auth change, but it does not run the getUserDoc() function automatically. If I refresh the page, the getUserDoc() runs correctly and the Vuex store updates. There must be something obvious that I don't see here. Thank for any help!

Cloud Functions, deleting Firestore SubCollections, is AdminToken necessary?

I am trying to build callable cloud functions, when users delete a post, it also try to delete the comments, which is a sub-collection of the post. so I saw the example and implemented just like a documentation example
const admin = require('firebase-admin');
const firebase_tools = require('firebase-tools');
const functions = require('firebase-functions');
admin.initializeApp({
serviceAccountId: 'xxxxxx-xxxxx#appspot.gserviceaccount.com'
}
);
exports.mintAdminToken = functions.https.onCall(async (data: any, context: any) => {
const uid = data.uid;
const token = await admin
.auth()
.createCustomToken(uid, { admin: true });
return { token };
});
exports.recursiveDelete = functions
.runWith({
timeoutSeconds: 540,
memory: '2GB'
})
.https.onCall(async (data: any, context: any) => {
// Only allow admin users to execute this function.
if (!(context.auth && context.auth.token && context.auth.token.admin)) {
throw new functions.https.HttpsError(
'permission-denied',
'Must be an administrative user to initiate delete.'
);
}
const path = data.path;
console.log(
`User ${context.auth.uid} has requested to delete path ${path}`
);
await firebase_tools.firestore
.delete(path, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true,
token: functions.config().fb.token
});
return {
path: path
};
});
and I succeeded in receiving the custom token to the client. but what I have to do now? after getting token I called the "recursiveDelete" function from client but it occurs error PERMISSION_DENIED
Should the user who received the token be initialized with a new custom admin token? (if I misunderstand let me know)
Is the admin token really necessary when deleting a sub collection like this? It's difficult to use, so I ask.
I don't think that you really need a custom token for this use case and I suggest that you use firebase firestore rules rather than implementing your own role based authentication.
Steps to follow:
1- create a collection that you may call "users" and include in it a field of the role that this user may have such as "ADMIN". every document id in this collection can be the auth uid of users that firebase auth generates. you can get this uid from your frontend by using the currentUser prop and it's all explained here
2- protect your database with firestore rules as such:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// only admins can remove posts
match /posts/{postID} {
allow read, write: if isAdmin();
}
// only admins can remove comments
match /comments/{commentID} {
allow read, write: if isAdmin();
}
// this function will check if the caller has an admin role and allow or disallow the task upon that
function isAdmin() {
return get(/databases/$(database)/documents/
users/$(request.auth.uid)).data.role == "ADMIN";
}
}
}
3- after you succefully deletes a post document you can create a function with onDelete trigger that get invoked and deletes the comments subcollection recursivley and to do that you should include this bit of code:
const client = require('firebase-tools');
exports.recursiveDelete = functions.firestore
.document('posts/{postID}')
.onDelete((snap, context) => {
.....
await client.firestore
.delete(collectionPath, {
project: process.env.GCLOUD_PROJECT,
recursive: true,
yes: true
});
}

Is there a way to get email or text notifications each time data is written to my Google Cloud Firestore bucket?

I have a google cloud bucket and firebase writes my app data there. I would like to monitor my data, and have any new update (write) to my firebase database it sent via a text or email to me. I currently have Twilio set up on Nodejs to send texts on Firebase and my code is:
const functions = require('firebase-functions');
var twilio = require('twilio');
const admin = require('firebase-admin');
admin.initializeApp();
var accountSid = 'account id'; // Account SID from www.twilio.com/console
var authToken = 'account token'; // Auth Token from www.twilio.com/console
var client = new twilio(accountSid, authToken);
exports.useWildcard = functions.firestore
.document('comments/{commentContent}')
.onWrite((change, context) => {
client.messages.create({
body: context.params.commentContent,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
Currently, I would like to build it out for just the comments document, which are organized inside firebase through comments/{commentContent}. Later, I would like to expand to other trees. I am however, unsure if the above will run each time there is a write to my comments tree. Does it require the firebase-admin module as I have put above? Thanks!
Yes, the onWrite method will not only run when there is a write to the comments tree, but will also be triggered by any change in any document and on the deletion of a document. This means that right now your code will responde in the same way to any of the above cases, and this could cause problems, especially in the case of a document being deleted since it will try to send a comment that doesent exist and will likely get some null exceptions.
Said that you have different solutions.
If you only want the function to react to a new comment, but not to an update or deletion you should use onCreate trigger instead of onWrite.
If you also want to handle a comment update notification you can use both onCreate and onUpdate, but sending different messages by doing something like:
exports.useWildcardCreate = functions.firestore
.document('comments/{commentContent}')
.onCreate((change, context) => {
client.messages.create({
body: context.params.commentContent,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
exports.useWildcardUpdate = functions.firestore
.document('comments/{commentContent}')
.onUpdate((change, context) => {
const newComment = change.after.data();
const previuosComment = change.before.data();
client.messages.create({
body: 'The comment ${previuosComment} has been changed to ${newComment}',
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
At last if you also need to notify when a comment has been deleted you should use onWrite method but differentiating between the 3 different cases as shown below:
exports.useWildcard = functions.firestore
.document('comments/{commentContent}')
.onWrite((change, context) => {
var textBody;
const oldComment = change.before.data();
const newComment = change.after.data();
if (change.after.exists == false) { // comment has been deleted
textBody = 'The comment ${oldComment} has been deleted';
}
else if (oldComment != newComment) { // comment has been updated
textBody = 'The comment ${oldComment} has been changed to ${newComment}';
}
else { // if its not an update or a deletion its a new comment
textBody = newComment;
}
client.messages.create({
body: textBody,
to: '+15555555555', // Text this number
from: '+15555555556' // From a valid Twilio number
})
.then((message) => console.log(message.sid));
});
Finally require('firebase-admin') is needed since it will allow you to interact with Firebase from privileged environments. Here you can find all the information to the Firebase Admin SDK

How to complete login only after functions.auth.user().onCreate is finished

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)
AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;

Within a firebase project, can I update values in the firestore database from the real time database

There is an integer value in the my real time database that I'd like to have sync'd with an integer value in my firestore database. The realtime database is fed through an external source and when it gets an update, I'd like that pushed to the firestore database
Here's what I have so far, I am able to access the value in the realtime database but not the firestore database.
=============== Data Structure===================
Real Time database
user1:
{ meter : 20 }
Firestore database
Collection: Users
{Document : user1
{ meter : 20 }}
/// =============Code Sample ======================================
const functions = require('firebase-functions');
// Initialize the Firebase application with admin credentials
const admin = require('firebase-admin');
admin.initializeApp();
// Define user sync method
exports.meterSync = functions.database.ref('/user1/meter').onUpdate( (change, context) => {
// Get a reference to the Firestore document of the changed user
var userDoc = admin.firestore().doc(`user/${context.params.user1}`);
const meterReading = change.after.val();
console.log(meterReading);
console.log(userDoc); /// Not able to access this
return null
});
My expectation is that user doc will give me the document, so I can update the fields within it. But I am getting a documentReference object, not sure how to access the meter field.
By doing
var userDoc = admin.firestore().doc(`user/${context.params.user1}`);
You actually defined a DocumentReference.
You have to use this DocumentReference to write to the Firestore database, using the set() or update() methods.
Here is a code using the set() method:
exports.meterSync = functions.database
.ref('/{userId}/meter')
.onUpdate((change, context) => {
const meterReading = change.after.val();
console.log(meterReading);
// Get a reference to the Firestore document of the changed user
const userDoc = admin.firestore().doc(`user/${context.params.userId}`);
return userDoc.set(
{
meter: meterReading
},
{ merge: true }
);
});
You will need to use cloud functions for this, lets say, you have one background function:
export const one = functions.firestore.document('users').onWrite((change, context) => {
// ... Your code here
})
export const two = functions.database.ref('/users/{userId}')
.onCreate((snapshot, context) => {
// ... you code here
})
Since you have access to the firebase sdk admin in there you basically can achieve your goal. You can read more about it here

Resources