~ 9 min read

OWASP Node.js Authentication, Authorization and Cryptography Practices

share this story on
Properly implementing authentication and authorization is crucial for securing Node.js apps. This section covers guidelines like proper session management, password hashing, and attack prevention.

Let’s learn about authentication best practices, authorization and cryptograph guidelines for Node.js. Properly implementing authentication is crucial for securing Node.js apps.

This section covers guidelines like proper session management, password hashing, and attack prevention. I previously reviewed OWASP Node.js Best Practices Guide which covers other areas like input validation, injection attacks, cross-site scripting, and more.

In this post, you’ll learn about:

Manage Sessions Securely

Failing to properly manage user sessions can lead to account hijacking. You’ll probably and up working on authentication controls and would need to be familiar with techniques like session cookies, token rotation, and destroying sessions after logout.

Here are some key session authentication tips you want to keep in mind for Node.js security hygiene:

  • Use session cookies. It’s often easier to go for the JWT authentication method and storing them in local storage, however if you’re not sure on the exact security details and requirements then a session cookie and a server-side store is best and most time-tested approach.

  • When using session cookies, set the httpOnly cookie flag. This ensures mitigation against XSS attacks, which would allow attackers to steal session cookies and hijack other user accounts on your app.

  • Generate a long, random session ID token for each user. Rotate the session token on each request if possible, or at least on a regular basis and when the user performs sensitive actions such as changing their password, email, or other account details.

  • Destroy and invalidate sessions on the server after logout. Don’t rely on front-end session removal.

Hash and Salt Passwords

Plaintext passwords are vulnerable to interception, but realistically even more so to data theft due to data breaches and data leak. Learn how bcrypt, scrypt, and other algorithms can securely hash passwords in Node.js.

  • Use bcrypt or scrypt for password hashing. These algorithms are designed to be slow to hinder brute force attacks. In Node.js, you can use the native module via the npm package bcrypt or the built in scrypt Node.js API in the crypto module.

  • Salt each password hash with a unique, random salt value. Salting ensures no two password hashes are identical. If you’re using bcrypt, the salt is automatically generated and stored in the hash, and unique for each password.

  • Compare hashes rather than plain passwords during authentication. Never store unhashed passwords. Even for something as simple as comparing two data variables, you have to take security in account. Use a constant-time string comparison function to prevent timing attacks.

👋 Just a quick break

I'm Liran Tal and I'm the author of the newest series of expert Node.js Secure Coding books. Check it out and level up your JavaScript

Node.js Secure Coding: Defending Against Command Injection Vulnerabilities
Node.js Secure Coding: Prevention and Exploitation of Path Traversal Vulnerabilities

Prevent Brute Force Attacks

Implementing rate limiting, CAPTCHAs, lockouts after failed attempts, and other defenses can prevent account brute forcing.

  • Enforce rate limiting after a number of failed login attempts from an IP address and other key identifiers.

  • Temporarily lockout accounts after repeated failed attempts to access that account. This prevents attackers from brute forcing a single account.

  • The bcrypt algorithm is designed to be slow to hinder brute force attacks, via a configurable work factor. Use a high work factor to slow down attackers.

Proper session management, password hashing, and attack prevention are crucial for securing Node.js authentication. Following OWASP guidelines helps mitigate common account vulnerabilities.

Authorization Practices in Node.js

Authorization is another critical aspect for securing access to sensitive resources and functionality in Node.js applications.

Proper authorization prevents unauthorized users from accessing pages or data they should not see. Here I’ll cover several best practices for implementing authorization in Node.js, including role-based access control, maintaining allowlist and denylist routes, and layered authentication checks.

Implement Role-Based Access Control

Assigning users specific roles with predefined permissions provides fine-grained control over access to Node.js application resources and functions.

For example, an admin role may have full access while a guest role can only view certain pages. Popular Node.js frameworks like Fastify (and Express) have a plugin or middleware capability to enable role-based access control.

Here’s a stock example for a route handler that checks if a user has the admin role, and relies on the req.user object to determine the role, which is set by the authentication middleware that precedes this authorization route handler:

