The client for my web-app wants to totally separate regular users and admins, hence I'm trying to implement two local strategies for passport.js:
passport.use('local', new LocalStrategy({
usernameField: 'email'
}, function(email, password, done) {
User.findOne({ email: email }, function(err, user) {
if (err) return done(err);
if (!user) return done(null, false, { message: 'Wrong email or password.' });
if (!user.validPassword(password)) return done(null, false, { message: 'Wrong email or password.' });
done(null, user);
});
}));
passport.use('admin', new LocalStrategy({
usernameField: 'email'
}, function(email, password, done) {
Admin.findOne({ email: email }, function(err, admin) {
if (err) return done(err);
if (!admin) return done(null, false, { message: 'Wrong email or password.' });
if (!admin.validPassword(password)) return done(null, false, { message: 'Wrong email or password.' });
done(null, admin);
});
}));
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
Admin.findById(id, function(err, admin) {
if (err) return done(err);
if (admin) return done(null, admin);
User.findById(id, function(err, user) {
done(err, user);
});
});
});
And then in my API admin router:
router.post('/', function(req, res, next) {
if (typeof req.body.email === 'undefined') return res.json({ success: false, message: 'Email not supplied.' });
if (!validator.isEmail(req.body.email)) return res.json({ success: false, message: 'Wrong email format.' });
if (typeof req.body.password === 'undefined') return res.json({ success: false, message: 'Password not supplied.' });
if (req.body.password.length === 0) return res.json({ success: false, message: 'Password not supplied.' });
passport.authenticate('admin', function(err, admin, info) {
if (!admin) return res.json({ success: false, message: info.message });
req.logIn(admin, function(err) {
if (err) return res.json({ success: false, message: 'Server error.', reason: err });
console.log('user:');
console.log(req.user); // both users and admins go here
console.log('admin:');
console.log(req.admin); // undefined
res.json({ success: true });
});
})(req, res, next);
});
In fact, I was following the answer here: https://stackoverflow.com/a/21898892/1830420, and Matthew Payne gets two session variables: req.user and req.sponsor. In my case, even if an admin authenticates, it gets written to req.user. Now, trying to implement my client's desire of totally separating users and admins, I want to get req.user and req.admin, but every time it gets written to req.user. I thought changing LocalStrategy name would help, but passport.js seems to ignore both the strategy name and model name.
Any help is very much appreciated.
P.S. I know I can have everyone in User model and write some middleware to protect certain routes based on roles, but then again, unfortunately, I don't get to choose here.
Unfortunately you can't do that with Passport. You will need to create express middleware to handle admin authentication.
And like Hya answered passport dont are good for authorization ...
You need to use middlewares for set admin context and admin roles:
// add this middlewares after your routes...
// set admin context and others things like admin templates
app.use('/admin/*', function adminContext(req, res, next) {
// set admin context
req.isAdmin = true;
next();
});
// then get roles for authenticated user in your passport stategy:
app.use(function getUserRoles(req, res, next) {
req.userRoleNames = [];
if (req.isAuthenticated()) {
req.userRoleNames.push('authenticated');
} else {
req.userRoleNames.push('unAuthenticated');
return next(); // skip role load if dont are authenticated
}
// get user roles, you may get roles from DB ...
// and if are admin add its role
req.userRoleNames.push('administrator');
next();
});
// and check roles in admin page
app.get('/admin/admin-page/', function (req, res, next) {
if (req.userRoleNames.indexOf('administrator') == -1) return res.status(403).send('forbidden');
console.log('roles:', req.userRoleNames);
res.status(200).send('Hey you are the administrator!');
});
See we.js core for see one advanced roles and permissions implementation: https://github.com/wejs/we-core
Related
Guys I can make user register and login. But I want to make a admin login. This admin will be a user too. So that I tried to make a ensureAdmin function. But it doesnt work. İt always redirects to login page.
passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'
},
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
bcrypt.compare(password, user.password, function(err, isMatch){
if(err) throw err;
if(isMatch){
return done(null, user);
} else {
return done(null, false, {message: 'Wrong password'});
}
});
});
}
));
function ensureAdmin(req, res, next){
User.find({"username":"Alp"}
).exec(function(user){
if(req.user = user){
req.isAuthenticated()
return next();
} else {
req.flash('danger', 'Please Login');
res.redirect('/login');
}
});
}
Can someone please help me to solve this?
In ensureAdmin function I see an error in if statement.
if(req.user = user) ...
Other than that, you are complecating the flow.
User.find({"username":"Alp"}) query would always return the user with username Alp. So, you can rewrite the function like this:
function ensureAdmin(req, res, next){
if(req.user.username === 'Alp'){
req.isAuthenticated()
return next();
} else {
req.flash('danger', 'Please Login');
res.redirect('/login');
}
}
As per my understanding,
Add a new field to user document as isAdmin
Then, create a new middleware as ensureAdmin
const ensureAdmin = (req, res, next) => {
if (!req.user){
return next(new Error("User Document not found."));
}
if (!req.user.isAdmin){
return next(new Error("User not authorized to access this route."));
}
return next();
}
You can use it like this wherever needed:
app.post("/signin", passportLogin, ensureAdmin, controller.signin);
This will firstly check the user authentication from passportLogin and push the user document to this path req.user. So now our middleware ensureAdmin play the game. It will check the admin status isAdmin. If isAdmin is true it will push to our controller function otherwise throw it to error handlers.
I'm trying to add a conditional to passport.js that also checks if the account is confirmed or not by inserting an 'if statement' to the bottom of the code after user email and password have been checked.
I tried several ways to add the if statement to the last part of the code in the 'LocalStrategy' function and tried reworking the whole thing with elif, and else statements. I'm getting 'Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client' and other errors as it is below.
I'm afraid I can't provide all the code for you to reproduce this.
Unless you have a mock sign page and mongodb running with passport.js sign in and an account with a true/false like user.confirmed is for my account.
const LocalStrategy = require('passport-local').Strategy;
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Load User model
const User = require('../models/User');
module.exports = function(passport) {
passport.use(
new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
// Match user
User.findOne({
email: email
}).then(user => {
if (!user) {
return done(null, false, { message: 'That email is not registered' });
}
// Match password
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) {
console.log(err);
res.sendStatus(500);
return;
} if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Password incorrect' });
}
});
// LINES I'M TRYING TO ADD HERE BELOW
if (!user.confirmed) {
return done(null, false, { message: 'This account is not confirmed' });
// LINES I'M TRYING TO ADD HERE ABOVE
}
});
})
);
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
};
In my setup the above code is in the config file as passport.js and used by a js file in the 'routes' folder that logs the user in with
router.post('/login', (req, res, next) => {
passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/users/login',
failureFlash: true
})(req, res, next);
The field 'confirmed' is either 'true' or 'false'. Read with !user.confirmed. (The process of confirmed or !confirmed is handled with a link sent to the user's email. This is outside the scope of my issue but sharing in case this helps frame the problem.)
I would like for passport.js to check the account with the email exists, that the password is correct, THEN let the user login ONLY if the account is confirmed 'true' and post the message that the account is not confirmed and deny entry if it is not confirmed.
I can't seem to add the confirmed check without errors and losing the other checks for things. With this current setup it tells you if the email is NOT registered. It tells you if the user is NOT confirmed, but it crashes if you enter the wrong password with a registered email.
So unless I am missing something
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) {
console.log(err);
res.sendStatus(500);
return;
}
if (isMatch && user.confirmed) {
return done(null, user);
} else if (isMatch) {
return done(null, false, { message: 'This account is not confirmed' });
} else {
return done(null, false, { message: 'Password incorrect' });
}
});
Other permutations are available.
I have an Express app with the passport-local strategy, using Mongoose for storing user Accounts. I had a freelancer write this part of the app for me because I couldn't make sense of how to build the login system from beginning to end (every single tutorial I found did it in a different way). The drawback of this is that I don't understand what each part does. This is in my app.js file:
const Account = require('./models/db/AccountsSchema');
app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(Account.authenticate()));
passport.serializeUser(Account.serializeUser());
passport.deserializeUser(Account.deserializeUser());
and this is in routes/index.js:
router.post('/register', function(req, res) {
Account.register(new Account({
username: req.body.username,
name: req.body.name
}), req.body.password, function(err, account) {
if (err) {
console.log(err);
} else {
console.log(account);
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/login'
})(req, res, function(err, user) {
res.redirect('/');
});
}
});
});
along with:
router.post('/login',
passport.authenticate('local'),
function(req, res) {
res.redirect('/');
}
);
Now in the login POST request I want to have a check for whether a user with that particular username exists, so that if the password is wrong at least I can tell the user that the password is wrong. User enumeration is not a security concern here.
Where in my code can I incorporate a database check for whether an Account with the specified username exists?
In Account.authenticate() function. As it sets the LocalStrategy in your case.
setting your local strategy:
passport.use(new LocalStrategy(Account.authenticate()));
Sample Code:
function authenticate(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
For a regular authentication, the 'Incorrect password' message is available via failureFlash
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true })
);
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));
But if I use a custom callback, how can I access that 'Incorrect password' message and show it to the user ? Because the custom callback only seems to check for (!user). I need custom callback as I am doing login via ajax and I cannot have redirects.
app.get('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.status(401).send({"ok": false}); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return return res.send({"ok": true});
});
})(req, res, next);
});
The message is in the info parameter in your custom callback handler making it very easy to access and send to the user.
On a side not, I wouldn't specify if the username or the password was the cause of the login failure. Using that kind of response it is very easy to test which usernames exist, and then just focus on the password.
I'm new to node express and passport so I'm not sure what I'm missing. I'm trying to use a custom callback as all I want is a json response from my registration strategy. Currently I keep getting the following error "Missing credentials". I've been at it for a while now and I'm stuck.
Here is my controller:
app.post('/services/authentication/registration', function(req, res, next) {
console.log('before authentication')
passport.authenticate('local-registration', function(err, user, info) {
console.log('authentication callback');
if (err) { return res.send({'status':'err','message':err.message});}
if (!user) { return res.send({'status':'err','message':info.message});}
if (user!=false) { return res.send({'message':'registration successful'});}
})(req, res, next);
},
function(err, req, res, next) {
return res.send({'status':'err','message':err.message});
});
And my passport strategy:
passport.use('local-registration', new LocalStrategy({
// by default, local strategy uses username and password, we will override with email
usernameField : 'email',
passwordField : 'password',
passReqToCallback : true
},
function(req, email, password, done) {
console.log('credentials passed to passport' + email + '' + password)
// asynchronous
// User.findOne wont fire unless data is sent back
process.nextTick(function() {
// find a user whose email is the same as the forms email
// we are checking to see if the user trying to login already exists
User.findOne({ 'local.email' : email }, function(err, user) {
// if there are any errors, return the error
if (err)
return done(err);
// check to see if theres already a user with that email
if (user) {
return done(null, false, {message: 'User already exists'});
} else {
// if there is no user with that email
// create the user
var newUser = new User();
// set the user's local credentials
newUser.local.email = email;
newUser.local.password = newUser.generateHash(password);
// save the user
newUser.save(function(err) {
if (err)
throw err;
return done(null, newUser);
});
}
});
});
}));
I dont think you should send "req" as a parameter to the authentication function. take a look at this example from the passport docs:
passport.use(new LocalStrategy(
function(username, password, done) {
User.findOne({ username: username }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
});
}
));