1

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: '/',
}));
shen
  • 933
  • 10
  • 19
  • 1
    Seems odd that the stringified `req.user` would show as nicely formatted JSON. Try this instead... `console.log('Router get user: ', JSON.stringify(req.user))`. As a basic rule-of-thumb, try not to concatenate objects into strings when logging to the console – Phil Jun 07 '22 at 04:43
  • @Phil It prints the entire object as a string: `{ _id: new ObjectId("629e3821bfb2869c42ac3c4b"), username: 'me', password: '123' }` – shen Jun 07 '22 at 04:45
  • Sorry, the output was `{"_id":"629e3821bfb2869c42ac3c4b","username":"me","password":"123"}`. The question is updated. – shen Jun 07 '22 at 04:51
  • It's `console.log(JSON.stringify(req.user))`. – shen Jun 07 '22 at 04:55
  • Try something like `

    WELCOME BACK "{{user.username}}"

    `. View the page source in your browser (right-click and select _"View Page Source"_). Can you see anything at all between the quotes?
    – Phil Jun 07 '22 at 05:05
  • @Phil `console.log(req.user.username)` got an error: `TypeError: Cannot read properties of undefined (reading 'username') at /Users/Wei/github/play-js/express/authentication/src/app.js:87:24` – shen Jun 07 '22 at 05:14
  • @Phil in the `app.get('/' ...)` router. I've updated the question. – shen Jun 07 '22 at 05:20
  • 1
    This isn't making sense. Could you please _not_ remove your prior logging. Keep both `console.log("req.user", JSON.stringify(req.user)); console.log("req.user.username", req.user?.username);` – Phil Jun 07 '22 at 05:21
  • I listed all three log in the question. `req.user` and `JSON.stringify(req.user)` works well. But `req.user.username` is `undefined`. I think you've already narrow the bug down to `req.user.username` passed to router. – shen Jun 07 '22 at 05:28
  • The point is, you haven't used them all together. You need to be able to see what `req.user` is when you're trying to access `req.user.username`. Please just try the code in [my comment above](https://stackoverflow.com/questions/72525975/handlebars-fails-to-access-property-of-an-object?noredirect=1#comment128117616_72525975) – Phil Jun 07 '22 at 05:29
  • @Phil It said `Handlebars: Access has been denied to resolve the property "username" because it is not an "own property" of its parent.` – shen Jun 07 '22 at 05:33
  • @Phil According to this answer https://stackoverflow.com/a/60368203/3043719, I guess `req.user` is a mongoose object instead of a json object. I have to convert it by `lean()` function somewhere. – shen Jun 07 '22 at 05:38
  • 1
    @Phil In `passport.deserializeUser()` function, I add `lean()` after `User.findById(...).lean()`. The problem solved. A huge THANKS! – shen Jun 07 '22 at 05:46
  • 1
    You should definitely add that as an answer below. Nice work! – Phil Jun 07 '22 at 05:46
  • @Phil Let me do it. But you are the true hero:) – shen Jun 07 '22 at 05:51
  • @Phil Last question, what's the difference between `req.user.username` and `req.user?.username`. The former get `TypeError: Cannot read properties of undefined`, while the later print the correct value `me`? What's the name of syntax like `req.user?.username`? – shen Jun 07 '22 at 06:13
  • 1
    [Optional chaining (?.)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) – Phil Jun 07 '22 at 06:15

1 Answers1

1

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
});
shen
  • 933
  • 10
  • 19