function checkRole(role) {
	return (req, res, next) => {
		if (req.user.role !== role) {
			res.status(401);
			return res.send('Not allowed');
		}

		next();
	};
}

app.get('/admin', checkRole('admin'), (req, res) => {
	// Admin page
});

Defining and assigning roles appropriately restricts access for unauthorized users. A lot of time though, that’s harder than to do in practice for production-grade applications.

I recommend using a third-party authorization service to ease up the process. One such service is Permit.io which is a lightweight authorization library for Node.js that supports role-based access control, allowlist and denylist routes, and more.

Allow-list Allowed Routes and Methods

Denylist and allowlist specific routes helps restrict access to authorized users. For example, you may allowlist paths starting with /app to only allow admin while blocking everything else:

function checkPath(path) {
	const allowedPaths = ['/app'];

	if (!allowedPaths.find((p) => path.startsWith(p))) {
		res.status(401);
		return res.send('Not allowed');
	}

	next();
}

app.get('/api/users', checkPath, (req, res) => {
	// Show users
});

Implementing these access control lists prevents access to unauthorized routes. In larger scale applications you’d want to centralize the access control list and use a middleware to check the path against the list.

Layered Authorization Checks

Verifying permissions at multiple points, like middleware and route handlers, prevents unapproved access even if one check is bypassed.

function checkAuth(req, res, next) {
	if (!req.user) {
		res.redirect('/login');
	}

	next();
}

app.get('/account', checkAuth, checkRole('user'), (req, res) => {
	// Show account
});

Here the checkAuth middleware and checkRole handler together act as layered auth to increase security.

Following authorization best practices like these helps protect access to Node.js apps and their sensitive data. Proper access controls are crucial for security.

Cryptography Guidelines for Node.js

Cryptography provides critical protections for data confidentiality and integrity when implemented properly. This section briefly reviews a high-level security overview for encryption, key management, hashing, and related areas in Node.js.

Properly Use Encryption Algorithms

Choosing the right encryption algorithm and configuring it correctly is essential to prevent data exposure.

Here are some key tips for choosing and configuring encryption algorithms to prevent data exposure, referencing industry standards like NIST and FIPS.

  • NIST Compliance: adhere to algorithms approved by the National Institute of Standards and Technology (NIST). Their guidelines are widely recognized for security and reliability.

  • FIPS 140-3: Prioritize algorithms validated under the Federal Information Processing Standard (FIPS) 140-3, ensuring they meet stringent government security requirements.

  • AES for Confidentiality: Adopt the Advanced Encryption Standard (AES) for symmetric encryption, offering robust protection for sensitive data. AES-256 is widely considered one of the strongest encryption algorithms available today, offering a very high level of protection for data.

  • RSA for Key Exchange: Utilize RSA for secure key exchange and digital signatures in asymmetric encryption, such as when you need to use a public key and a private key to encrypt and decrypt data. RSA is a widely used and trusted algorithm for this purpose.

  • SHA-3 for Hashing: Employ SHA-3 (Secure Hash Algorithm 3) for data integrity verification. SHA-3 is a cryptographic hash function that generates a unique hash value for each input. It’s a secure and reliable algorithm for verifying data integrity.

Whatever you choose, evaluate the strengths and limitations for each algorithm. For example, AES-256 is a strong encryption algorithm but it’s computationally intensive and may not be suitable for all use cases, especially on low-powered devices.

Securely Encrypting Data at Rest in Node.js Apps

Using the built-in Node.js crypto module, you can encrypt data at rest using the AES-256-GCM algorithm. Here’s a simple example:

const crypto = require('crypto');

function encrypt(data) {
	const algorithm = 'aes-256-gcm';
	const key = crypto.randomBytes(32);
	const iv = crypto.randomBytes(16);

	// Create the cipher
	const cipher = crypto.createCipheriv(algorithm, key, iv);

	// Encrypt the data
	let encrypted = cipher.update('my secret data', 'utf8', 'hex');
	encrypted += cipher.final('hex');

	// Add the auth tag
	const authTag = cipher.getAuthTag();

	// The encrypted data
	const encryptedText = `${encrypted}:${authTag}`;
	return encryptedText;
}

