Express is one of the most popular web frameworks for Node.js, with tons of documentation and tutorials available. It was purposefully designed to be flexible and "unopinionated", which can help you get new projects up and running quickly...
...until you smack head-first into user authentication.
Unlike frameworks in other languages (such as PHP's Django), Express doesn't have a built-in login system. It's up the developer to figure out how to authenticate users and handle their data - and security is tough! As a result, most tutorials pull in the middleware package Passport.js for assistance. Passport supports a variety of "strategies" that can be used to verify the identity of a user attempting to access your application, including Open Id Connect with Google, oAuth with Facebook, and more. And since those third-party strategies usually have even more setup steps, many tutorials resort to the "simplest" option -- the passport-local strategy which stores usernames and passwords in a database you control.
⚠️ A note about username/password
It's worth pausing for a moment to consider: is storing passwords the right choice for your project in the first place? While the 'local' strategy does get you up and running quickly, most tutorials leave out important steps for safely handling passwords. (Heck, even these one isn't as in-depth as it could be!)
Some strongly recommended reading includes:
🙄 Pshh, I'm sure I have this under control - I'm salting and hashing stuff!
Okay, well...while it's a great step to store passwords hashed and salted, it's also important to think about retrieval. Even if our passwords aren't in plain text, we still don't want to make them accessible to users! If passwords can be saved to a malicious person's machine, they have all the time in the world to try cracking them. (And if your password protection isn't as rigorous as you think, that can take as few as a couple minutes!) So it's important to make sure that your project both stores passwords securely and avoids leaking them back out.
For example, consider an Express project using the Sequelize ORM. Perhaps we have a user model like this:
class User extends Model {
validPassword(passwordToCheck) {
return bcrypt.compareSync(getSHA512(passwordToCheck), this.password);
}
}
User.init({
nickname: {
type: DataTypes.STRING,
allowNull: false,
validate: { is: /^[0-9a-z_]+$/i, len: [1, 32] }
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
}, {
hooks: {
beforeCreate: (user) => {
if (typeof user.password !== "string" || user.password.length < 8 || user.password.length > 128) {
throw new Error("Invalid incoming password!");
}
user.password = bcrypt.hashSync(getSHA512(user.password), bcrypt.genSaltSync(12), null);
}
},
sequelize
});
Now, let's say we write a route that is meant to get a list of all users so we can display them:
router.get("/api/users", (req, res) => {
db.User.findAll({})
.then(result => res.json(result))
.catch(err => res.json(err));
});
But if we then hit this route with a client...
...whoops, our passwords are exposed!
😆 Well I would be more careful lol!
Maybe so! It's always important to think about what information our queries are selecting. Trouble is, it's also easy to miss places where this data is cropping up. For example, let's say we want to get data about the user who is currently logged in. If we were copying from Passport's documentation for the local strategy, the login configuration probably ended up looking something like this:
var passport = require('passport')
, LocalStrategy = require('passport-local').Strategy;
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);
});
}
));
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
A quick run-down: When a user first logs in, passport will attempt to find a record for their email in our database. If both email and password are correct, the user's information is passed along through the middleware. Very commonly, Express/Passport are also configured to start a 'session' for that user using their unique id as the way to recall who they are. Every time the user makes a subsequent request, the deserialization process will look up the user's current information from the db using that id. The fresh info will then be attached to req.user.
So if you wanted to make a route that grabs info about the user who is currently logged in, it might be tempting to do something like this:
router.get("/auth/whoami", (req, res) => {
if(!req.user) {
return res.sendStatus(401);
}
res.json(req.user);
})
And once again we are exposing a password! Since req.user is meant to be used internally, this isn't typically called out in tutorials.
😡 Okay yeah, this is getting annoying!
Well knowing is half the battle, and now that we've seen a couple places where sensitive data might leak, we can certainly pay more attention to how we write queries. But the more complex a project gets, the easier it becomes to make a mistake. What if we had an extra layer of protection that prevented us from accidentally retrieving sensitive info from the database in the first place?
🛡️ Your new buddy: exclusion-by-default 🛡️
Many ORMs (such as Sequelize and Mongoose) provide a way to exclude specific fields/columns from query results by default. The developer must then specifically override that behavior on the rare occasion that they wish to access that data. By making it hard for this information to leave the database in the first place, it's harder to slip up further down the line. Plus, this isn't limited to passwords -- we can apply this strategy to anything we don't want to share broadly! Here's how it works with Sequelize.
When defining a Model, we will add a few additional items to our options object: 'defaultScope' and 'scopes':
User.init({
...
}, {
hooks: {
...
}
},
defaultScope: {
attributes: { exclude: ['password'] },
},
scopes: {
withPassword: {
attributes: {},
}
},
sequelize
});
defaultScope allows us to specify that we should not normally be able to retrieve the 'password' field. However, the default configuration for passport still needs it! As a result, we define a 'custom' scope called 'withPassword' -- this will retrieve everything. We also need to modify a line in our passport config:
...
db.User.scope('withPassword').findOne({
where: {
email: email
}
})
...
And so in one fell swoop, we have fixed our /api/users:
...as well as that /auth/whoami route:
Though I'd still recommend caution with req.user -- remember, that's internal!
😄 Hey, that was pretty cool! Now I don't have to stress as much.
Absolutely! An ounce of prevention is always worth a pound of cure. When using ORMs, we might as well take advantage of their features to make repetitive work easier. By designing our application so that sensitive information stays in the db by default, we help ourselves nip some problems in the bud.
(And lastly, remember: if it works for your project to avoid the password game entirely, other strategies are there for you too ;)
Top comments (0)