Hi everyone am trying to hash my put request using bcrypt in my express-mongoose server
the put request
// updating a user
router.put('/:id', async (req, res) => {
const {error} = validate(req.body)
if (error) return res.status(400).send(error.details[0].message)
const user = await User.findByIdAndUpdate(req.params.id, {
$set : {
name: req.body.name,
email: req.body.email,
password: req.body.password
}
})
// hashing user passwords
const salt = await bcrypt.genSalt(10)
user.password = await bcrypt.hash(user.password, salt)
if (!user) return res.status(404).send('User with that id does not exist')
res.send(user)
})
All other functions inside the update request is working perfectly apart from hashing the updated password. As a newbie I need your help/ best approach recommendation in this.
Thanks in advance...
Solution 1: Easy Way
For your personal solution, without really modifying the code, it works like the following.
// updating a user
router.put('/:id', async (req, res) => {
const {error} = validate(req.body)
if (error) return res.status(400).send(error.details[0].message)
// Why not make the hash function here?
const salt = await bcrypt.genSalt(10)
const newPassword = await bcrypt.hash(req.body.password, salt)
const user = await User.findByIdAndUpdate(req.params.id, {
$set : {
name: req.body.name,
email: req.body.email,
password: newPassword
}
})
if (!user) return res.status(404).send('User with that id does not exist')
res.send(user)
})
You have a mistake in your user.password call. The findByIdAndUpdate method does not return an object that you can modify instantly. In above workaround, we simply move the function so that it hashes the new password first before updating your document.
Solution 2: My Own Style
For my personal solution, I'd go like this. Let's say that you have a userModel that stores the schema of your User entity. I will add a new middleware that will run every time the password changes.
/** your user schema code. **/
userSchema.pre('save', async function (next) {
// Only run the encryption if password is modified.
if (!this.isModified('password')) {
return next();
}
// Hash the password with BCRYPT Algorithm, with 12 characters of randomly generated salt.
this.password = await bcrypt.hash(this.password, 12);
next();
});
Next, we'll create a new dedicated route in order to handle password changes. I think it's better if we define a new route for it as passwords are sensitive data. Below is pseudocode, don't instantly copy and paste it, it wouldn't work.
const user = await User.findById(...);
user.password = req.body.password;
await user.save({ validateBeforeSave: true });
Remember that save middleware runs every time after the save command is run.
Further reading about Mongoose's middlewares here.
Related
I created some sample code to demonstrate my issue on a smaller scale. I would like a solution that doesn't involve adding 'unique: true' to my model, if possible, because I seem to run into similar problems in many different scenarios:
const express = require('express')
const mongoose = require('mongoose')
const app = express()
const PORT = 6000
app.use(express.json())
// Initializing mongoose model and connection
const SampleSchema = mongoose.Schema({
username: String,
password: String
})
const Sample = mongoose.model('sample', SampleSchema)
mongoose.connect('mongodb://localhost/testdatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
})
// Running my post request
app.post('/api', async (req, res) => {
await Sample.findOne({
username: req.body.username
}).then(data => {
if(data) {
console.log(data)
return res.json('This user already exists in my database')
}
})
await Sample.create({
username: req.body.username,
password: req.body.password
})
return res.json('User created')
})
app.listen(PORT, () => {
console.log('Server running on 6000')
})
Here is my request and database the first time I send a request:
This is as intended. However, if I send the same request a second time:
I want the code to stop on the first 'res.json' if that line of code is executed - basically, in this example I don't want to create a new Sample if one with the same username already exists. I do realize that in this case I can approach the issue differently to solve the problem, but I want to know why my 'Sample.create' line runs, and if there's a way to prevent it from running aside from the aforementioned method.
This is because the .then callback executes after the enclosing function has already finished. In this code here:
await Sample.findOne({
username: req.body.username
}).then(data => {
if(data) {
console.log(data)
return res.json('This user already exists in my database')
}
})
The function being returned from is the data => ... arrow function passed to .then, not the enclosing request handler, so it doesn't prevent subsequent code from executing.
You want to rewrite that bit to use async/await syntax as well:
const data = await Sample.findOne({
username: req.body.username
})
if(data) {
console.log(data)
return res.json('This user already exists in my database')
}
You might want to read up a bit on async/await and Promises in general-- asynchronous code can be quite confusing at first! https://developers.google.com/web/fundamentals/primers/async-functions
So I'm making an app with profiles and stuff. And the user would connect to his profile by using the route /user/:id (the :id would be req.user.id) the thing is when I try to log in users with same username req.user is the same for both eventhough they have different email/credentials. And I think it's because I'm using passport and when serializing a user, and saving his credentials to the session is saving the username, and of course when desirializing it's going to find the user by his username. I've already tried to change the session key to be email or id, so it would not find users with same username but I can't make it work.
Here is the code
passport.serializeUser(User.serializeUser(function (user, done) {
done(null, user.email)
}));
passport.deserializeUser(User.deserializeUser(function (email, done) {
user.findById(id, function (err, user) {
done(err, user)
})
}))
OUTPUT
Session {
cookie: {
path: '/',
_expires: 2021-05-11T18:40:11.634Z,
originalMaxAge: 604800000,
httpOnly: true
},
flash: {},
passport: { user: User's name }
}
As you can see eventhough I'm trying to add the email key to the session, it seems not to work.
Can someone help me fix this issue or even prupose a new solution
I would recommend looking into User.serializeUser and User.deserializeUser are affecting things. It's unclear to me why they are being passed the passport methods.
Here is an idea of a common implementation that may simplify how you are getting data and passing it to the req object.
passport.serializeUser((user, done) => {
done(null, user.email);
});
passport.deserializeUser((email, done) => {
// Mongoose query
// Find matching user based on email
const user = await User.findOne({ email }).exec();
done(null, user);
});
when I created passport-saml strategy, during login, there is a profile object pass to the middleware function, with nameID info there. I need that info to call logout later on.
// passportHandler.js
const passport = require("passport");
const passportSaml = require("passport-saml");
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
// SAML strategy for passport -- Single IPD
const samlStrategy = new passportSaml.Strategy(
{
entryPoint: process.env.SSO_ENTRYPOINT,
logoutUrl: process.env.SSO_LOGOUT,
issuer: process.env.SSO_ISSUER,
callbackUrl: process.env.SSO_CALLBACK_URL || undefined,
path: process.env.path,
cert: process.env.SSO_CERT.replace(/\\n/gm, "\n"), // change "\n" into real line break
},
(profile, done) => {
console.log('profile', profile); // nameID and nameIDFormat are in profile object
done(null, profile)
}
);
passport.use(samlStrategy);
module.exports = passport;
index.js
// index.js of Express server
import passport from "./src/passportHandler";
import { getLogout } from "./src/routes.js";
const app = express();
app.use(passport.initialize());
app.use(passport.session());
app.get('/sso/logout', getLogout); // this route, I need the above 2 data
getLogout function import from another file, I hardcode nameID and nameIDFormat, how do I get them from the beginning profile object, save them somewhere, and pass them to this route?
// routes.js
export const getLogout = (req, res) => {
!req.user && (req.user = {})
req.user.nameID = 'Eric1234#outlook.onmicrosoft.com'; // hardcode, how to pass this info?
req.user.nameIDFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'; // hardcode too
const samlStrategy = req._passport?.instance?._strategies?.saml; // is this correct?
samlStrategy.logout(req, (err, request) => {
if (!err) {
res.redirect(request);
}
})
};
my second question is, I get the samlStrategy object from req._passport?.instance?._strategies?.saml, is it a proper way to get it? or, again the similar question, how can I pass saml strategy obj from the beginning create logic to this route?
thanks for any help!
answering my own silly question...
in samlStrategy, at last calling done(null, profile)
const samlStrategy = new passportSaml.Strategy(
{
entryPoint: process.env.SSO_ENTRYPOINT,
logoutUrl: process.env.SSO_LOGOUT,
issuer: process.env.SSO_ISSUER,
callbackUrl: process.env.SSO_CALLBACK_URL || undefined,
path: process.env.path,
cert: process.env.SSO_CERT.replace(/\\n/gm, "\n"), // change "\n" into real line break
},
(profile, done) => {
console.log('profile', profile); // nameID and nameIDFormat are in profile object
done(null, profile)
}
);
then the profile object will become req.user object in the Service Provider's Login Post Callback function
Then I can save the user object somewhere, and use it again when logout being called.
I wrote a sign up functionality in nuxtjs. It saves a new user in my database. However, there seems to be a problem with generating a token afterwards, to log in the user.
The register action gets called by a method in the register component. It returns the error response in the catch block. It seems to fail after the token is generated on the server.
Action in the store
async register ({ commit }, { name, slug, email, password }) {
try {
const { data } = await this.$axios.post('/users', { name, slug, email, password })
commit('SET_USER', data)
} catch (err) {
commit('base/SET_ERROR', err.response, { root: true })
throw err
}
}
Post function on the nodejs server
router.post('/users', async (req, res) => {
try {
const body = _.pick(req.body, ['name', 'slug', 'email', 'password']);
const user = new User(body);
await user.save();
const token = await user.generateAuthToken(); // execution seems to fail on this line
console.log(token); // never gets called
req.session['token'] = 'Bearer ' + token;
req.session['user'] = user;
res.header('Authorization', 'Bearer ' + token).send(user);
} catch (err) {
res.status(400).json({ message: "Der Account konnte leider nicht erstellt werden" });
}
});
GenerateAuthToken function in mongo model User
UserSchema.methods.generateAuthToken = function () {
var user = this;
var access = 'auth';
var token = jwt.sign({_id: user._id.toHexString(), access}, process.env.JWT_SECRET).toString();
user.tokens.push({access, token});
return user.save().then(() => {
return token;
});
};
Error message
I would be tremendously thankful for any kind of help!
Maybe it doesn't help too much, but I would try to create a dummy token and try to make everything works with it. One of my debugging techniques is to isolate every single part of my code and be sure that everything works piece for piece, maybe that technique is slow but most of the time it works.
If everything works, I would continue debugging the generateAuthToken function.
If your console log never gets called, then the error could be in the function.
I hope it helps a little and sorry I don't know too much about MongoDB but everything seems to be ok.
Setup
I am doing web site authorization, and want to embed best practices into it, while keeping code clean and readible. For now I have classic code like this:
let foundUser = await userModel.findOne({ email: recievedEmail });
if(!foundUser)
error("not authorized!");
const isPasswordMatch = await bcrypt.compare(recievedPassword, foundUser.password);
if(!isPasswordMatch)
error("not authorized!");
foundUser.update({ $set: { lastLogin: new Date() }, $push: { myEvents: authEvent } });
foundUser.save();
success("authorized OK!");
Meanwhile, I've asked a question on the best mongoose command to perform auth, and we've forged up the following "auth-check-and-update" command, in an "atomic" manner:
const foundUser = await userModel.findOneAndUpdate(
{ email: recievedEmail, password: recievedPassword },
{ $set: { lastLogin: new Date() }, $push: { myEvents: authEvent } }
);
if(foundUser)
success("authorized OK!");
else
error("not authorized!");
Idea here is obvious - if a user with matching email and password is found then user is considered as authorized, and its last login timestamp is updated (simultaneously).
Problem
To combine best practices from the two above, I need somehow to embed bcrypt.compare() call inside findOneAndUpdate() call. That is tricky to do, because I cannot just "compare hashed passwords"; bcrypt just works differently from simple hashes (like sha or md5): For security reasons it returns different hashes every time. (Answers in the link explains "why and how").
Solution Attempt
I've looked into mongoose-bcrypt package: it is utilizing Schema.pre() functionality:
schema.pre('update', preUpdate);
schema.pre('findOneAndUpdate', preUpdate);
To get the idea, please, take a look at mongoose-bcrypt\index.js.
You will see, that preUpdate affects only creating new user (..andUpdate part), but not actual checking (findOne.. part). So this plugin could fit for implementing "user registration" / "change password". But it can't work for authorization in the proposed way.
Question
How would you "combine" bcrypt.compare() and userModel.findOneAndUpdate() calls under such circumstances?
What about compare password in UserModel like this
// method to compare password input to password saved in database
UserModel.methods.isValidPassword = async function(password){
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
And inside your auth or passport (i am using passport) do something like this
passport.use(new LocalStrategy(
(username, password, done) => {
// change your query here with findOneAndUpdate
User.findOne({ username: username }, (err, user) => {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.isValidPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));