Poor Express Authentication Patterns in Node.js and How to Avoid Them
Tags: node, express, authentication, security
Published: 2024-06-06 05:13:43

---
title: Poor Express Authentication Patterns in Node.js and How to Avoid Them
published: true
date: 2024-05-03 00:00:00 UTC
tags: nodejs, express, authentication, security
canonical_url: https://lirantal.com/blog/2024-05-03/
---

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](https://lirantal.com/blog/environment-variables-configuration-anti-patterns-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 to `strict` provides the strongest protection but might require additional configuration. Consider `lax` 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:

- [Input Validation Security Best Practices for Node.js](https://www.nodejs-security.com/blog/input-validation-best-practices-for-nodejs)
- [Protecting Against Common Node.js Vulnerabilities](https://www.nodejs-security.com/blog/protecting-against-common-nodejs-vulnerabilities)
- [Node.js Security Best Practices](https://www.nodejs-security.com/blog/nodejs-security-best-practices)