Authenticate new user with passport.js only after email verify - javascript

I'm using passport.js with mongoose (mongoDB) (and passport-local-mongoose) to create and authenticate user accounts in a node/express backend.
The strategy I'm currently using is:
User completes a signup form, with a username input type="email" name="username" and a password input name='password' (among others)*
User submits form.
Add new user to a Profile schema let newUser = new Profile({username: req.body.username, ...})
Generate a token using crypto.randomBytes(...)
Profile.register(newUser, password) & passport.authenticate user and set token field generated from above set, along with an expiry date
Send an email to new user, with a url containing their mongoose _id and their token (/u/${id}/is-valid/${token}).
On visiting this url, Profile.findOne({_id: req.params.id}) and check that the tokens match, and has not expired
If not, send them to the login page.
Then problem with this approach, is that in calling Profile.register and passport.authenticate together, a pre authenticated account is created before the user has validated their email.
How can I decouple these two methods so that I can authenticate users only after they've verified their email address?
Here is how I call Profile.register and passport.authenticate (as part of an async.waterfall([]):
Profile.register(user, pw)
.then(response => {
passport.authenticate("local")(req, res, function(err) {
if (err) return callback(err);
user.resetEmailToken = token;
user.resetEmailExpires = Date.now() + 8.64e+7; // 1 day
user.save(function(err) {
callback(err, token, user);
});
});
})
.catch(err => {
return callback(err);
});
I would like to Profile.register as I do above, but only passport.authenticate once I've checked the tokens.
Here is how I check the tokens (on visiting link contained in email - /u/${id}/is-valid/${token}):
async verifyEmail(req, res, next){
let user = await Profile.findOne({ '_id': req.params.id })
if(!user) return res.redirect("back");
if (user.resetEmailToken != req.params.token){
req.flash("error", "Tokens don't match");
return res.redirect("back");
}
if(user.resetEmailExpires < Date.now()){
console.log("This date has expired", moment(user.resetEmailExpires).calendar());
return res.redirect(`/signup?token-expired=${user.email}`);
}
user.isVerified = true;
// Here is where I want to passport.authenticate() and redirect to their new profile
user.save();
return res.render('login', { user: user });
}
If this is not possible, what are other approaches I could take to achieve this? It must be a fairly common signup strategy.
* This is a bit of a hack as passport.js assumes registration with username & password (not email)

Related

Is it better to keep just the user_id or the whole user object in the payload when signing a jwt?

There are 2 ways which I know.
1) Passing the whole user object:
let payload = {
user: {
id: user._id,
name:user.name,
email:user.email,
username:user.username
},
}
let token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '1d' })
2) Passing only the _id:
let token = jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
expiresIn: '1d',
});
In the second approach I have to make a database call to fetch user's detail using the _id stored in the payload whenever I want to access a protected route.
// protect.js
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
While in first approach I always have a complete user's detail object without making an extra database call.
After reading few blogs I came to know we should keep our payload lightweight but the question in my mind is that on the other hand we have to make a seperate database call everytime we access a protected route.
I want to know which approach is more efficient.
Thanking in advance for the answer:)
Sign the JWT Token with the ID, since that is unlikely to change after initial creation.
Other data like username and email are prone to changes.
EDIT: here's an example User registration from one of my exercises I did a while back. Hope it gives some help.
// POST api/users => Register user
router.post("/", (req, res) => {
const { name, email, password } = req.body;
// Simple validation
if (!name || !email || !password) {
return res.status(400).json({ msg: "Please enter required information" });
}
// Check for existing user
User.findOne({ email }).then((user) => {
if (user) return res.status(400).json({ msg: "User already exists" });
const newUser = new User({
name,
email,
password,
});
// Create salt and Hash
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser.save().then((user) => {
jwt.sign(
{ id: user.id },
jwtSecret,
{ expiresIn: 3600 },
(err, token) => {
if (err) throw err;
res.json({
token,
user: {
id: user.id,
name: user.name,
email: user.email,
},
});
}
);
});
});
});
});
});
Lets suppose you have a payload which contains whole user object including:
_id
name
email
username
Drawback Case:
Assume that the user later edited his name, it means that his data
gets updated in the database but his payload remains outdated with
the old name because no database call is made to fetch the updated record.
If the user now tries to make a POST request which needs his name in
the request body, then the outdated name will be pushed to the
database instead of his new name.
app.post('/event', auth, (req, res) => {
// creates an event with his old name.
// Because the current payload contains old name.
})
Now if user logged out and then logged in again, the token will
contain the new name of the user in the payload.
And whenever all events are fetched using the user's new updated name
saved in the payload, the latest event which was recently created
with the old name will not be fetched because it contains the old
name of the user.
This is the best possible explanation I can give according to my knowledge.
Share your thoughts!
Thanks

restrict users from accessing other user profiles

I have to implement security on my app by preventing users to access other users profile.
route.js
router.get('/currentUser/:username', userAuth, (req, res) => {
User.findOne({
username: req.params.username
}).then(user => {
if (user) {
return res.status(200).json(user);
} else {
return res.status(404).json({
message: 'User not found'
});
}
});
});
and my userAuth.js
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
try {
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, 'app_secret_token');
next();
} catch (error) {
res.status(401).json({
message: 'Authentication failed!'
});
}
};
now if I am logged in as user test so my URL will be http://localhost:4200/currentuser/test but if I change my URL to another user test2 it redirects and loads the test2 even though I am logged as test
how do I prevent this?
You need to also check that the logged in user accesses his data.
you can achieve this by checking the user in the token against the requested page. This means you need to encode the user Id inside the jwt token. That will also make sure this parameter wasn't meddled with since jwt.verify would fail if someone tried to change the jwt token without having the secret.
you can add that data to the jwt token when signing it:
jwt.sign({
userId: 'username'
}, 'secret', { expiresIn: '1h' });
Basically if you save the same data as serializeUser\deserializeUser result, it should also work (the username is just a suggestion).
you can use the callback from jwt.verify to get the decoded token and retrieve that data
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
try {
const token = req.headers.authorization.split(' ')[1];
jwt.verify(token, 'app_secret_token', (err, decoded) => {
if (err) { throw err; }
const currentUsername = decoded.userId; // <-- this should be whatever data you encoded into the jwt token
// if the user requested is different than the user in the token,
// throw an authentication failure
if (req.originalUrl.includes('/currentUser/') &&
!req.originalUrl.includes(`/currentUser/${currentUsername}`)) {
throw new Error('access to other user data denied');
}
next();
});
} catch (error) {
res.status(401).json({
message: 'Authentication failed!'
});
}
};
Even though I think this might be a good case separating this into two different middlewares :-)
PS - as #anand-undavia mentioned, it might be better to identify the user request based on the jwt token itself instead of the 'url' itself. that way, each user should only have access to their own data and this problem can't occur at all.
basically, the user should be accessible with the method above (getting it from the token) or from a req.user field if you use any middleware that adds it automatically.
let us assume that user profile page id mydomian?passedId=userId so simply add profile-guard to check who can visit or activate this page, in CanActivate check if passed id id the same of current user id, then return true else redirect him to previous page
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
let passedId: string = next.queryParams.passedId;
let user = this.authService.GetUser();
if (user.id == passedId)
return true;
else {
this.router.navigate(['/home']); // or any page like un authorized to log to this page
return false;
}
}

Directly authenticate(login) after successful signup in Node.js

