Securing Passwords and Authentication Best Practices
Securing Passwords and Authentication Best Practices
Last updated: 3/7/2025
Securing Passwords and Authentication Best Practices
π Introduction
In the previous lessons, we implemented user authentication and authorization. However, authentication systems are common targets for attacks.
To protect user accounts, we must secure passwords and authentication mechanisms.
In this lesson, you'll learn:
β
Best practices for password security.
β
How to prevent brute force attacks.
β
How to protect JWT tokens from being stolen.
β
Other security measures like rate limiting and multi-factor authentication (MFA).
π 1. Best Practices for Password Security
β 1οΈβ£ Always Hash Passwords Before Storing
Storing raw passwords in the database is extremely dangerous. If a database leak occurs, all passwords would be exposed.
πΉ Solution: Use bcrypt for hashing passwords.
πΉ Why? bcrypt applies salt (random data) to the hash, preventing rainbow table attacks.
β Good Example (Hashing Passwords in Node.js)
const bcrypt = require("bcryptjs"); const hashPassword = async (password) => { const salt = await bcrypt.genSalt(10); return await bcrypt.hash(password, salt); };
β Bad Example (Storing Plain Text Passwords)
const user = new User({ email, password: req.body.password }); // β No Hashing
β 2οΈβ£ Enforce Strong Password Policies
Weak passwords make brute-force attacks easier.
β Good Password Policy
- Minimum 8 characters
- At least one uppercase letter
- At least one number
- At least one special character
πΉ Solution: Use a validation library like Zod or Joi:
const z = require("zod"); const passwordSchema = z.string() .min(8, "Password must be at least 8 characters") .regex(/[A-Z]/, "Must include an uppercase letter") .regex(/[0-9]/, "Must include a number") .regex(/[^A-Za-z0-9]/, "Must include a special character");
β 3οΈβ£ Implement Rate Limiting to Prevent Brute Force Attacks
Attackers may attempt multiple login requests to guess passwords.
πΉ Solution: Use Express Rate Limit Middleware
const rateLimit = require("express-rate-limit"); const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // Limit to 5 login attempts per IP message: { error: "Too many login attempts. Please try again later." } }); app.use("/auth/login", loginLimiter);
β Blocks excessive login attempts and prevents brute force attacks.
π 2. Protecting JWT Tokens
β 4οΈβ£ Use Secure HTTP-Only Cookies Instead of Local Storage
Many APIs store JWT tokens in localStorage, but this is unsafe due to XSS attacks.
πΉ Solution: Store JWTs in HTTP-only cookies (safer)
res.cookie("token", token, { httpOnly: true, secure: true, // Only send over HTTPS sameSite: "Strict" });
β
Why?
β Prevents cross-site scripting (XSS) attacks.
β Ensures the token is only accessible by the server.
β 5οΈβ£ Implement Token Expiry and Refresh Tokens
πΉ Why? If a JWT token is stolen, it should not remain valid forever.
πΉ Solution:
- Set an expiry time for access tokens (
expiresIn: "1h"
) - Use refresh tokens to issue new access tokens without requiring login.
Example of issuing a short-lived access token and a refresh token:
const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "15m" }); const refreshToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" });
β Short-lived tokens reduce risk if stolen.
β Refresh tokens allow re-authentication without needing passwords.
π 3. Implementing Multi-Factor Authentication (MFA)
Even with strong passwords, accounts can still be compromised. MFA adds an extra layer of security.
β 6οΈβ£ Enable Two-Factor Authentication (2FA)
πΉ Users must verify their identity using:
β A password (something they know)
β A one-time code from Google Authenticator or SMS (something they have)
πΉ Solution: Use an MFA provider like Authy, Google Authenticator, or Twilio Verify.
Example: 2FA Flow
- User enters their email & password.
- Server sends a one-time code (OTP) to their phone.
- User enters the OTP to complete login.
Example: Generating a 2FA Code
const speakeasy = require("speakeasy"); // Generate a secret for the user const secret = speakeasy.generateSecret({ length: 20 }); // Generate a one-time token const token = speakeasy.totp({ secret: secret.base32, encoding: "base32" }); console.log("2FA Code:", token);
β
Why?
β Even if a password is stolen, hackers cannot log in without the OTP.
π 4. Additional Security Best Practices
β Use HTTPS β Prevents man-in-the-middle attacks.
β Log failed login attempts β Detects potential threats.
β Monitor API activity β Identify suspicious behavior.
π― Summary
β
Passwords must always be hashed before storing them.
β
Implement rate limiting to prevent brute force attacks.
β
Use HTTP-only cookies for storing JWT tokens instead of localStorage.
β
Implement MFA (Two-Factor Authentication) for extra security.
β
Enforce token expiration & refresh tokens to improve security.
β Next Lesson: OAuth and Third-Party Authentication (Google, GitHub, etc.)
In the next lesson, weβll implement OAuth authentication using Google and GitHub logins! π