~ 5 min read
Poor Express Authentication Patterns in Node.js and How to Avoid Them
It’s ok to roll your own authentication if you want to build that into your Express applications, but following bad security advice is not going to end well. More often than not, I’m double-clicking into security related guides and tutorials on blogs because I’m curious at what and how other developers are teaching security topics and what they consider as best practices.
Unfortunately, I’ve come across a lot of guides that are teaching poor authentication patterns. I’ll skip the name calling but I’ll point out this was a dev.to post that was highly upvoted (279 bookmarked) and it was teaching a poor authentication pattern.
I’ll share two specific snippets that are really obvious: one related to cookie setup in an Express application and the other related to a login route handler in classic Node.js applications.
Avoid Poor Cookie Setup in Express Applications
Here’s an example of a Node.js application using Express.js to implement session authentication, straight from the original blog post:
const express = require('express');
const session = require('express-session');
const app = express();
// Middleware setup
app.use(session({
secret: 'your_secret_key',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Set the cookie as HTTP-only, Optional
maxAge: 60*30 // In secs, Optional
}
}));
Let’s call out the problems:
1. Hardcoded Secrets
Problem: We often see examples with sensitive information like secret keys stored directly in the code (e.g., secret: your_secret_key
). I realize this is a tutorial, but it’s a bad practice to hardcode secrets in your codebase and then push it to a public repository. At least, add a reference to .env
files or use a process.env.SECRET_KEY
code reference to access the secret key that conveys the point about security best practice here.
Solution: Move the secret key definition to a dedicated environment variable. This allows separation of concerns and keeps sensitive information out of the codebase. You can access environment variables in Node.js using process.env.VARIABLE_NAME
.
Recommended read on this topic is environment variables and configuration anti patterns in Node.js applications
2. Poor Cookie Configuration
Not only poor cookie configuration but also a lack of security headers in the response, and generally a lack of good security practices in cookie setup.
Consider the following cookie configuration that are completely lacking:
- Secure Flag: The secure flag should be set to true to ensure the cookie is only transmitted over HTTPS connections. This mitigates eavesdropping on unsecured connections.
- SameSite Attribute: The
SameSite
attribute helps prevent Cross-Site Request Forgery (CSRF) attacks. Setting it tostrict
provides the strongest protection but might require additional configuration. Considerlax
as a compromise for broader compatibility. - Domain: The
domain
attribute specifies the domain for which the cookie is valid. By default, it’s set to the server’s domain. If your application operates on a single subdomain, set the domain to that subdomain (e.g., domain: ‘yoursubdomain.example.com’). For applications spanning multiple subdomains under the same main domain, consider using a wildcard domain (e.g., domain: ‘.example.com’). However, use caution with wildcards as they can introduce unintended behavior with third-party cookies. - Path: The path attribute specifies the path on the server where the cookie is valid. Set the
path
to the specific path where the cookie is needed (e.g., path: ‘/api’). This restricts the cookie’s scope and reduces the risk of exposure on unintended paths.
3. No mention of CSRF
The blog post doesn’t mention CSRF protection at all. CSRF attacks are a common threat to web applications, and it’s important to protect against them. If you are writing about a session-based authentication with cookies, you should at least mention Cross-Site Request Forgery (CSRF) as a potential threat and how propose some ways to mitigate it.
4. No mention of Session Expiration
The example sets a maxAge
for the cookie, but it’s recommended to also implement server-side session expiration. This ensures sessions are invalidated even if the user’s browser doesn’t clear the cookie properly.
Avoid Poor Login Route Handlers in Node.js Applications
The following is a bad example of a login route handler in a Node.js application, however this is a copy&paste from the original blog post:
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (user) {
req.session.userId = user.id; // Store user ID in session
res.send('Login successful');
} else {
res.status(401).send('Invalid credentials');
}
});
Let’s call out the problems here too:
1. Plain Text Password Storage
Problem: The code compares the user’s password directly with the stored password (u.password
). This assumes passwords are stored in plain text, which is highly insecure.
Solution: Always hash passwords using a strong hashing algorithm like bcrypt
before storing them in the database. When a user logs in, hash the provided password and compare the hash with the stored hash. This ensures even if the database is compromised, attackers cannot easily obtain user passwords.
2. Vulnerability to Timing Attacks
In the given code, if the password comparison is done character-by-character, an attacker could potentially exploit the time difference between a correct and incorrect character match. With enough attempts, they might be able to guess the password based on subtle variations in response times.
There are several ways to prevent timing attacks in password comparisons:
-
Constant Time Comparison: Use libraries like
crypto.timingSafeEqual
(built-in Node.js) to ensure the comparison takes a constant amount of time regardless of password length or correctness. These libraries perform comparisons in a way that avoids timing leaks. -
Hash-Based Comparison: As mentioned earlier, always store passwords as hashes. During login, hash the provided password and compare it with the stored hash using a constant time comparison function. This eliminates the possibility of timing attacks based on character-by-character comparisons.
What else can be done?
Additional considerations include implementing rate limiting on login attempts to make brute-force attacks with timing analysis significantly slower and less effective, re-generating session ids on sensitive operations (changing a password or email), lockout mechanism after a certain number of failed login attempts to further deter attackers, and more.
More in-depth security practices concerning Node.js, authentication and secure coding in general are: