Cloud function trigger on document update - node.js

I am trying to fire my cloud function if theres a document update on users/{userId}
I have a signIn method that's fired everytime a user logs in
signin: (email, password, setErrors) => {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(() => {
const isVerified = firebase.auth().currentUser.emailVerified
const userUid = firebase.auth().currentUser.uid
const db = firebase.firestore()
if (isVerified) {
db.collection('/users')
.doc(userUid)
.update({ isVerified: true })
}
})
.catch(err => {
setErrors(prev => [...prev, err.message])
})
},
Nothing fancy, aside from the basic log in stuff, it also checks if the user has verified their email, if it is verified it will update the collection for that user. Everything is working as intended here.
However I can't seem to get my cloud function to fire.
Basically, it's listening for changes on ther user collection. If users/{userId} document has isVerified and the user email address ends with xxxx it should grant them admin privledges.
exports.updateUser = functions.firestore.document('users/{userId}').onUpdate((change, context) => {
const after = change.after.data()
if (after.isVerified) {
firebase.auth().onAuthStateChanged(user => {
if (user.emailVerified && user.email.endsWith('#xxxx')) {
const customClaims = {
admin: true,
}
return admin
.auth()
.setCustomUserClaims(user.uid, customClaims)
.then(() => {
console.log('Cloud function fired')
const metadataRef = admin.database().ref('metadata/' + user.uid)
return metadataRef.set({ refreshTime: new Date().getTime() })
})
.catch(error => {
console.log(error)
})
}
})
}
})
Right now the function is not firing, any ideas?

Code in Cloud Functions runs as an administrative account, which cannot be retrieved with the regular Firebase Authentication SDK. In fact, you should only be using Firebase Admin SDKs in your Cloud Functions code, in which the onAuthStateChanged method doesn't exist.
It's not entirely clear what you want this code to do with the user, but if you want to check whether the user whose uid is in the path has a verified email address, you can load that user by their UID with the Admin SDK.
To ensure that the uid in the path is the real UID of the user who performed the operation you can use security rules, as shown in the documentation on securing user data.

