Node.js Authentication: Best Practices and Key Strategies
#authentication #webdev #node #security
This article showcases secure login strategies for Node.js, implementing multiple security techniques to safeguard user credentials, sessions, and account access.
Key Security Features
1. Password Hashing with Salt
Passwords are hashed using a strong hashing algorithm (e.g., bcrypt) along with a random salt for each password. This ensures that even if the hashed password is compromised, it cannot easily be reversed into the original password.
Why its important:
Implementation:
import bcrypt from 'bcrypt';
const hashPassword = async (password: string): Promise<string> => {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
return await bcrypt.hash(password, salt);
};
2. Token-Based Authentication
JWT (JSON Web Token) is used for token-based authentication, allowing stateless authentication between the client and server. Users receive a JWT upon login, which is then sent with every subsequent request in the Authorization header.
Why its important:
Implementation:
import jwt from 'jsonwebtoken';
const generateToken = (userId: string) => {
return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '1h' });
};
3. Refresh Tokens
A refresh token is used to obtain new access tokens without requiring the user to log in again. This ensures a smooth experience for users without compromising security.
Why its important:
Implementation:
const generateRefreshToken = (userId: string) => {
return jwt.sign({ userId }, process.env.REFRESH_SECRET, { expiresIn: '7d' });
};
4. HTTP-Only Cookies
Store sensitive tokens such as refresh tokens in HTTP-only cookies. This prevents JavaScript from accessing the tokens, protecting them from XSS (Cross-Site Scripting) attacks.
Why its important:
Implementation:
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
});
5. JWT Blacklisting
JWT tokens do not have built-in revocation. To invalidate tokens (e.g., on logout), JWT blacklisting is used. A database or Redis is used to store invalidated tokens until they expire.
Why its important:
Implementation:
const blacklistToken = async (token: string) => {
await redisClient.set(token, 'blacklisted', 'EX', tokenExpirationTime);
};
6. Single Session at a Time
Ensure that only one session is active for a user at any given time. When a new session is created, invalidate previous tokens or sessions.
Why its important:
Implementation:
// Schema for User table:
sessionId: { type: String, unique: true }
//login API
const newSessionId = generateAccessToken(user);
user.sessionId = newSessionId;
await user.save();
// Auth API
const decodedToken: any = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!); // Replace with your JWT secret key
const sessionIdFromToken = decodedToken.sessionId;
if (user.sessionId !== sessionIdFromToken) {
return res.status(403).json({ message: 'Session invalidated. You are logged in elsewhere.' });
}
7. Ideal Time Session Expiration
Sessions expire after a period of inactivity. Implement a session expiration timer to automatically log out users who are inactive for a predefined time.
Why its important:
领英推荐
Implementation:
const sessionExpirationTime = 15 * 60 * 1000; // 15 minutes
const IDLE_TIMEOUT = 15 * 60 * 1000;
// User Schema
lastActivity: { type: Date, default: Date.now }, // Track last activity timestamp
// in auth middleware
const now = new Date();
const idleTime = now - new Date(user.lastActivity);
if (idleTime > IDLE_TIMEOUT) {
// Token is still valid, but session is idle for too long
return res.status(401).json({ message: 'Session expired due to inactivity' });
}
// Else Update last activity timestamp if within idle timeout
user.lastActivity = now;
await user.save();
8. Account Lockout Mechanism
Prevent brute force attacks by locking out accounts after several failed login attempts. Implement time-based lockout, where accounts are temporarily locked after repeated failures.
Why its important:
Implementation:
// Schema
failedLoginAttempts: { type: Number, default: 0 },
lockoutUntil: { type: Date, default: null }, // Track lockout time
const MAX_ATTEMPTS = 3;
const LOCK_TIME = 15 * 60 \_ 1000; // 15 minutes
// Example for MongoDB with Mongoose
const handleFailedLogin = async (email: string) => {
const user = await User.findOne({ email });
if (!user) return false;
if (user.lockoutUntil && user.lockoutUntil > new Date()) {
return true; // Account is still locked
}
user.failedLoginAttempts += 1;
if (user.failedLoginAttempts >= MAX_ATTEMPTS) {
user.lockoutUntil = new Date(Date.now() + LOCK_TIME); // Lock for 30 mins
}
await user.save();
return user.lockoutUntil && user.lockoutUntil > new Date(); // Return if account is locked
};
// Reset login attempts if the login is successful
9. Secure Password Reset Procedures
Users can reset their passwords through a secure process. Use a time-limited, one-time token (OTP) sent to the user's email for the reset process. Ensure the reset token expires after a short period.
Why its important:
Implementation:
// User Schema
resetPasswordToken: String,
resetPasswordExpires: Date,
/* Use crypto.randomBytes() to generate a secure hex instead of Math.random(), Math.random() hex are easier for attackers to guess or brute force. */
const generateResetToken = () => {
return crypto.randomBytes(20).toString('hex');
};
const token = generateResetToken();
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour from now
10. Password Expiration and Restricting Past 3 Passwords
Here's how this works:
// Schema
passwordHistory: [{ type: String }], // Array to store past 3 password hashes
passwordUpdatedAt: { type: Date, default: Date.now }, // When the password was last updated
// You can Check Expiration on Login or have a corn job to find and notify users via email.
11 . Implementing Strong Password Policies
Strong password policies help prevent unauthorized access and minimize the risk of brute-force attacks, dictionary attacks, and credential stuffing.
Implementation:
import validator from 'validator';
const password = 'User@1234';
if (
!validator.isStrongPassword(password, {
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
})
) {
throw new Error('Password does not meet complexity requirements.');
}
12. User-Specific Token Revocation
User-specific token revocation is the process of invalidating a single user’s tokens without affecting others.
It’s useful in cases like:
How it Works:
Tokens (e.g., JWTs) contain user data and a tokenVersion. The tokenVersion is stored in the database to track the validity of tokens for each user.
When a request is made, the server decodes the token and compares the tokenVersion in the token with the version stored in the database. If they match, the token is valid; if not, the token is invalidated.
To revoke all tokens for a user, simply increment the tokenVersion in the database. All tokens with the previous version are automatically invalidated.
Implementation:
// Schema: token_version INT DEFAULT 1
// JWT Creation
const createToken = (user) => {
const payload = { userId: user.id, tokenVersion: user.tokenVersion };
return jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
};
// Token Validation
if (user.tokenVersion !== decoded.tokenVersion) {
throw new Error('Invalid token: version mismatch');
}
// To revoke, increment tokenVersion
user.tokenVersion++;
13. Cross-Origin Resource Sharing (CORS) Configuration
Configure CORS to restrict which origins are allowed to access the API. Only trusted domains should be allowed to send requests to your server.
Why its important:
Implementation:
import cors from 'cors';
app.use(
cors({
origin: 'https://your-frontend-domain.com',
credentials: true,
})
);
I hope this guide helps you. Happy coding! ??