Handlebars fails to access property of an object - javascript

After authentication (with passport module), a template was rendered with the req.user object as follow,
app.get('/', (req, res) => {
console.log(`Router get user: ${req.user}`);
console.log("Router get user of type: " + (typeof req.user));
res.render('index', {
layout: false,
user: req.user,
});
});
I checked the req.user by console.log, and the user object was printed as expected,
Router get user: {
_id: new ObjectId("629e3821bfb2869c42ac3c4b"),
username: 'me',
password: '123'
}
The second console.log showed the type of req.user is object,
Router get user of type: object
After convert the req.user to a string,
app.get('/', (req, res) => {
console.log(JSON.stringify(req.user));
console.log("Router get user of type: " + (typeof req.user));
res.render('index', {
layout: false,
user: req.user,
});
});
the output became,
{"_id":"629e3821bfb2869c42ac3c4b","username":"me","password":"123"}
Router get user of type: object
If I further log req.user.username as follow,
app.get('/', (req, res) => {
console.log(req.user.username);
console.log("Router get user of type: " + (typeof req.user));
res.render('index', {
layout: false,
user: req.user,
});
});
I got the error,
TypeError: Cannot read properties of undefined (reading 'username')
at /Users/Wei/github/play-js/express/authentication/src/app.js:87:24
But when I used user.username in the template file, it didn't show the username.
<body>
{{#if user}}
<h1>WELCOME BACK {{user.username}}</h1>
{{/if}}
</body>
But when I replace the {{user.username}} by {{user}}, the user object was printed correctly,
<body>
{{#if user}}
<h1>WELCOME BACK {{user}}</h1>
{{/if}}
</body>
WELCOME BACK { _id: new ObjectId("629e3821bfb2869c42ac3c4b"), username: 'me', password: '123' }
But according to the Handlebars Doc, Handlebars expression CAN be dot-separated paths.
So what's the problem here?
Here's the complete code how I set up the express server and passport authentication,
// connect to MongoDB
const mongoDB = process.env.DB_URI;
mongoose.connect(mongoDB);
const db = mongoose.connection;
db.on('error', console.error.bind(console), 'MongoDB connection error');
// Schema & Model
const userSchema = new Schema({
username: {
type: String,
required: true,
},
password: {
type: String,
required: true,
}
});
const User = mongoose.model('User', userSchema);
// Express server
const app = express();
app.set('views', path.join(__dirname, 'views'));
const eh = handlebars.create(); // ExpressHandlebars instance
app.engine('handlebars', eh.engine); // register the engine() function
app.set('view engine', 'handlebars');
// Middleware
app.use(morgan('dev')); // logger
app.use(session({
secret: 'cats',
resave: false,
saveUninitialized: true,
}));
app.use(passport.initialize());
app.use(passport.session()); // this middleware will set cookie in client computer for each session.
app.use(express.urlencoded({
extended: false,
}));
// Verify username & password in our database
// Register the LocalStrategy to the passport.
passport.use(
new LocalStrategy(function verify(username, password, done) {
User.findOne({username: username}, (err, user) => {
if (err) return done(err);
if (!user) return done(null, false, {message: 'Incorrect username'});
if (user.password !== password) return done(null, false, {message: 'Incorrect password'});
return done(null, user);
});
})
);
// Only store user._id in the cookie.
passport.serializeUser(function(user, done) {
console.log(`serialize: ${user._id}`);
done(null, user._id);
});
// Get the user object from database by searching user._id.
passport.deserializeUser(function(_id, done) {
console.log(`deserialize search for: ${_id}`);
User.findById(_id, function(err, user) {
console.log(`deserialization find user: ${user}`);
done(err, user);
});
});
// router
app.get('/', (req, res) => {
console.log(JSON.stringify(req.user));
console.log("Router get user of type: " + (typeof req.user));
res.render('index', {
layout: false,
user: req.user,
});
});
app.post('/log-in', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/',
}));

With the help of #Phil, we found the bug in deserializeUser() function.
// Get the user object from database by searching user._id.
passport.deserializeUser(function(_id, done) {
User.findById(_id, function(err, user) {
console.log(`deserialization find user: ${user}`);
done(err, user);
});
});
passport.deserializeUser() simply extracts user._id stored in cookies, and search for entire user object from MongoDB, which will be lately set as our req.user.
The problem is that User.findById() return a mongoose Document object, which doesn't have its own username property.
What we need here is a plain javascript object. We can achieve that by enable the lean option right after the findById() function.
// Get the user object from database by searching user._id.
passport.deserializeUser(function(_id, done) {
User.findById(_id, function(err, user) {
console.log(`deserialization find user: ${user}`);
done(err, user);
}).lean(); // it returns a plain javascript object now
});

Related

Missing Credentials while using Passport on Node JS and Express

I'm working on a project developing an API. The thing is I'm working with Passport and there's this thing that throws me an error of "Missing Credential". I've searched everywhere and I don't know what to do.
Part of Index.js
app.use(express.static(publicc));
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(passport.initialize());
app.use(session({
secret: 'secret',
resave: false,
saveUninitialized: false
}));
app.use(passport.session());
app.use(cors());
app.use(morgan('dev'));
Passport.js with the SignUp Function
passport.use('signup', new localStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, async (email, password, done) => {
try {
console.log('si')
const user = await findUser(email);
if (user.length > 0) {
return done(null, false, { message: 'User already exists' });
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const newUser = {
user_name: req.body.user_name,
user_role: req.body.user_role,
email: email,
user_password: hashedPassword
};
console.log(newUser);
const result = await createUser(newUser);
return done(null, newUser);
}
catch (error) {
console.log('si')
done(error);
}
}
));
Controller with the SingUp
const postSignup = async (req, res, next) => {
passport.authenticate('signup', { session: false }, async(err, user, info) => {
try{
console.log(req.body);
if(err){
const error = new Error('An error occurred');
return next(error);
}
const message = info.message;
if(!user){
res.status(401).json({ success: false, message });
return
}
return res.json({succes: true, message});
}catch(error){
return next(error);
}
})(req, res, next);
};
I already tried adding the body parsing and nothing changed. I've searched every forum but still no answer.
I see you are saving email and password at DB, but you don't save salt to compare recieved password from user on sign in with that you have saved at DB.
https://en.wikipedia.org/wiki/Salt_(cryptography)
You steps to auth.
Get user by email from database.
Check that hashed password that you recieved from user + salt saved at database for this user equals hashed password at your database. Or just remove salt, don't hash password (but it strongly not recommended), after it will be work, try to complicate the task and hash passwords without salt. After it will be works, try to add salt.
Look at this example how to use passport with local strategy + Express.
https://github.com/jaredhanson/passport-local/wiki/Examples

Passport deserializeUser() is never called with Axios http request

When I send a request via axios to my Express app, req.isAuthenticated() is always false and req.user does not exist, even after logging in. But when I send a request via Postman to the app, it works. It seems that deserializeUser() is never called so the req.session.passport field is never populated.
I've tried all of the suggestions online, any help is appreciated.
External request:
async tweet(content) {
try {
await axios.post(this.url + '/tweets/new', {
content: content,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
}
});
} catch (err) {
console.log(err);
}
}
index.js
const passport = require('passport');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const RedisStore = require('connect-redis')(session)
const redisCookie = require('heroku-redis-client');
require('./config/passport')(passport);
// required for passport
app.use(cookieParser());
app.use(session({
// secret: process.env.SECRET || 'enteryoursecrethere',
secret: 'enteryoursecrethere',
cookie: { maxAge: 3600000 },
resave: true,
store: new RedisStore({client: redisCookie.createClient()}),
saveUninitialized: true
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
next();
});
router.js
var tweets = require('../controllers/tweets');
var router = express.Router();
var isLoggedIn = require('../middleware/isLoggedIn');
router.post('/tweets/new', isLoggedIn, tweets.tweet);
middleware/isLoggedIn.js
module.exports = (req, res, next) => {
// If user is authenticated in the session, carry on.
if (req.isAuthenticated()) {
next();
return
}
// If they aren't redirect them to the home page.
res.redirect('/');
}
passport.js
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models').User;
const Sequelize = require('sequelize');
module.exports = function(passport) {
// The login request establishes a session maintained in a browser cookie.
// Requests after the login request not contain credentials,
// but rather the unique cookie that identifies the session. The user object
// is constructed to and from the ID in the cookie.
// Converts user to user id.
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// Converts user id to user, stored in req.user.
passport.deserializeUser(function(id, done) {
User.findById(id).then(function(user) {
done(null, user);
}).catch(function(err) {
done(err);
});
});
/* ============Login============ */
passport.use('local-login', new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
passReqToCallback : true // Send entire request for flash message.
}, loginCallback));
passport.use('local-signup', new LocalStrategy({
usernameField: 'username',
passwordField: 'password',
passReqToCallback : true
}, signupCallback));
};
function loginCallback(req, username, password, done) {
if (req.isAuthenticated()) {
return done(null, req.user);
}
// Look up the user by username.
User.findOne({
where: {
username: username
}
}).then(function(user) {
if (!user) {
return done(null, false, req.flash('loginUsernameMessage', 'Wrong username.'));
}
if (!user.validatePassword(password)) {
return done(null, false, req.flash('loginPasswordMessage', 'Wrong password.'));
}
return done(null, user.get());
}).catch(function(err) {
return done(err);
});
}
function signupCallback(req, username, password, done) {
// Asynchronous. User.findOne wont fire unless data is sent back.
process.nextTick(function() {
if (password != req.body.password_confirm) {
return done(null, false, req.flash('signupMessage', 'Passwords don\'t match.'));
}
// 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({
where: {
[Sequelize.Op.or]: [ { username: username }, { email: req.body.email }]
}
}).then(function(user) {
// Check to see if theres already a user with that username or email.
if (user) {
return done(null, false, req.flash('signupMessage', 'That email or username is already taken.'));
}
// Create the user.
var data = {
fname: req.body.fname,
lname: req.body.lname,
username: username,
email: req.body.email,
password: User.generateHash(password)
}
User.create(data).then(function(newUser) {
return done(null, newUser);
}).catch(function(err) {
return done(err);
});
}).catch(function(err) {
return done(err);
});
});
}
It works only with postman because the session is being set at the server(express) but not at the client(axios).
So when you request from axios, It doesn't know if the session is already set at the server. So you need to send the credentials origin along with the request. Modify your request like this:
await axios.post(this.url + '/tweets/new', {
content: content,
withCredentials: true,
headers: {
'Content-Type': 'application/json',
},
credentials: "same-origin"
});
Refer this for more info.
This question is a year old now but I had a heck of a time solving this problem using Node/Express/Passport/React with a local-strategy and express-session.
For me, adding this to the header:
withCredentials:true
wasn't enough. That did trigger a CORS problem but did not actually make axios send the session cookie with the request.
This causes a situation where the login would create a session, but the deserialize function would never be called and req.user would always be empty.
This is what fixed the issue:
In my React component constructor I added this line of code:
axios.defaults.withCredentials = true;
That fixed it.
In fact, it fixed it so well that I was able to remove all other headers in Axios. Everything just started working.
Figuring this out took me a solid two hours today, I hope this answer helps someone.

