Passport.js - serializeUser doesn't find user properties after lodash _.merge - node.js

I have a strange problem with implementing serializeUser() on my express app. My data model has called for a single type of "user" to exist in two models, basically an Identity (username, password, role, etc) and then role-specific data, such as Author (data specific to the Author role). I solve this by having a ref (ObjectID) of an Identity stored inside my Author model. Thus, my signup function appears as follows (simplified):
// Mongoose will just take what it needs for each model
var identity = new Identity(req.body);
var author = new Author(req.body);
identity.save(function(err) {
if (!err) {
// Save reference to identity
author._identity = identity._id;
// Save author document, then login
author.save(function (err) {
// create new "user" object from both identity and author objects
var user = _.merge(identity, author);
req.login(user, function(err) { res.json(user); });
});
}
});
This is working fine and everything is stored nicely in mongo. However, for serializeUser(), I want to store user._identity, not user._id (or user.id as it is strangely used), in order to access the ObjectID of the Identity document, not the Author document:
passport.serializeUser(function(user, done) {
console.log('user', user); // returns full, concatenated object
console.log('_id', user._id); // returns ObjectID value
console.log('_iden', user._identity); // returns undefined
// breaks with a "Failed to serialize user into session" error
done(null, user._identity);
});
What is strange is that console.log(user) gives the full (concatenated object), and user._id returns a value, but user._identity returns undefined (and no other value is returned for that matter. Any insight into why this might be happening or a different approach I should take?
NOTE: This seems to be caused by the _.merge call. I'm using lodash v.2.4.2

Turns out mongoose doesn't like attempting to _.merge two documents - simply using the document.toObject() method resolved this issue:
var user = _.merge(identity.toObject(), author.toObject());
Not bad at all!

Related

Associating user data between multiple passport strategies

I'm using node, express, express-session, mongoose and passport-discord to authenticate users. But I also wish to use passport-steam to optionally link a user's steam account to their discord account.
The problem is, in order to link their steam account, I would also need a way of knowing which discord user is trying to do so, but the authentication callback doesn't include any request/response headers to access the current session, and the database I'm storing discord users in doesn't have any steam data in it until steam's authentication callback is called, at which point I have no idea which discord user is trying to authenticate their steam account.
To help remedy this, I've setup 3 mongoose models to have 3 separate database collections:
DiscordUser (stores discord user information, including their discordId)
SteamUser (stores steam user information, including their steamId)
User (stores required discordId and optional steamId)
I then try to serialize and deserialize the User object, but the passport.deserializeUser(obj, done) method seems to pass in an id instead of a User object, and I can't be sure if it's a discordId or a steamId, or which discord user I'm deserializing a SteamUser for, so I end up back at square one.
What's the best way to go about associating these users?
If needed, I can supply some code snippets upon request.
I found an article which helped solve this problem.
Here's what I did to fix it:
Using passReqToCallback: true in the new Strategy(...) options
In both strategy callback functions, adding req as the first parameter(ie., async function callback(req, accessToken, refreshToken, profile, done) { ... })
And then changing this:
router.get('/steam', passport.authenticate('steam'), (req, res) => {
//...
});
To this:
// Note that the route here is the same as the callback/redirect
router.get('/steam/redirect', (req, res, next) => {
passport.authenticate('steam', (err, user, { nextRoute }) => {
if (err)
return next(err);
if (nextRoute) {
// user was authorized with provider?
// send em back to profile
return res.redirect(nextRoute);
} else {
// give em some cookies
req.logIn(user, err => {
if (err)
return next(err);
return res.redirect('/');
});
}
})(req, res, next)
});
And then finally, modifying the User model schema to include the sessionID, which can be accessed via req.sessionID (or req.session.sessionID) from within the callback functions of both strategies.
This allows me to update the database upon successful login with my primary strategy(s) (discord, in this case), and then reference it from secondary strategies (steam, in this case), providing enough time to update the User database to have linked information between each strategy for as long as the session is still valid. After which, you can always simply reference the User database anywhere in code to check for valid primary and secondary strategy ids, allowing you to cross-reference other database collections for information, update the session id if the current session id doesn't match (and possibly forcing the user to re-authenticate?), etc.

How to add current logged in status to Users Schema in MongoDB using Passport and node.js

I’m quite new to backend development…
With using my API I would like to be able to display a list of users and also indicate if they are currently logged in. I got the basic authentification working using passport and json web token
I’m not looking to get the current logged in user.
I want to be able to retrieve a list of users and see if they are logged in or not.
Like this:
var users = Users.find({});
// console.log(users) output:
{
name: 'foo'
password: ...
isLoggedIn: false
},
{
name: 'bar'
password: ...
isLoggedIn: true
},
{
name: 'baz'
password: ...
isLoggedIn: false
}
isLoggedIn would be set to true if the user is currently logged in and to falseif not.
How can I do that? Thank you!
It sounds like what you would like to do is update your MongoDB database based on login/logout events. To do this you could use something like mongoose to work with your Node backend to easily access your database in MongoDB.
You can include mongoose after installing with npm install mongoose like so:
var mongoose = require('mongoose');
var User = mongoose.model('User');
Note that User corresponds to whatever schema you create for storing user information.
Assuming you have some sort of router object for handling requests, you could construct route handlers for /logout and /login and use your imported mongoose User model to retrieve and then modify a specific User object as such:
// whenever user goes to '/login' (you can have, say, your 'login' button make a request to this URL
router.get('/login', function(req,res) {
// your authentication here; passport stores the currently authenticated user in req.user
var username = req.user.name; // here we assume the username is stored as 'name' as you have in your code but change this based on your schema
User.findOne({name: username}, function(err, user, data) {
if(err) res.send(err);
user.isLoggedIn = true;
user.save(function (err) {
if (err) {
console.log(err);
} else {
// redirect to some page here maybe
}
});
});
});
// whenever user goes to '/logout' (you can have a logout button make a request to this URL
router.get('/logout', function(req,res) {
// currently authenticated user is still in req.user
var username = req.user.name;
User.findOne({name: username}, function(err, user, data) {
if(err) res.send(err);
user.isLoggedIn = false;
user.save(function (err) {
if (err) {
console.log(err);
} else {
// redirect to login/register page maybe
}
});
});
});
So to summarize what this code would do:
based on the url a user would go to, our route handler would fetch one correct, unique User object from our database based on the name (username)
it would do so by accessing the username property of req.user which corresponds to the currently authenticated user with Passport, which, again will be different for all users
update the field that we use to keep track of login status (isLoggedIn)
and then save the changes, after which we are done updating the state to reflect whether the user is logged in or not, so we can now redirect to some other page or display other content
Finally then, you could retrieve a list of all users similarly to your code like so:
User.find({}, function(err, users, data) {
// all users from your database are in `users`
console.log(users);
});
Edit for expired sessions:
So, to track expired sessions, since you're using Passport, would in theory require functionality to signal with some sort of event / callback / message, etc. the moment the session is deemed invalid. Now that is tough to monitor and from my experience with Passport, stuff like that isn't implemented in all authentication strategies and might vary based on the strategy to be used by developers (think for instance if a browser window is closed, based on Passports authentication strategy, or just browser, it might destroy the cookie for the session right away and our server has no way of knowing about it). I do recommend checking out all the authentication strategies Passport offers in case there are some better ones here.
Now, if you would like to add functionality to track the users passive login/logout status with sessions yourself, you could use something related to cookies. Again, not necessarily one to use, but here's a couple handy Express modules: cookie-parser and cookie-session.
Then, you could set and read cookies like this, using cookie-parser:
var express = require('express');
var cookieParser = require('cookie-parser');
var app = express();
app.use(cookieParser());
You would put this code somewhere right after the user is authenticated:
// cookies are stored here
console.log(req.cookies);
// configure your cookie
var options = {
expires: 1000 * 60 * 60, // expires after one hour
httpOnly: true
}
// Set cookie
res.cookie('session', ('user-' + req.user.name), options);
And then, on the client side check if that cookie is valid continuously on some time interval, and if it expired Date.now() > cookie.expires then make a GET request to /logout, and there log out the user (currently still authenticated) by updating MongoDB and all.
However, since this would require making a mechanism to basically simulate an expired session, I would recommend using something analogous to a timeout, which would be much easier to implement. Just a note, this is sort of analogous to mechanisms on some pages you might have encountered where you get a pop-up saying 'You will be logged out due to inactivity'. In your main.js or whatever client-side script define a function to keep going on a time-out, unless the user does some action.
var inactivity = function () {
var t;
// user doing something on your page, so keep resetting time counter when events happen
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
// this is a callback function that will get called once a time-out countdown is done
function timeOut() {
// make a request to '/logout' here and logout the current user (you still will have access to req.user from Passport)
// also can redirect from back-end route handler to the login page for instance
}
// this gets called whenever an event happens, resetting the counter of sorts
function resetTimer() {
t = 0;
t = setTimeout(timeOut, 1000 * 60 ) // set this to however long you should wait to log out your user time (in milliseconds)
}
};
So basically what this approach would let you do, is automatically invalidate sessions yourself, which means you would have much greater control over updating the state of your database and logging users out.
Hope this helps!

Passport.js async vs sync deserialization

I'm implementing a payment feature on my app, and my provider requres AccountId (identification of the user in my system) and Email. I've noticed some strange behaviour I cannot explain with Passport.js. First of all, deserialization looks like in docs:
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
Now, I get that this is an async operation, however when I form the data for the request, I do:
var data = {
AccountId: toString(req.user._id),
Email: req.user.auth.local.email
// ...
}
For some reason Email gets set correctly, but AccountId always returns [object Undefined]. Well, I thought that maybe it's due to Passport's async nature of deserialization (since it requires time to User.findById()), but why does Email gets set correctly then?
I've found a way of setting AccountId to req.session.passport.user, but it seems like a hack to me.
Your problem is here:
AccountId: toString(req.user._id),
toString is a method of an object. In the browser, simply calling toString assumes that you mean window.toString() and it returns [object Window]. In Node, since the global window does not exist, you get [object Undefined].
I think the way you meant to call that function would be like so:
AccountId: req.user._id.toString(),

nodejs express profile property in request

I got very confused for one usage:
In the route file:
app.param('userId', users.load);
And the users.load function:
exports.load = function (req, res, next, id) {
var options = {
criteria: { _id : id }
};
User.load(options, function (err, user) {
if (err) return next(err);
if (!user) return next(new Error('Failed to load User ' + id));
req.profile = user;
next();
});
};
Here, route should have the userId to response but why does the author use req.profile here. profile is not a property.
Anyone can help?
Thanks.
What the code does is this: for routes that have a userId parameter (that is, routes that look similar to this: /user/:userId), Express will call the load() function before the route handler is called.
The load function loads the user profile belonging to the userId from the database, and adds it to req as a newly created property req.profile.
The .profile property name is arbitrarily named by the author and demonstrates the fact that it's perfectly valid to add properties to req (or res, for that matter, but convention is to add these properties to req).
In the route handler, you can then use req.profile. It's basically a way of propagating data from middleware and app.param() implementations to other parts of the route handling.
the line req.profile = users; think of it this way, 'i want to take all the powers of the users and paste them to req.profile' why? remember this part is sort of a middleware if you want to target any of the read, update and delete code it has to pass through here, it only makes sense if it involves the req, because you are practically requesting to access the said pages (read, edit and delete or any other:userId page) now the profile name doesn't matter you could use any name but its sort of a convention in the community to use the profile name.

Tidy callbacks node.js

Trying to think of a logical way of structuring this. For simplicity, I am creating a user registration page utilising Node.js, Redis and Express.js.
User posts details to page.
Username is confirmed to be valid then Redis checks username is unique. If it is, we continue, if it isn't we return an error message and all the previous details.
Password is confirmed to be valid. If it isn't an error is returned and we don't continue.
Email is confirmed to be unique with Redis. If it is we continue, if it isn't we return an error message and stop.
If no errors at this point, the data is inserted into Redis.
This seems very simple, but using callbacks has generated a total mess - particularly when it comings to returning an error.
How can I structure this in a tidy way?
What you've experienced is callback hell. There are a variety of things you could do like name your callback functions instead of inlining them, follow the same pattern etc...
However, I would recommend you have a look at the async module.
In your, very typical, case I would use async.series like this:
var validateUserName = function(username, callback){
if(username!='valid'){
return callback('invalid username');
}
};
var checkRedis = function(username, callback){
// check to redis
};
var checkPassword = function(password, callback){
// if password valid callback();
// else callback('invalid password');
}
etc...
async.series([checkUserName, checkRedis, checkPassword, etc...], next);

Resources