I am trying on a firebase cloud function to add some custom claims when a new user is created. As the custom claims I need to add a User role to the created user
I have tried on some tutorials where user role is added after the user created. https://www.youtube.com/watch?v=4wa3CMK4E2Y. But I thought of adding custom claims on the creation of the user and returning with the response
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.addDefaultUserRole = functions.auth.user().onCreate((user) => {
let uid = user.uid;
//add custom claims
return admin.auth().setCustomUserClaims(uid,{
isAdmin: true,
});
});
Even the above code get executed in firebase, nothing happened and claims were not received in the response. Is it not a good practice to add custom claims on User creation? What will be the reason for not attaching the custom claims with the above code
Your Cloud Function code looks OK and the Custom Claim shall be correctly set.
The problem you seem to encounter is that you cannot confirm that the claim is correctly set. As a matter of fact the setCustomUserClaims() method returns a non-null Promise containing void (and nothing else!).
You could do as follows if you want to verify, through the Log, that the claim has been correctly set.
exports.addDefaultUserRole = functions.auth.user().onCreate((user) => {
let uid = user.uid;
//add custom claims
return admin.auth().setCustomUserClaims(uid,{
isAdmin: true
})
.then(() => {
//Interesting to note: we need to re-fetch the userRecord, as the user variable **does not** hold the claim
return admin.auth().getUser(uid);
})
.then(userRecord => {
console.log(uid);
console.log(userRecord.customClaims.isAdmin);
return null;
});
});
Finally, note that it is not at all a "bad practice" to add custom claims on User creation! It makes full sense to do that upon user creation when you know which Claim(s) to set.
exports.addDefaultUserRole = functions.auth.user().onCreate((user) => {
let uid = user.uid;
//add custom claims
return admin.auth().setCustomUserClaims(uid,{
isAdmin: true
})
.then(() => {
//Interesting to note: we need to re-fetch the userRecord, as the user variable **does not** hold the claim
return admin.auth().getUser(uid);
})
.then(userRecord => {
console.log(uid);
console.log(userRecord.customClaims.isAdmin);
return null;
Hope it works.
Related
I'm building a forum-style application where users post content that displays on a global feed. I want to display information about the user in posts (photoURL, displayName) similar to Twitter.
I have firebase v9 using the authentication and firestore for the posts. The reason I want to reference the auth is that I can catch changes to the user's information as it happens, this way the feed is up to date.
I save the user's unique ID with the post so I am able to reference who to display. I can successfully reference the post title and description with doc.title & doc.description however I get stuck when retrieving user information. I'm trying doc.UserID.displayName for the display name but I know this is incorrect. I can't find anything in the docs for this specific use case, is this something that I can do with just firestore and auth?
Do I need to create a reference to the auth storage with doc.UserID?
Here is the code:
// add a new post
addPostForm.addEventListener('submit', (e) => {
e.preventDefault();
onAuthStateChanged(auth, (user) => {
const colRef = collection(db, 'Posts');
console.log(hiddenURL.value);
addDoc(colRef, {
UserID: user.uid,
beatURL: hiddenURL.value,
title: addPostForm.postTitle.value,
description: addPostForm.postDescription.value,
})
.then(() => {
console.log("Document written with ID: ", doc.id);
addPostModal.classList.remove('open');
addPostForm.querySelector('.error').textContent = "";
})
.catch(error => {
addPostForm.querySelector('.error').textContent = error.message;
alert(error);
})
})
});
export const initApp = async () => {
initFirebaseAuth;
const posts = await collection(db, 'Posts');
// render data to the page
return renderPosts(posts);
};
const renderPosts = (posts) => {
const main = document.getElementById("feed");
onSnapshot(posts, (snapshot) => {
let cardsArray = []
snapshot.docs.forEach((doc, user) => {
cardsArray.push({ ...doc.data(), id: doc.id })
name.textContent = `${doc.UserID.displayName}`; // users display name
avatar.src = doc.UserID.photoURL; //user's image
description.textContent = `${post.description}`;
title.textContent = `${post.title}`;
});
console.log(cardsArray);
});
};
There are two cases and approaches at first sight:
1. Your users profiles are only available in the Auth Service
In this case, via the JS SDK, a user X cannot "query" the Auth profile of a user Y.
This means that you need to save the author's displayName together with the author uid when the post is created.
2. Your users profiles are also available in a users collection (a common pattern)
In this case, when you display a post, you could fetch the user's document to get the author's displayName.
However, in the NoSQL world, you should not be afraid to duplicate data and denormalize your data model. When designing your data-model you should think about it from a query perspective, trying to minimize the number of queries for a given screen/use case. So approach #1 is recommended, even if you maintain a user's collection.
In case of changes in the user's profile, in order to synchronyse the post documents and user's data a common approach is to use a set of Cloud Functions (which are executed in the back-end) to update the post documents. The link between the posts and the users profile being the user's uid.
I have a getUser function that return a User entity after being saved into the database for caching reasons. Here is the pseudo-code for it:
export const getUser = async (userId: string): Promise<User> => {
if (userStoredInDatabase) return userStoredInDatabase // <- pseudo code
// ...if no user stored -> fetch external services to get user name, avatar, ...
const user = new User()
user.id = userId // <- PRIMARY KEY
// ...add data to the user
return getManager().save(user)
}
I use this function in 2 distinct routes of a simple expressjs API:
app.get('/users/:userId/profile', async (req, res) => {
const user = await getUser(req.params.userId)
// ...
})
app.get('/users/:userId/profile-small', async (req, res) => {
const user = await getUser(req.params.userId)
// ...
})
So far so good until I came to the problem that my frontend need to fetch /users/:userId/profile and /users/:userId/profile-small at the exact same time to show the 2 profiles. If the user is not yet cached in the database, the .save(user) will be called twice almost at the same time and I will respond for one of the 2 with an error due to an invalid sql insertion as the given user.id already exists.
I know I could just delay one of the request to make it work good enough but I'm not in the favor of this hack.
Do you have any idea how to concurrently .save() a User even if it is called at the same time from 2 different contexts so that TypeOrm knows for one of the 2 calls that user.id already exist and therefore do an update instead of an insert?
Using delay will not solve the prolm since the user can open 3 tabs at the same time. Concurency has to be handled.
Catch if there is a primary key violation (Someone already has stored the user between the get user and persist user code block)
Here is an example:
export const getUser = async (userId: string): Promise<User> => {
try {
if (userStoredInDatabase) return userStoredInDatabase // <- pseudo code
// ...if no user stored -> fetch external services to get user name, avatar, ...
const user = new User()
user.id = userId // <- PRIMARY KEY
// ...add data to the user
return getManager().save(user)
} catch (e) {
const isDuplicatePrimaryKey = //identify it is a duplicate key based on e prop,this may differ depending on the SQL Engine that you use. Debugging will help
if (isDuplicatePrimaryKey) {
return await //load user from db
}
throw e; // since e is not a duplicate primary key, the issue is somewhere else
}
}
I am facing a problem with setting custom claims for Firebase Authentication service's token. I am using Cloud function to set the custom claims for Hasura. The cloud function executes upon new user create event to set the custom claims. Here's my code running in cloud function
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
exports.processSignup = functions.auth.user().onCreate(user => {
// create custom claims for hasura
const hasuraClaims = {
"x-hasura-default-role": "user",
"x-hasura-allowed-roles": ["user"],
"x-hasura-user-id": user.uid
}
// attach claims to user auth object
return admin.auth().setCustomUserClaims(user.uid, hasuraClaims)
.then(_ => {
functions.logger.info('SUCCESS: Custom claims attached');
})
.catch(err => {
console.log('ERROR: ', err);
})
})
In my frontend web page, I am running the following code to get the idToken
// subscribe to user state change
firebase.auth().onAuthStateChanged(async user => {
console.log('Firebase auth state changed');
if (user) {
// User is signed in.
window.User = user;
let idToken = await user.getIdTokenResult();
console.log('idToken: ', idToken);
}
})
I don't know what I'm doing wrong, but the token doesn't contain the custom claims that I've set in my Cloud function processSignup(). I know that the function executed without error because I can check my function logs and find the info entry SUCCESS: Custom claims attached.
Can anyone please help me solve this problem?
Updating claims does not trigger an onAuthStateChanged (the auth state of being logged in or not has not changed, but the users' claims have) and tokens are minted and then used for ~1h.
You are calling getIdTokenResult but not forcing a refresh, try:
let idToken = await user.getIdTokenResult(true);
which will force a new token to be fetched from the server and will (hopefully) include your custom claims.
when clicking on a button i called a function,
onDelete(id:string){ this.db.collection('Students').doc(id).delete(); }
Here, id is a name of document that i want to delete, db is a property of type AngularFireStore, 'Students' is a name of collection.
Structure of document:
enter image description here
In the above image, collection name is Students, under which multiple documents exist, since document name must be unique so i given that name a number of type string which acts as id. In every document, there is email field, i want to delete that email from authentication when i delete the same document.
code to sign up users:
this.afAuth.auth.createUserWithEmailAndPassword(email:string,password:string).then(res=>{})
If you want to delete a user existing in Firebase authentication you have two possibilities:
1/ Using the JavaScript SDK (since your app is made with angular)
You call the delete() method, as follows:
const user = firebase.auth().currentUser;
user.delete()
.then(() => {
//....
})
.catch(err => {
if (err.code === "auth/requires-recent-login") {
//Re-authenticate the user
} else {
//....
}
})
Note however, that this method "requires the user to have recently signed in. If this requirement isn't met, ask the user to authenticate again and then call firebase.User.reauthenticateWithCredential". An error with the auth/requires-recent-login code is "thrown if the user's last sign-in time does not meet the security threshold".
So, only the logged-in user can call this method from a front-end, in order to delete his/her own account.
2/ Using the Admin SDK
You can use the Admin SDK's deleteUser() method, for example within a Cloud Function.
In this case, there is no need to have the user logged-in since this is executed in the back-end and it is therefore possible to delete any user.
For example, you could have a Callable Cloud Function triggered by an admin user.
Another possibility, is to trigger a Cloud Function upon the Firestore user's document deletion.
Update based on your Question update:
I understand that you want to delete the user record in the Auth service upon deletion. For that you can write a Cloud Function as follows:
exports.deleteUser = functions.firestore
.document('Students/{studentID}')
.onDelete((snap, context) => {
const deletedValue = snap.data();
const userEmail = deletedValue.Email;
return admin.auth().getUserByEmail(userEmail)
.then(userRecord => {
const userID = userRecord.uid;
return admin.auth().deleteUser(userID)
})
.catch(error => {
console.log(error.message);
return null;
})
});
I read all the answers I found about this but I still have some problems. Probably something with .ref(), but I can't see what I'm doing wrong. My cloud function is not triggered at all.
DB example:
business/{businessId}/reservations/{reservationId}
I want to trigger this function every time when a new reservation is created [a new document is created in reservation collection] (business/{businessId}/reservations/).
And then I want to sent a notification message, but that is another thing.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
exports.sendAdminNotification = functions.database
.ref("business/{businessId}/reservations")
.onWrite((event: any) => {
// It never comes here...
console.log('Here');
const payload = {
notification: {
title: 'New registration',
body: 'You have new registration!',
},
};
// You can ignore this part
admin
.messaging()
.sendToDevice('SomeToken', payload)
.catch(function (error: any) {
console.log('Notification sent failed:', error);
});
});
The fact that you're using the terms "document" and "collection" suggests that your database is Firestore. But what you've written here is a Realtime Database trigger. Realtime Database is a completely different database. Instead, you will need to write a Firestore trigger instead. They begin with functions.firestore.