Logging with role in Node.js and Express

I'm new in Node.js and Express and I have a problem with access control with Passport.
I want to check with which role user is logged in.
My user schema:
let mongoose = require('mongoose');
let userSchema = new mongoose.Schema({
name:{
type: String,
required: true
},
email:{
type: String,
required: true
},
password:{
type: String,
required: true
},
username:{
type: String,
required: true
},
role:{
type: String,
default: 'user'
},
activated:{
type: Boolean,
default: false
}
});
let User = module.exports = mongoose.model('User', userSchema);
My passport.js file
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/user');
const dbConfig = require('../config/database');
const bcrypt = require('bcrypt');
module.exports = (passport) => {
//Local Strategy
passport.use(new LocalStrategy((email, password, done) => {
//Match email
let query = {email: email}
User.findOne(query, (err, user) => {
if(err){
console.log(err);
}
if(!user) {
return done(null, false, {message: 'No user found'});
}
//Match password
bcrypt.compare(password, user.password, (err, isMatch) => {
if(err){
console.log(err);
}
if(isMatch){
return done(null, user);
} else {
return done(null, false, {message: 'Wrong password'});
}
});
});
}));
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
}
The small part of app.js file where I'm setting up global variable user
app.get('*', (req, res, next) => {
res.locals.user = req.user || null;
next();
});
Part of layout.pug file where I'm using global user variable
.navbar.collapse.navbar-collapse
ul.nav.navbar-nav
li
a(href='/') Home
if user
li
a(href='/articles/add') Add article
What I want to do is to check if user has an admin role. I tried to do that in different ways but with no success. I wanted to check the role in app.js, but I was getting errors:
if(req.user.role == 'admin'){
res.locals.admin = req.user;
}
Also I wanted to pass only the role in passport.js and it was working then but I need also other property of user.
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
How can I resolve my problem?
You should check if req.isAuthenticated() then checking the role. Cuz if req.user is null then you can't check the property 'admin' of null
if(req.isAuthenticated() && req.user.role == 'admin'){
res.locals.admin = req.user;
}
The req.isAuthenticated() function is provided by passport.js

