Undefined error on simple firebase auth function - node.js

Strange issue, I have a function that trigger by Firebase newUser auth
if the user uses Google or Facebook provider the function works but if it email/password signup the function returns an undefined error.
what I'm missing here?
The function code:
exports.newUser = functions.auth.user().onCreate((user) => {
const docRef = admin.firestore().collection('Users').doc(user.uid)
return docRef.set({
email: user.email,
name: '',
photo: '',
signupDate: admin.firestore.FieldValue.serverTimestamp()
},{merge: true});
});

The documentation for the email property of UserRecord suggests that it is not always available. So, you should check for that in your code.

user.email field appears under provider and not like in the Firebase Example directly under user.
here how it should look like:
var email = user.email; // The email of the user.
if (email == undefined) {
for (var provider of user.providerData) {
if (provider.email) {
email = provider.email;
break;
}
}
}
return docRef.set({
email: email,
name: '',
photo: '',
signupDate: admin.firestore.FieldValue.serverTimestamp()
},{merge: true});
});

Related

Firebase: how to get getAdditionalUserInfo in onCreate Firebase Cloud function

I'm trying to get some additional data from a SAML login provider. I can see this data in the client but I fail to see how to get this in the back end (firebase functions).
I'm using these in the FE
"firebase": "^9.8.2",
"firebase-functions": "^3.14.1",
And this in the BE
"firebase-admin": "^10.2.0",
"firebase-functions": "^3.21.2",
This is the data and how I get it in the client:
async myproviderSignIn() {
const provider = new SAMLAuthProvider('saml.myprovider');
const auth = getAuth();
const userCredential = await signInWithPopup(auth, provider);
const credential = SAMLAuthProvider.credentialFromResult(userCredential);
if (!environment.production) {
console.log('User:', userCredential, credential);
console.log(
'getAdditionalUserInfo:',
getAdditionalUserInfo(userCredential)
);
}
}
This is what I'm after and logged by getAdditionalUserInfo in the client:
{
"isNewUser": false,
"providerId": "saml.myprovider",
"profile": {
"urn:schac:attribute-def:schacPersonalUniqueCode": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:1.3.6.1.4.1.25178.1.2.9": "diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.25178.1.2.14": "urn:schac:personalUniqueCode:nl:local:diy.myproviderconext.nl:studentid:123456",
"urn:oid:0.9.2342.19200300.100.1.3": "student1#diy.myproviderconext.nl",
"urn:oid:1.3.6.1.4.1.5923.1.1.1.1": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:eduPersonAffiliation": [
"student",
"employee",
"staff",
"member"
],
"urn:mace:dir:attribute-def:sn": "One",
"urn:mace:dir:attribute-def:givenName": "Student",
"urn:oid:2.5.4.42": "Student",
"urn:mace:dir:attribute-def:mail": "student1#diy.myproviderconext.nl",
"urn:oid:2.5.4.4": "One",
"urn:mace:terena.org:attribute-def:schacHomeOrganization": "diy.myproviderconext.nl"
}
}
Finally this is my BE on user create trigger. It creates a DB record of the user when a new user is created in Firebase auth. I'd wish to map some of the properties shown above here to the user record in the DB.
export const onCreate = functions.auth
.user()
.onCreate((user: UserRecord, context: EventContext) => {
const timestamp = serverTimestamp();
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,
lastSeen: timestamp,
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
// Get additional user data from the UserCredential
// const additionalUserInfo = getAdditionalUserInfo(user); ???
const result = getFirestore()
.collection(constants.dbCollections.users)
.doc(user.uid)
.set(dbUser);
return result;
});
How do I access these additional properties in my cloud function without relying on the client?
the problem you try to solve is something explained in here:
https://firebase.google.com/docs/auth/admin/custom-claims#node.js
some of the data you described below is stored within the auth.user
const dbUser: DbUser = {
uid: user.uid,
name: user.displayName || '',
firstName: user.displayName || '',
lastName: '',
email: user.email,
photoURL: user.photoURL,
emailVerified: user.emailVerified,
createdDate: timestamp,<-----------------------------------------------here
lastSeen: timestamp,<--------------------------------------------------and here
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
};
but the data i marked as "here" needs to be created via "custom user claims" but its under the FireBaseAnalytics tab neither auth. nor firestore if you insist to retrive this data from Firebase.
But for this data :
providerData: JSON.parse(JSON.stringify(user.providerData)),
userDump: JSON.parse(JSON.stringify(user)),
contextDump: JSON.parse(JSON.stringify(context)),
you should use FireStore or FirebaseRealTimeDatabase i repeat if you insist of getting those data from FireBase because of the 1000byte policy of Custom Claims.
i hope i do understand your question well. But im sure if you add those properties with this
// Set admin privilege on the user corresponding to uid.
getAuth()
.setCustomUserClaims(uid, { admin: true })
.then(() => {
// The new custom claims will propagate to the user's ID token the
// next time a new one is issued.
});
you can retrive it with .auth token