How can i directly authenticate the user after signup.
Below are the the deatail of serializeUser and deserializeUser.
passport.serializeUser(function(user, done) {
done(null, {tutorId: user.tutorId, userType: user.userType});
});
passport.deserializeUser(function(userData, done) {
Tutor.getTutorById(userData.tutorId, (err, user) => {
if (err) {
try {
logger.silly(`message: POST inside passport.deserializeUser; file: index.js; error: ${err}; user: ${JSON.stringify(user)}`);
logger.error(`message: POST inside passport.deserializeUser; file: index.js; error: ${err}; user: ${JSON.stringify(user)}`);
} catch (e) {
You can use req.login() after successful registration.
From official Passport documentation:
Note: passport.authenticate() middleware invokes req.login()
automatically. This function is primarily used when users sign up,
during which req.login() can be invoked to automatically log in the
newly registered user.
A sample registration code might look like this:
router.post("/register",(req,res) => {
var user = new User();
user.name = req.body.name;
user.email = req.body.email;
//define other things here
//create hash and salt here
user.save().then(user => {
//on successfult registration
//login user here, using req.login
req.login(user ,err => {
if(!err){
//redirect to logged-in page
//or user page
res.redirect('/')
}
})
})
})
Read about req.login() in official passport documentsation
I hope this helps you out.
you can create token just after successful registration and send it back in registration response

How to hash password before saving to db to be compatible with passport module (passport local)

I am using passport-local strategy of passport for authentication. In my express server, I am getting a register post request and I should save password to db for a new user. But I need to hash the password before saving to db.
But I am not sure how to hash it, since passport will authenticate user by hashing the login password credential to match my hashed password from db. How should I hash my passwords ?
I am using this module.
passport-local does not hash your passwords - it passes the credentials to your verify callback for verification and you take care of handling the credentials. Thus, you can use any hash algorithm but I believe bcrypt is the most popular.
You hash the password in your register handler:
app.post('/register', function(req, res, next) {
// Whatever verifications and checks you need to perform here
bcrypt.genSalt(10, function(err, salt) {
if (err) return next(err);
bcrypt.hash(req.body.password, salt, function(err, hash) {
if (err) return next(err);
newUser.password = hash; // Or however suits your setup
// Store the user to the database, then send the response
});
});
});
Then in your verify callback you compare the provided password to the hash:
passport.use(new LocalStrategy(function(username, password, cb) {
// Locate user first here
bcrypt.compare(password, user.password, function(err, res) {
if (err) return cb(err);
if (res === false) {
return cb(null, false);
} else {
return cb(null, user);
}
});
}));
Have you tried this?
https://www.npmjs.com/package/passport-local-authenticate
var auth = require('passport-local-authenticate');
auth.hash('password', function(err, hashed) {
console.log(hashed.hash); // Hashed password
console.log(hashed.salt); // Salt
});
auth.hash('password', function(err, hashed) {
auth.verify('password', hashed, function(err, verified) {
console.log(verified); // True, passwords match
));
});
auth.hash('password', function(err, hashed) {
auth.verify('password2', hashed, function(err, verified) {
console.log(verified); // False, passwords don't match
));
});
Why should we go for hashing algorithm, when passport already provided it for us? I mean we just need to plugin the passport-local-mongoose to our user schema like: UserSchema.plugin(passportLocalMongoose) and then, inside the register route we just tell the passportLocalMongoose to do the hashing for us by using:
User.register(new User({username:req.body.username}), req.body.password,function(err,newUser)
{
if(err){
something
}else{
something
}
)
By doing above we don't need to take care of hashing and it will be done for us. Please correct me if I am wrong or got your question wrong.

Passport.js Execution

I have been looking at some passport.js tutorials online but haven't grasped a clear understanding of what is happening. Can someone help me clear my doubts below? Please read the paragraph at the bottom first.
So assuming I set up everything correctly, this is the login strategy:
passport.use('login', new LocalStrategy({
passReqToCallback : true
},
function(req, username, password, done) {
// check in mongo if a user with username exists or not
User.findOne({ 'username' : username },
function(err, user) {
// In case of any error, return using the done method
if (err)
return done(err);
// Username does not exist, log error & redirect back
if (!user){
console.log('User Not Found with username '+username);
return done(null, false,
req.flash('message', 'User Not found.'));
}
// User exists but wrong password, log the error
if (!isValidPassword(user, password)){
console.log('Invalid Password');
return done(null, false,
req.flash('message', 'Invalid Password'));
}
// User and password both match, return user from
// done method which will be treated like success
return done(null, user);
}
);
}));
Now in my app.js (server) I have this as one of my routes:
/* Handle Login POST */
router.post('/login', passport.authenticate('login', {
successRedirect: '/home',
failureRedirect: '/',
failureFlash : true
}));
Now in my AJS file:
app.controller('loginController', function($scope) {
var user = $resource('/login');
$scope.createUser = function() {
var User = new user();
User.username = $scope.usernameVar;
User.password = $scope.passwordVar;
User.save();
}
});
Please read this first (Instead of going through the code first):
So when the user clicks on the login button on the login page the createUser function above is run (in my AJS file). Then I create a resource object for the endpoint '/login' and when I call save on that it will run the route for that '/login' endpoint on my server (app.js). Then in my server it will passport.authenticate('login', ... which will run the passport middleware.
Now my question is:
In the passport.use('login'... strategy where the do values for the variables req, username, and password come from in the callback to that strategy. Do I have to explicitly pass the username and password the user enters in the textfield on my front end. Like I have a two way data binding for those two textfields in AJS view. If so how do I pass those username and password values?
Do these two lines in my AJS controller User.username = $scope.usernameVar; and User.password = $scope.passwordVar; attach the usernameVar and passwordVar values to the req object on my server for the route '/login'?
If you have a form with action to post to your path /login, and have input names labeled after your username and password, the submit button will pass the values along to your passport code.
Check out Form in the docs.

Categories

Resources