I support what Frank said.
You can fetch users with admin in this way:
if (after.isVerified) {
admin.auth().getUser(userId)
.then((userData) => { ...

Related

Is there a way to tell if a user is registered with phone number or email & password on Firebase Auth

I am building a React App with Firebase back end.
I have two types of users, Admin and Standard users. The admins are created by a Super Admin using email and password and their phone numbers is set to their profile. The Standard user registers using phone number. I would like to send a different registration success SMS for the Admin and the standard user using Firebase cloud functions with the onCreate auth trigger.
Is there a way to tell which user was created through Admin SDK and which one registered through phone Auth.
My create Admin function is as below.
exports.addAdmin= functions.https.onCall((data, context) => {
return admin.auth().createUser({
email: data.email,
password: data.password,
displayName: data.name,
phoneNumber: data.phone,
disabled: false,
emailVerified: true
})
});
You can check if email and phoneNumber are defined in user object of onCreate() function like this:
export const onUserCreate = auth.user().onCreate(async (user) => {
if (user.phoneNumber) {
// User has phone number
}
if (user.email) {
// User has email
}
});
If both admin created and standard users can have email-password auth setup and you want check if they were created by a super admin or no, then you'll have to store the source in a database or custom claims. Try:
export const addAdmin = https.onCall(async (data, context) => {
const user = await getAuth().createUser({
email: "dharmaraj.r244#gmail.com",
password: "data.password",
});
// Add 'source' custom claim
await getAuth().setCustomUserClaims(user.uid, {
source: "admin",
});
return 'User created';
});
export const onUserCreate = auth.user().onCreate(async (user) => {
const { customClaims } = await getAuth().getUser(user.uid);
if (customClaims.source === "admin") {
console.log("User created by super admin")
}
return null;
});

How to set custom auth claims through Firebase and identify platform

I am following the firebase documentation here to set custom auth claims for users logging into my app for the first time using firebase auth + identify platform but it does not seem to be working.
When a user logs in for the first time, I want them to get the admin custom claim. I have created the following blocking function and have verified from the logs that it runs when I log in for the first time to my app using sign-in with google:
exports.beforeCreate = functions.auth.user().beforeCreate((user, context) => {
return {
customClaims: {
admin: true,
},
};
});
I would expect this to create the admin custom claim in the user's token. However, when I get a list of claims using another cloud function the admin claim does not appear.
exports.getclaims = functions.https.onRequest(async (req, res) => {
const uid = req.query.uid as string;
if (uid) {
const user = await admin.auth().getUser(uid);
res.send(user.customClaims);
} else {
res.sendStatus(500);
}
});
If I set the claim using the admin SDK directly using the below cloud function, the admin claim does appear.
exports.setclaim = functions.https.onRequest(async (req, res) => {
const uid = req.query.uid as string;
if (uid) {
await admin.auth().setCustomUserClaims(uid, {admin: true});
res.sendStatus(200);
} else {
res.sendStatus(500);
}
});
What am I doing wrong in the beforeCreate function?
There's an open GitHub issue regarding that. See sessionClaims content not getting added to the decoded token. Also, there's a fix that has been recently merged regarding this issue.
From the snippet you provided, there does not appear to be anything wrong with beforeCreate as coded.
You may want to check you do not have a beforeSignIn that is overwriting the customClaims directly or via sessionClaims.
https://firebase.google.com/docs/auth/extend-with-blocking-functions#modifying_a_user
Try to use onCreate method instead of beforeCreate how it is shown on the official docs
functions.auth.user().onCreate(async (user) => {
try {
// Set custom user claims on this newly created user.
await getAuth().setCustomUserClaims(user.uid, {admin: true});
// Update real-time database to notify client to force refresh.
const metadataRef = getDatabase().ref('metadata/' + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
await metadataRef.set({refreshTime: new Date().getTime()});
} catch (error) {
console.log(error);
}
}
});
The main point here is that you need to create the user at first and then update claims and make the force update of the token at the client side:
firebase.auth().currentUser.getIdToken(true);

How to get an idToken that is valid for development from firebase without having to spin up my frontend?

I am working on some firebase functions. This one will check if an user is logged in in firebase first. However this is a bit of a hassle in development. Now I need to login on the frontend first to get the id_token, pass it to my function url, then see the result.
The process I am following is described in the official docs: https://firebase.google.com/docs/auth/admin/verify-id-tokens
node.js
const admin = require('firebase-admin');
admin.initializeApp();
module.exports = function( request, response ) {
if( !request.query.id_token )
response.status(400).json({message: 'id token has not been provided'});
admin.auth()
.verifyIdToken( request.query.id_token )
.then( token => {
// TODO: redirect to payment portal
return response.status(200).json({message: 'Success'});
})
.catch( error => {
return response.status(401).json({message: 'You are currently not logged in as an authorised user'});
})
}
Is there a way to get an id_token that is valid from firebase without having to spin up my frontend? Good and simple alternatives solutions are welcome too.
NOTE: I am using the firebase emulators during development.
Since you're using the Firebase emulators you may create a fake user and retrieve an id token programmatically. The code below creates and logs in a user and returns an id_token that will be accepted by your function.
var firebase = require("firebase/app");
require("firebase/auth");
// Initialize Firebase and connect to the Authentication emulator
var firebaseConfig = {
// Insert Firebase config here
};
firebase.initializeApp(firebaseConfig);
firebase.auth().useEmulator('http://localhost:9099/');
// Create a fake user and get the token
firebase.auth().createUserWithEmailAndPassword("example#example.com", "password")
.then((userCredential) => {
console.log("User created")
});
firebase.auth().signInWithEmailAndPassword("example#example.com", "password")
.then((userCredential) => {
console.log("User logged in")
userCredential.user.getIdToken().then((idToken) => {
console.log(idToken)
});
});

writing to firestore after authenticating

Im creating a react native application using expo and I'm currently trying to connect the firestore database to the application. When I run this in a function it doesn't write to the database but it does create a authenticated user.
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then((result) => {
firebase
.firestore()
.collection('users')
.doc(result.user.uid)
.set({ uid: result.user.uid });
})
.catch((err) => console.log(err));
I found a way around this so that it does write to the database but wasn't sure if this is an acceptable way
firebase
.auth()
.signInWithCredential(credential)
.catch(function (error) {
console.log(error);
});
firebase.auth().onAuthStateChanged((user) => {
if (user) {
console.log('adding to db');
firebase
.firestore()
.collection('users')
.doc(user.uid)
.set({ uid: user.uid });
} else {
// No user is signed in.
}
Any advice is appreciated !
Thank you
but wasn't sure if this is an acceptable way
This is not only acceptable, but it's currently the right way. The problem is that the Firebase Auth SDK doesn't set the current user before the promise resolves from the call to signInWithCredential. Without a current user set, the Firestore SDK doesn't know what credentials to use for a query. However, when onAuthStateChanged delivers a user object, you know for certain that the current is user is set.

do something 'before' login in loopback

I am pretty new to loopback and here is what I am doing:
I am using standard login route provided by the loopback to log in the users - extended base Users to my own model say orgadmin.
With prebuilt route /api/orgadmin/login, I can easily login.
Now, I have a flag in orgadmins say 'status' which can be either 'active' or 'inactive' based on which I have to defer user login.
I was thinking something with remote hooks like beforeRemote as below but it doesn't work:
//this file is in the boot directory
module.exports = function(orgadmin) {
orgadmin.beforeRemote('login', function(context, user, next) {
console.log(user)
// context.args.data.date = Date.now();
// context.args.data.publisherId = context.req.accessToken.userId;
next();
});
};
So what is the best way to accomplish this?
The user attribute will only be available if the request is coming with a valid access token. The attribute is unused for unauthenticated requests, which login is.
Here's a possible alternative:
module.exports = (OrgAdmin) => {
OrgAdmin.on('dataSourceAttached', () => {
const { login } = OrgAdmin;
OrgAdmin.login = async (credentials, include) => {
const accessToken = await login.call(OrgAdmin, credentials, include);
const orgAdmin = await OrgAdmin.findById(accessToken.userId);
if (orgAdmin.status !== 'active') {
OrgAdmin.logout(accessToken);
const err = new Error('Your account has not been activated');
err.code = 'NOT_ACTIVE_USER';
err.statusCode = 403;
throw err
}
return accessToken;
};
});
};
The above code overrides the login method and does the following:
Login the user, using loopback's built-in login
Take the response of login, which is an access token, and use it to get the user.
If the user is active, return the access token, satisfying the expected successful response of login.
If the user is not active, remove the access token that was created (which is what logout does), and throw an error.

Resources