MongoDB and Mongoose Abnormal behavior?

I apologize for the maybe trivial question. I am accessing mongo db to obtain information on a user, verify the password, and save some data in Redis if the validation was successful.
But I'm misbehaving (probably my mistake).
My simple backend is built in express 4.17.2, node 17.2.0, and redis4.0.1.
Server-side MongoDB on cloud MongoDB Atlas (Version 4.4.10) and RedisLab (Redis in cloud version 6.2.3).
Here are some code snippets:
app.post('/identity/login', (req, res) => {
let userHash = '';
let result = false;
const user = req.body;
userHash = crud.login(user.username);
userHash.then(h => {
result = bcrypt.compareSync(user.password, h.password);
console.log('All object ',h);
console.log('Access to roles array',h.roles);
let value = {name:h.name, surname:h.surname, roles:h.roles, username:h.username};
console.log('value', value);
client.set(h.password, JSON.stringify(value), { EX: 60, NX: true });
res.send(result);
});
});
Making the POST call to that endpoint, I get the following (unexpected) results, highlighted by the logs:
All object {
_id: new ObjectId("61d1d38d0f4bac12d8f504ee"),
username: 'jdoe',
password: '$2a$14$5GULZRmiN0DXQXMaLwQiEO9S/OlBll5U4ZwkBn2NYpju0VRDFUqVO',
name: 'John',
surname: 'Doe',
roles: [ 'Admin' ]
}
Access to roles array undefined
value { name: 'John', surname: 'Doe', roles: undefined, username: 'jdoe' }
As you can see, if I log h.roles I get that the array is not defined, while logging the whole object the array property 'roles' is correctly valued. So obviously, when I go to build the object to save in Redis (as h.roles), the array is not created.
Why? Could you please help me and say me what I am doing wrong? My mistake, a mongoose problem (see 6.4.1)?
Below, I show the code to access MongoDB:
const login = async (username) => {
connectDB();
let dbPwd;
const User = mongoose.model('Users', UserSchema);
return await User.findOne({ username: username }, 'password name surname roles username').exec();
}
Thanks so much!

Express - How to exclude a field from the response returned by Model.findOne() and Model.create() mongoose

I want to exclude the user's password field when sending a response to my front end. of course, I don't want anyone to access the user's encrypted password.
I have looked at this SO question, but the solution does not look good for models with many fields.
I'm using .select('-password') with Model.find(), Model.findById(), and Model.findByIdAndUpdate()
but It's not working for Model.findOne() and Model.create()
How can I exclude some fields when returning a response ?
/**
* #desc Authenticate User
* #route /api/acccounts/signin
* #access Public
*/
export const authenticateUser = asyncHandler(async (req, res) => {
const values = await loginSchema.validateAsync(req.body);
const { email, password, rememberMe } = values;
const account = await Account.findOne({ email });
if (account && (await account.matchPassword(password))) {
return res.json({
id: account.id,
firstName: account.firstName,
lastName: account.lastName,
email: account.email,
isAdmin: account.isAdmin,
...other fields,
token: generateToken(account.id, rememberMe),
});
}
return res.status(400).json({ message: 'Invalid email or password' });
});
Here you can use projection to exclude the field in your response like this
db.collection.findOne({
email: "sample#sample.com"
},
{
password: 0
})
Here is like of playground to test it: MongoPlayground
For more details check out findOne official documentation

Node JS Sequelize: running the same query again is retrieving the previous result