And for decrypting the data later:

const crypto = require('crypto');

function decrypt(encryptedText) {
	// Split the encrypted data and auth tag
	const [encrypted, authTag] = encryptedText.split(':');

	const algorithm = 'aes-256-gcm';
	const key = crypto.randomBytes(32);
	const iv = crypto.randomBytes(16);

	// Create the decipher
	const decipher = crypto.createDecipheriv(algorithm, key, iv);

	// Verify the auth tag
	decipher.setAuthTag(authTag);

	// Decrypt the data
	let decrypted = decipher.update(encrypted, 'hex', 'utf8');
	decrypted += decipher.final('utf8');

	// The decrypted data
	const decryptedText = decrypted;
}

Key takeaways from the above Node.js encryption and decryption code examples:

  • AES-256-GCM: Provides strong encryption, authentication, and integrity protection.
  • Strong Key: Use crypto.randomBytes(32) to generate a secure key.
  • Secure Key Storage: Store the key securely (e.g., secrets manager, even though most developers will end up relying on environment variables).
  • Authentication Tag: Use the authentication tag to verify data integrity during decryption.

Additional considerations for even stronger security, consider using a key derivation function (KDF) to derive the encryption key from a password or passphrase, and regularly rotate keys to mitigate the impact of potential compromises.

What is AES-256-GCM ?

The AES (Advanced Encryption Standard) is a symmetric encryption algorithm, meaning the same key is used for both encryption and decryption. It’s widely adopted as a strong and efficient standard for encrypting sensitive data.

This algorithm is available in different key lengths, with AES-256 offering the highest level of security due to its 256-bit key size.

What is GCM (Galois/Counter Mode)?

It’s an authenticated encryption mode of operation that combines encryption and authentication. It provides confidentiality by encrypting data to prevent unauthorized access. It also provides integrity to ensure data hasn’t been tampered with during transmission or storage.

The authenticity verifies the origin of the data. It offers high performance and security, making it a popular choice for modern applications.

As a whole, the AES-256-GCM combines AES-256 encryption with GCM mode. This offers robust protection for sensitive data by combining confidentiality, integrity, and authenticity in a single algorithm. It’s a NIST-approved and widely used in various security-critical domains.

Secure Generation and Storage of Keys

The codecov security incident from April 2021 is a good example of how a compromised key can lead to a data breach.

Compromised cryptography keys completely bypass encryption defenses. Follow these practices for robust key management:

  • Use a key derivation function like PBKDF2 to generate keys from high entropy passphrases.

  • Store keys securely - not in code or configs. Use hardware security modules where possible. Otherwise, use a secure key management system such as AWS KMS or HashiCorp Vault.

  • Enforce key rotation policies and carefully destroy old keys. Rotate keys when employees leave the company or when they are suspected to be compromised.

Use Password Hashing Algorithms

Don’t fall for the oldest trap in the book of storing passwords in plaintext, or encrypting them. Account passwords should be hashed, not encrypted.

Apply strong cryptographically secure password hashing algorithms like bcrypt when storing user passwords to prevent plaintext exposure. Here are some key tips for password hashing:

  • Salt and hash passwords before storing them.

  • Use adaptive hash functions like BCrypt, which are designed to be computationally intensive.

  • Compare hashed passwords using constant-time string comparison functions.

Following cryptography best practices is crucial for building secure Node.js apps. Proper implementation provides powerful protections while mistakes can lead to total failure.


Node.js Security Newsletter

Subscribe to get everything in and around the Node.js security ecosystem, direct to your inbox.

    JavaScript & web security insights, latest security vulnerabilities, hands-on secure code insights, npm ecosystem incidents, Node.js runtime feature updates, Bun and Deno runtime updates, secure coding best practices, malware, malicious packages, and more.