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.
Top comments (17)
Thank you for the article,
I have a question, when you use a 3rd party service like Google, do you still store/create the user in your database. I guess I want to better understand the flow for signup and login authentication when you use a 3rd party auth service. Thanks.
Yes, you do, David.
The only difference is you don't have to store their password. Google/whoever will do the authentication part and give you a unique id for that particular user which you can store if they don't exist in your db or simply sign them in if they do.
I suggest looking into PassportJs or AuthJs (as I've also learnt from the discussion here). Feel free to reach out to me directly if you need any help or guidance.
Oh, that is true I will try it out. I am actually using next-auth.
Thank you very much that is very kind of you, I will actually be taking you up on your offer, because I am kind of tackling this issue at the moment with a personal project and I want to have better understanding of auth process.
I also researched and saw access token and refresh token as means to having better security, what is your perspective on this?
Thanks.
This is also my my first time hearing about next-auth...we are really blessed to have so many options as developers.
Acess Token and refreshtoken aren't that difficult terms. in simpler terms, accesstoken is the key you give to users to enter a gated place on your site after validating that they are who they are(authentication).
Refresh token just makes the process of issuing Acess Token seamless for the user so they don't have to reenter their details everytime their accesstoken expires
reach out through: @AsaboroD on twitter; we can take it up from there ;)
I can't send u message directly but I sent hello as a tweet.
Nice article! What about when using Auth.js?
No, I don't. In fact, this is my first time of hearing about it.
Also, correct me if I'm wrong...AuthJs is a library? if so...I use PassportJs, haven't got any reason to search for alternatives yet...However, Auth Js seems like an interesting library(simple to implement
at first sight)
Yes, It is simple. Best for using on Next.js full stack projects.
Yeah, I read that too...
I probably need a project that will force me to look into Next.js (an hackathon maybe), what do you suggest, Varga?
Maybe have a look at my latest article if you want to get deeper inside Vercel and the Next.js ecosystem. HERE you can find it.
I like a lot of the points in this article. I think Iβve decided that Iβm never going to write a password system again. Auth0, cognito, Okta, and various OAuth identity providers make it so that storing user passwords is no longer necessary.
I avoid storing user passwords like the plague.
Thanks for pointing that out, Michael...that means a lot to me.
I'm also with you on the identity providers thing. Once you start integrating Oauth and the rest, you never want to go back...even for the end users...but some situations, one I found myself a few months ago still require going the old way....it is what it is.
software is about democraticizing acess.
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
Thanks you so much. You have really enlightened me about this. Am really grateful for this.
You are welcome, Ebine. It's a pleasure to do so.
Storing the user's password IS a critical mistake.
There are cases where it's unavoidable