Display passport.js authentication error message in view

I have a new Sails.js project using Passport.js to authenticate users. I have the basic authentication working (meaning a user can sign up and successfully log in), but would like to display the appropriate error message in the login view if they don't enter the correct credentials. I can't figure out how to print any error messages in the view.
Here's my setup. I have config/passport.js, which contains the following:
var passport = require('passport'),
LocalStrategy = require('passport-local').Strategy,
bcrypt = require('bcrypt');
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findOne({ id: id } , function (err, user) {
done(err, user);
});
});
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback : true
},
function(req, email, password, done) {
User.findOne({ email: email }, function (err, user) {
if (err) { return done(err); }
if (!user) {
return done(null, false, { message: 'Please enter a valid email address.' });
}
if (!req.body.username) {
return done(null, false, { message: 'Please enter your username.' });
}
bcrypt.compare(password, user.password, function (err, res) {
if (!res) {
return done(null, false, {
message: 'Invalid Password'
});
}
var returnUser = {
username: user.username,
email: user.email,
createdAt: user.createdAt,
id: user.id
};
return done(null, returnUser, {
message: 'Logged In Successfully'
});
});
});
}
));
Then I have api/controllers/AuthController.js, which contains the following:
var passport = require('passport');
module.exports = {
_config: {
actions: false,
shortcuts: false,
rest: false
},
login: passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login',
failureFlash: true
}),
logout: function(req, res) {
req.logout();
res.redirect('/login');
}
};
Again, this is working properly if the user fills in their correct credentials. I'm using Handlebars as my templating engine, and would like to display the error message in the login view like so:
<div class="alert">{{ message }}</div>
So far I've tried {{ failureMessage }} {{ message }} {{ req.flash.failureMessage }} {{ req.flash.err }} {{ req.flash.message }} to no avail. So all this to say, how do I display the appropriate error message in my view? And better yet, how would I highlight the errant field in my view?
At first glance, looks like sails is using Express 3.0. Per the Passport docs (http://passportjs.org/docs), you will need to explicitly add middleware to support flash (they recommend https://github.com/jaredhanson/connect-flash).
Not a passport expert here, but according to this all you need to do is to re-render the login view with req.flash('error'):
res.render("login", {error: req.flash("error")});
And then in the handlebars template, display the error:
{{ error }}
Don't know if this might help you, Brad.
https://stackoverflow.com/a/25151855/3499069
I think in your serializeUser call you need it to be user[0].id, but I've moved away from Passport recently, so I could be wrong.
In my opinion you didn't include express flashes ?
var flash = require('connect-flash');
app.use(flash());
in your router add
req.flash(type, message);
and using
var t = req.flash(type);
res.render(view, {flashMessage: t});

User not serialized when using async auth route and passport.js

I've got a node app that operates in isolation. Without js on in the client it just operates synchronously, setting cookies with passport.js. When the client is js enabled then the auth is done via a rest route.
All seems fine, except that if I have authenticated (and have cookies set) asynchronously, but then refresh the page (or navigate synchronously to a new one [for whatever reason]), the server sends the response with new cookies that overwrite the old ones and setting the user back to being unauthenticated.
// =============================================================================
// AUTHENTICATE (FIRST LOGIN) ==================================================
// =============================================================================
// process the login form
app.post('/login', passport.authenticate('local-login', {
successRedirect: '/profile', // redirect to the secure profile section
failureRedirect: '/browse?p=0', // redirect back to the signup page if there is an error
failureFlash: true // allow flash messages
}));
app.post('/async/login', function(req, res, next) {
passport.authenticate('local-login', function(err, user, info, status) {
if (err) {
return res.send(err);
}
if (user) {
user.local = null;
return res.send(user);
} else {
return res.send(info);
}
})(req, res, next);
});
// =========================================================================
// passport session setup ==================================================
// =========================================================================
// required for persistent login sessions
// passport needs ability to serialize and unserialize users out of session
// used to serialize the user for the session
passport.serializeUser(function(user, done) {
done(null, user.id);
});
// used to deserialize the user
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
// =========================================================================
// LOCAL LOGIN =============================================================
// =========================================================================
passport.use('local-login', new LocalStrategy({
// by default, local strategy uses username and password, we will override with email
usernameField: 'username',
passwordField: 'password',
passReqToCallback: true // allows us to pass in the req from our route (lets us check if a user is logged in or not)
},
function(req, email, password, done) {
// asynchronous
process.nextTick(function() {
User.findOne({
'local.email': email
}).populate({
path: 'spots comments currentLocation'
}).exec(function(err, user) {
// if there are any errors, return the error
if (err)
return done(err);
// if no user is found, return the message
if (!user)
return done(null, false, req.flash('loginMessage', 'No user found.'));
if (!user.validPassword(password))
return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.'));
// all is well, return user
else
User.populate(user, {
path: 'spots.bird',
model: 'Bird'
}, function(err, user) {
if (err)
return done(err);
else
User.populate(user, {
path: 'spots.location',
model: 'Location'
}, function(err, user) {
if (err)
return done(err);
else
console.log(util.inspect(user, showHidden = false, depth = 5, colorize = true));
return done(null, user);
});
});
});
});
}));
Node App:
app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(cookieParser());
sessionVars = {
keys: ['<removed>'],
//secureProxy: true, // if you do SSL outside of node
maxAge: 2592000000
};
app.use(session(sessionVars));
app.use(flash());
require('./../modules/auth/passport')(passport);
app.use(passport.initialize());
app.use(passport.session());
Does anyone know why?
UPDATE: The async route doesn't seem to call the serializeUser function (which explains everything!). Does anyone know how to force serialization?
If I try a direct call: passport.serializeUser(user, done) then I need the done parameter, which isn't available in the routes file (although I could pass it through);
Does anybody know if serializeUser is meant to be called as a matter of course with passport strategies?
So I found that if you use the custom form of the authenticate function:
app.get('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});
it doesn't seem to pass it through the serializeUser function that you provide. The cookies for sessions stay in your browser and change all the time, even when you're logged out as they probably still 'tracking' your behaviour.
If you use the 'black box' version of the function:
app.post('/login',
passport.authenticate('local', { successRedirect: '/',
failureRedirect: '/login' }));
that works because it calls serializeUser automatically.

Categories

Resources