Firebase onCreate add custom claims to the auth.user - reactjs

I am trying to add a custom claims, isRegistered to firebase. My firestore has another user collection to keep register info records. Now I am trying to keep a isRegistered custom claim but I can not seem to get it work.
exports.addRegisteredRole = functions.database.ref('/user')
.onCreate((snap, context) => {
return // **I added this later, but the issue still remains.**
admin.auth()
.setCustomUserClaims(context.auth.uid, {isRegistered: true})
.then(() => {
console.log('done', snap)
return {
message: 'done',
data: snap
}
})
.catch(err => {
console.log('something went wrong', err);
return err
})
});
I am checking this claim by,
currentUser.getIdTokenResult()
.then(res => {
console.log(res.claims.isRegistered)
})
(auth user object). Even if I re-logged it remains undefined. Am I doing something wrong, I am very new to firebase.

The issue is with your onCreate trigger. You assumed you're getting the uid in the context.auth object, which is not correct.
The onCreate trigger will be triggered automatically on the addition of a new document in your "user" collection. In this case, the context.aut.uid is undefined. You should trace this in your function logs.
You can achieve what you are trying to do in a couple of ways
If the user record document has the the user Id as the name of the document, you can do the following
exports.addRegisteredRole =
functions.firestore
.document('test/{docId}')
.onCreate((snap, context) => {
admin.auth()
.setCustomUserClaims(snap.id, { isRegistered: true })
.then(() => {
console.log('done', snap)
return {
message: 'done',
data: snap
}
})
.catch(err => {
console.log('something went wrong', err)
return err
})
})
If you're naming the user record document with something else, or letting firebase decide the name for you, then you must have the uid as an attribute in the document data. Then you use docSnapshot.data().uid instead of docSnapshot.id as follows
exports.addRegisteredRole =
functions.firestore
.document('test/{docId}')
.onCreate((snap, context) => {
admin.auth()
.setCustomUserClaims(snap.data().uid, { isRegistered: true })
.then(() => {
console.log('done', snap)
return {
message: 'done',
data: snap
}
})
.catch(err => {
console.log('something went wrong', err)
return err
})
})
Good luck

I suspect that your Cloud Function gets terminated before the call to Auth completes. The reason for that is that you're not returning anything from the top-level of your code, which means that Cloud Functions assumes that your code is done once the final } runs. But since the call to `` is asynchronous, that call hasn't completed yet and for example your console.log('done', snap) won't have run yet.
exports.addRegisteredRole = functions.database.ref('/user')
.onCreate((snap, context) => {
return admin.auth()
.setCustomUserClaims(context.auth.uid, {isRegistered: true})
.then(() => {
console.log('done', snap)
return {
message: 'done',
data: snap
}
})
.catch(err => {
console.log('something went wrong', err);
return err
})
});

I think you should use functions.firestore.document with wildcard to trigger your "user" documents:
exports.addRegisteredRole = functions.firestore.document('/user/{userId}')
.onCreate((snap, context) => { ... });

