Throughout my years of tweaking, designing, and engineering software systems, I've come to realize a fundamental truth: there are no perfect systems, no perfect process, and no perfect solutions — only the necessary tradeoffs.
For instance, It has been repeatedly demonstrated that longer passwords/passphrases are a better measure of security, and not complicated ones.
However, imposing stringent security protocols on users can be inconvenient, indirectly compromising the initial security goal —which is why the National Institute of Standards and Technology (NIST) recommends a minimum password length of 8 characters.
Hashing User's password is another example.
Generally, the more times a password is hashed, the stronger it becomes. However, hashing itself is a resource-intensive and time-consuming task for Computer processors. Thus, finding the optimal compromise is crucial to ensure both protection and user experience.
The same notion applies to storing passwords in NoSQL databases like MongoDB. While these databases offer flexibility and scalability, they may require additional measures to ensure password security.
A common mistake among software architects
The number one benefit of MongoDb, is unlike relational database, it lets you read and write data about a specific entity in one sweep — No complex joins, No rigid schema, and No impedance mismatch!
But most people take this too literally! They end up storing password hashes on the User document. What a big mistake — one which I fell into until recently.
But first, what's wrong with this approach?
"Aren't they hashed?", you ask. I'll explain, so read on.
🛑 Why storing users password on the User document is bad practice ❌💀
One, too many oppurtunities for mistake.
Storing password hashes in the User document increases the attack surface area since the User document is one of the easily accessible object in an application runtime.
Once an attacker gains unauthorized access it, then they have immediate access to both the password hashes and other confidential user data — you definitely don't want that, at least not that simple!
And worse, if an attacker has access to multiple users account, they can compare the hashes and implement a bruteforce. Besides, storing user password in the User document adds more overhead to your data bandwith when you only need it just once: during Authentication.
A popular way developers mitigate this risk is using Mongoose's
schema-level projections(setting the select property to false).
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const userSchema = new Schema({
email: {
type: String,
required: true
},
password: {
type: String,
required: true,
select: false
}
});
Here, The user's password will be not be retrieved when you do User.find()
unless you project it using select('+password')
let foundUser = await User.findOne();
foundUser.password; // undefined
//Using Schema-level projections
foundUser = await User.findOne().select('+password');
foundUser.password; // String containing password hash
The problem with this approach is it limits flexibility and it makes password retrieval unnecessarily tedious. You'd have to use complex queries to ensure you are selecting the right property, at the right time, and not excluding others (just too many chance to fuck up).
And lastly, storing password hash on the User document makes it challenging to enforce regular password updates or implement password complexity requirements.
Your job is to make it as hard as possible to make a major mistake says Technical Architect, Valeri Karpov. How do you do that? Beside outsourcing authentication to a third-party provider like Google, Apple, or Facebook using PassportJs...what follows is how I'm handling User's password for the projects I work on.
And it's simple: Seperate Password from the User's document.
This ensures the impact of a potential breach is mitigated.
User Model
const mongoose = require('mongoose');
const Auth = require('../auth/auth');
const bcrypt = require("bcrypt");
const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true
},
// other attributes removed for brevity
}, { timestamps: true, }
);
const User = mongoose.model('User', userSchema);
module.exports = User;
Auth Model
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');
const authSchema = new Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
type: {
type: String,
enum: ['PASSWORD', 'FACEBOOK_OAUTH', 'GOOGLE_OAUTH'],
default: 'PASSWORD'
},
secret: {
type: String,
required: true,
},
}, { timestamps: true });
authSchema.pre('save', async function (next) {
try {
const salt = await bcrypt.genSalt();
this.secret = await bcrypt.hash(this.secret, salt);
next();
} catch (error) {
console.log(error);
throw error('Something Wrong Happened');
}
})
const Auth = mongoose.model('auth', authSchema);
module.exports = Auth;
Other than security, this approach also follow a software design concept: Principle of Data Locality. This principle states that a document should contain all data necessary to display a web page for that document. More on that here.
Security shouldn't be an afterthought
At least not in this era where data breaches and privacy concerns have become way too common. And so, securing your User's passwords should be a top priority, not just an afterthought.
As developers, we have a responsibility to prioritize user data security. It's not enough to use strong algorithms in hashing and salting password — How accessible are they to unauthorized personnels?
This is why we must embrace the Seperation of concerns and understand that Password are sensitive information which deserve special attention.
Let us strive for a future where user privacy is upheld. Together, we can create a safer digital landscape and inspire others to follow suit. This article is just one way, there's many more.
It's my default choice, but there are cases where people are sceptical to connect their social accounts with your app...(when working on apps used my old folks) this is where an email and password comes in :)
tough work, but it's a must