I am building a Web API using Node JS. For database operation, I am using the Sequalize package. In my test when I run the same query that gets a record after updating the record again, it is giving me the old/previous result.
Here is my code:
let testUser = await User.findOne({
where: {
email: {
[Op.eq]: testHelper.testUser.email
}
}
})
let now = new Date();
//genrate the token in the database before making the request.
let verificationToken = await VerificationToken.create({
userId: testUser.id,
verificationToken: "testing",
expiresAt: date.addSeconds(now, 40000)
})
let requestBody = {
email: testUser.email,
password: "Newpassword123",
confirm_password: "Newpassword123",
token: "testing"
}
const res = await request(app).put("/api/auth/reset-password").send(requestBody);
expect(res.statusCode).toBe(200);
expect(res.body.error).toBe(false);
//refresh the user instance by fetching the new record from the database.
let user = await User.findOne({
where: {
email: {
[Op.eq]: testHelper.testUser.email
}
}
})
// here user.password and testUser.password are the same even the record is actually updated
As you can see in the comment, the password of user object is still the same as the password of testUser object even though the password is updated in the database. How can I fix it to get the latest record?

How to to access first and last name for the following user model (based on mongoose)?

I am looking at some code, with a user schema, similar to the following.
var UserSchema = new mongoose.Schema(
{
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
password: {
type: String,
required: true
},
profile: {
firstName: {
type: String
},
lastName: {
type: String
}
}
}
);
Now, as far as I can understand, the top-level properties are email, password and profile.... firstName and lastName should only be accessible from within profile. However, the details are being accessed with something like the following.
exports.register = function(req, res, next) {
// Check for registration errors
const email = req.body.email;
const password = req.body.password;
const firstName = req.body.firstName;
const lastName = req.body.lastName;
// Return error if no email provided
if (!email) {
return res.status(422).send({ error: 'You must enter an email address.'});
}
// Return error if no password provided
if (!password) {
return res.status(422).send({ error: 'You must enter a password.' });
}
// Return error if full name not provided
if (!firstName || !lastName) {
return res.status(422).send({ error: 'You must enter your full name.'});
}
...
I don't seem to understand why the firstName and lastName are being accessed directly with req.body.firstName instead of req.body.profile.firstName. There don't seem to be any virtual attributes in place either. So what's going on!?
req.body added by body-parser and this is not related to your mongoose model. You will get data in req.body sent from front-end(client side). Apart from this issue, I would like to recommend you to use following format that may help you
You may like to use schema for sub-document
var profileSchema = new Schema({
firstName: String,
lastName: String
});
var UserSchema = new mongoose.Schema({
email: {
type: String,
lowercase: true,
unique: true,
required: true
},
password: {
type: String,
required: true
},
profile: profileSchema
});
and may use like
exports.register = function(req, res, next) {
if(!req.body)
return res.status(500).send({ error: 'Unable to parse data'});
// Check for registration errors
const userData = {
email: req.body.email,
password: req.body.password,
profile: {
firstName: req.body.firstName,
lastName: req.body.lastName
}
}
// Return error if no email provided
if (!userData.email) {
return res.status(422).send({ error: 'You must enter an email address.'});
}
// Return error if no password provided
if (!userData.password) {
return res.status(422).send({ error: 'You must enter a password.' });
}
// Return error if full name not provided
if (!userData.profile.firstName || !userData.profile.lastName) {
return res.status(422).send({ error: 'You must enter your full name.'});
}
var newUser = new User(userData);// you must import user schema before using User
In an express application request parameters are passed in as first parameter of a route (req, res, next). The sample code posted shows the result of a POST request to a route called /register.
This data does not relate to the model posted with the question.
To be able to work with the model, the data needs to be stored into a new Mongoose object.
So within the route one would write:
exports.register = function(req, res, next) {
const User = new User();
User.profile.firstName = req.body.firstName;
// ... and so on
User.save((err, savedUser) => {
if(err) return next(err);
// end request
});
}
Please note that some kind of sanity check is recommended when dealing with user provided variables. Using it like in my example above may enable an attacker to store a string of arbitrary length inside the database which is most probably not desired.
As pointed out by #DanielKhan, within the above comments, mongoose is only being used to model the data. However, at this point, it has nothing to do with the data coming in directly from the client. Hence, all the fields, including email, password, and first name, and last name will be retrieved at the same level... using req.body.

Resources