Most likely the problem is that you didn't propagate the new custom claims to the client. See here: https://firebase.google.com/docs/auth/admin/custom-claims#propagate_custom_claims_to_the_client
This github issue points that out as well: https://github.com/firebase/firebase-tools-ui/issues/424
If you log out and log back in with the user, it should work. Simply refreshing the page for a logged in user doesn't refresh the user's ID token.
As setting custom claims in the onCreate trigger is a common use case, it would be nice to have a note on this in the docs (https://firebase.google.com/docs/auth/admin/custom-claims) specifically for firestore, as the given example is for the realtime db.

Related

How to make asynchronous calls inside of a firestore .forEach loop?

Im using the firebase admin SDK server-side, and in one of my routes, im getting a collection, and then looping over each document in said collection...during each iteration, i use a property value to do a second get(). in the then() of this secondary get(), i again use data from the initial get to make final, tertiary get().
unfortunately the asynchronous nature of these nested calls seems to be creating undesirable outcomes.
heres the route function:
router.get('/list', authorization, (req, res) => {
console.log('/reports/list entered...')
admin
.firestore()
.collection('user-reports')
.get()
.then(querySnapshot => {
const reports = []
querySnapshot.forEach(snapshotDocument => {
const closed = snapshotDocument.get('closed')
console.log(`closed status: ${closed}`)
if (closed === false) {
const data = snapshotDocument.data()
console.log(`condition passed, data: ${JSON.stringify(data)}`)
// get # of reports made by sender
admin
.firestore()
.collection('users')
.doc(data.reportee)
.get()
.then(doc => {
data['reportee'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`first then => data; ${JSON.stringify(data)}`)
// get report history of reportee
admin
.firestore()
.collection('users')
.doc(data.reporter)
.get()
.then(doc => {
data['reporter'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`second then, ${JSON.stringify(data)}`)
reports.push(data)
})
.catch(err => {
return res.json({ error: true, message: err })
})
})
.catch(err => {
return res.json({ error: true, message: err })
})
}
})
console.log(`pre-response: ${JSON.stringify(reports)}`)
return res.json({ reports })
})
.catch(err => res.json({ error: true, message: err }))
})
what im logging is the "first condition passed", "pre-response: []", and "first then => data". by the time i ultimately return "reports" its empty. is there a more effective way to run firestore methods inside of foreach loops?
The solution almost always is to use Promise.all() to wait for a bunch of promises to resolve:
rturn Promise.all(querySnapshot.map(snapshotDocument => {
const closed = snapshotDocument.get('closed')
console.log(`closed status: ${closed}`)
if (closed === false) {
const data = snapshotDocument.data()
console.log(`condition passed, data: ${JSON.stringify(data)}`)
// get # of reports made by sender
return admin
.firestore()
.collection('users')
.doc(data.reportee)
.get()
.then(doc => {
data['reportee'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`first then => data; ${JSON.stringify(data)}`)
// get report history of reportee
return admin
.firestore()
.collection('users')
.doc(data.reporter)
.get()
.then(doc => {
data['reporter'] = {
reportActivity: doc.get('reportActivity')
}
console.log(`second then, ${JSON.stringify(data)}`)
return data;
})
.catch(err => {
return res.json({ error: true, message: err })
})
})
.catch(err => {
return res.json({ error: true, message: err })
})
}
})
.then((reports) => {
console.log(`pre-response: ${JSON.stringify(reports)}`)
return res.json({ reports })
})
There might be some syntax errors, but the basic changes:
Turn the documents into a list of promises/results with querySnapshot.map(snapshotDocument => {.
Use Promise.all to create a promise that resolves when all these results are done.
Returns results from as deep down as needed all the way up to the main code.
Note that nesting of promises the way you do here is unusual and an anti-pattern, so in recommend also looking into learning to chain promises (so that you end up with a single catch - instead of having them all over the place.

Firebase auth, React: Auth sporadically failing

I have a component GigRegister - one of it's functions is to get all the documents from a collection, and return only the documents created by the currently logged in user:
authListener() {
auth().onAuthStateChanged(user => {
if(user){
this.setState({
userDetails:user
},
() =>
firebase.firestore().collection('gig-listing').onSnapshot(querySnapshot => {
let filteredGigs = querySnapshot.docs.filter(snapshot => {
return snapshot.data().user === this.state.userDetails.uid
})
this.setState({
filterGigs: filteredGigs
})
})
) //end of set state
} else {
this.setState({
userDetails:null
})
console.log('no user signed in')
}
})
}
componentDidMount() {
this.authListener();
}
Another function of this component is to capture data from the user and then post it to firebase, after which it redirects to another component.
handleSubmit(e) {
let user = auth().currentUser.uid;
const gigData = {
name: this.state.name,
venue: this.state.venue,
time: this.state.time,
date: this.state.date,
genre: this.state.genre,
tickets: this.state.tickets,
price: this.state.price,
venueWebsite: this.state.venueWebsite,
bandWebsite: this.state.bandWebsite,
user: user
};
auth()
.currentUser.getIdToken()
.then(function (token) {
axios(
"https://us-central1-gig-fort.cloudfunctions.net/api/createGigListing",
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: "Bearer " + token,
},
data: gigData,
}
);
})
.then((res) => {
this.props.history.push("/Homepage");
})
.catch((err) => {
console.error(err);
});
}
So here's the issue. Sometimes this component works as it should, and the data submit and redirect work as intended. Occasionally though, I'll hit submit but trigger the message TypeError: Cannot read property 'uid' of null . Interestingly, the post request is still made.
I've been logged in both when it succeeds and fails, and I can only assume that this.state.userDetails.uid evaluating to null means that auth state has expired, or that the component is rendering before userDetails can be assigned a value?
The issue I have is that I can't tell if this is an async problem, or an auth state persistence problem, and I also can't figure why it's a sporadic failure.
This line of code might be causing you trouble:
let user = auth().currentUser.uid;
currentUser will be null if there is no user signed in at the time it was accessed (or it's not known for sure if that is the case). This is covered in the API documentation.
Ideally, you should never use currentUser, and instead rely on the state provided by onAuthStateChanged. I talk about this in detail in this blog post. If you do need to use currentUser, you should check it for null before referencing properties on it.
You should also know that getting an ID token is best done by using a listener as well. The call is onIdTokenChanged, and it works like the auth state listener.
Keep in mind also that setState is asynchronous and doesn't set the state immediately. It's possible that your Firestore query isn't getting the state it needs immediately.

React Native (expo) and Firebase Firestore - Issue when a user is created within the app (Undefined is not an object...evaluating...)

I am having trouble with firebase and expo. When we do the signup process, and the user is generated through firebase, we sometimes get "undefined is not an object (evaluating 0.docs[0].data)" and the user is redirected to the entry point of the application instead of going to the next screen.
Most of the users will be able to go through the process without any problem. But few reported the same issues, and I have seen that with one account.
Here below the function that does not work properly
const createUser = async () => {
//Check if user already exists
await props.firebase.db
.collection('users')
.where('email', '==', props.userProfile.email)
.get()
.then(async (snapShot) => {
//if no document is found, save user to db
if (snapShot.docs.length === 0) {
await saveUserToDataBase()
.then(() => {
setShowLoader(false);
props.navigation.navigate('NotificationPermission');
})
.catch((err) => Alert.alert('An error occurred', err.message));
}
//else log an error, TODO: add error text in app.
else {
setShowLoader(false);
}
})
.catch((err) => {
Alert.alert('An error occurred', err.message);
});
};
const saveUserToDataBase = async () => {
//finds the correct collection and creates a new document within in containing the profile data.
await props.firebase.db
.collection('users')
.add({
first_name: props.userProfile.firstName,
email: props.userProfile.email.toLowerCase(),
phone_number: props.userProfile.phoneNumber,
organization: props.userProfile.organization,
profileId: props.userProfile.profileId,
})
.then(async (docRef) => {
await props.firebase.db
.collection('profile')
.doc(props.userProfile.profileId)
.update({
user_id: docRef.id,
})
.then(async () => {
await uploadProfilePhoto(docRef.id);
if (props.accessToken) {
props.navigation.navigate('NotificationPermission');
} else {
props.navigation.navigate('NotificationPermission');
}
})
.catch((err) => {
Alert.alert(
'An error occurred updating the users profile ',
err.message,
);
});
})
.catch((err) => {
Alert.alert('An error occurred creating the user', err.message);
});
};
I have used the Alert component to check the error directly, the error is consistent with few email addresses. I have upgraded the firebase package, did no do anything.
I feel like the onAuthStateChanged could be responsible of that but I am not sure how to handle that? Your help or suggestions will be greatly appreciated.

Cannot get data from real time database with anonymous authentication (React Native) - Permission Denied

I am trying to implement anonymous authentication in my app. I have done it with React Native Firebase (https://rnfirebase.io/). So far the sign up is good. The user signs up and I get a user id for that particular case. This is my code for it:
const authenticate = async () => {
await auth()
.signInAnonymously()
.then(() => {
console.log('User signed in anonymously');
console.log(user);
firebase.auth().onAuthStateChanged(user => {
if (user != null) {
fetchData();
}
});
})
.catch(error => {
if (error.code === 'auth/operation-not-allowed') {
console.log('Enable anonymous in your firebase console.');
}
setFetching(false);
console.error(error);
});
};
As you can see, if you check the part when the sign up is successful, we have a function called fetchData(). This function retrieved my data form the real time database. Here it is:
const fetchData = async () => {
try {
await fetch(setLocale())
.then(response => response.json())
.then(responseJSON => {
saveFavoritesFromAsync(responseJSON);
setFetched(true);
setFetching(false);
fetchMedusa();
});
} catch (error) {
console.log(error);
setFetching(false);
}
};
The problem comes when I implement the rules in firebase. Before this, I had everything open (read: true, write: true) but to add a bit of security I am trying to add this anonymous authentication. My rules now are:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Once I put this rules the data cannot be retrieved. I get no data basically, and my console log says "Permission Denied".
What am I doing wrong? Should I pass the user ID with the fetch? Please help
Thanks very much
I have solved it.
The packages that come from React Native Firebase are ment to be used together. What was happening was:
I was using the fetch method from react native. You have to use the firebase database package they give you to retrieve or write data.
See how my fetch looks now:
await database()
.ref(setLocale())
.once('value')
.then(responseJSON => {
if (responseJSON.error) {
setFetching(false);
setFetched(false);
} else {
saveFavoritesFromAsync(responseJSON.val());
setFetched(true);
setFetching(false);
fetchMedusa();
}
Also important, the responseJSON data has to be saved with the .val() method or it won't be correct.
I hope it can help someone in need.

What is the correct way of handling firebase user creation flow in react-redux application

I've been writing my React/Rediux/Firebase app for some time (started ~1year ago, but I had some few months breaks during this time - so I can review my own code from a time perspective).
Now I'm checking the code again and I have gut feeling that it's not the state of the art.
I am using Firebase also for managing account, in this case for creating a new one
I put all the chain of actions related to creation of user and error handling in one function block in Actions/index.js.
export const signUpUser = (data) => dispatch => {
Firebase.auth().createUserAndRetrieveDataWithEmailAndPassword(data.email, data.password)
.then(response => {
const userId = Firebase.auth().currentUser()
getUserInfoRef(userId).set({
uid: userId,
isAdmin: false
})
})
.then(response => {
Firebase.auth().currentUser().updateProfile({
displayName: `${data.name} ${data.surname}`
})
})
.then(() => {
dispatch(sendEmailVerification())
})
.catch(error => {
console.log('Error during signUpUser', error)
dispatch(authError(error))
})
}
but is this a good approach?
Isn't dispatching actions from its body some kind of anti pattern?
Maybe it should be split somehow (how?) ?
It's working, but I'm not delighted by how it looks :)
Please, advise.
I don't see anything wrong with your code. As long as you are using promise with the then, catch blocks it has to dispatched within.
But if you still want it to look better i will suggest you use async/await like this:
export const signUpUser = (data) => async dispatch => {
try {
await Firebase.auth().createUserAndRetrieveDataWithEmailAndPassword(data.email, data.password)
const userId = await Firebase.auth().currentUser()
getUserInfoRef(userId).set({
uid: userId,
isAdmin: false
}) // am not sure if this func is async or not
await Firebase.auth().currentUser().updateProfile({
displayName: `${data.name} ${data.surname}`
})
dispatch(sendEmailVerification())
} catch(e) {
console.log('Error during signUpUser', error)
dispatch(authError(error))
}
}
PS: It's nice to see SO asking people to be "nice" here. I think it's very much needed.

Resources