Two-Factor Authentication and Password Security Best Practices
You've built a web application, and users are signing up. But are their accounts truly secure? Weak passwords and missing two-factor authentication (2FA) are among the easiest ways for attackers to break in. In this guide, we'll walk through practical, evergreen best practices for password security and 2FA that you can implement today.
Why Password Security Still Matters
Despite years of warnings, passwords remain the primary authentication method for most web applications. Attackers know this and target passwords through phishing, credential stuffing, and brute force. A single breached password can lead to account takeover, data theft, and reputational damage.
Strong password policies and 2FA significantly raise the bar for attackers. Let's look at how to do both correctly.
Password Storage: Hashing Done Right
Never store passwords in plain text. Always hash them with a strong, slow algorithm designed for passwords. Fast hashes like SHA-256 are unsuitable because they enable rapid brute-force attacks.
Recommended algorithms:
- Argon2id – Winner of the Password Hashing Competition, resistant to GPU and side-channel attacks.
- bcrypt – Widely supported, battle-tested, and slow by design.
- scrypt – Memory-hard, good for preventing hardware-accelerated attacks.
Example using bcrypt in Node.js:
const bcrypt = require('bcrypt');
const saltRounds = 12;
async function hashPassword(password) {
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
Always use a unique salt per password (most libraries handle this automatically). Adjust the cost factor so hashing takes ~100-500ms on your server.
Password Policies That Actually Help
Forget complex rules like "must include a symbol and a number." Modern guidance (NIST SP 800-63B) recommends:
- Minimum length of 8 characters, but encourage passphrases (e.g., 4+ random words).
- Check against breached password lists (e.g., Have I Been Pwned API).
- No forced periodic expiration unless compromise is suspected.
- Allow all printable ASCII characters and spaces.
- Rate-limit login attempts to prevent brute force.
Implement a simple strength meter that gives feedback without blocking legitimate passwords.
Two-Factor Authentication (2FA) Methods
2FA adds a second factor: something you have (phone, hardware key) or something you are (biometrics). Here are common methods, ranked by security:
| Method | Security | Usability |
|---|---|---|
| Hardware security keys (WebAuthn/FIDO2) | High | Medium |
| Authenticator apps (TOTP) | High | High |
| Push notifications | Medium | High |
| SMS codes | Low | High |
SMS is vulnerable to SIM swapping and interception. Prefer TOTP or WebAuthn when possible.
Implementing TOTP-Based 2FA
TOTP (Time-based One-Time Password) is a great balance of security and usability. Here's a high-level workflow:
- Generate a secret key for the user (e.g., 32 random bytes, base32 encoded).
- Create an otpauth:// URI and display a QR code for the user to scan with an authenticator app.
- Verify the user can generate a valid code before enabling 2FA.
- Store the secret securely (encrypted at rest).
- Provide backup codes for account recovery.
During login, after verifying the password, prompt for the TOTP code. Verify it with a library like otplib or speakeasy.
const { authenticator } = require('otplib');
// Generate secret
const secret = authenticator.generateSecret();
// Verify token
const isValid = authenticator.verify({ token: userToken, secret });
Allow a small time window (e.g., ±1 interval) to account for clock drift.
WebAuthn: The Future of Authentication
WebAuthn (part of FIDO2) enables passwordless and second-factor authentication using hardware keys or platform authenticators (Touch ID, Windows Hello). It's phishing-resistant because the credential is bound to the origin.
Implementing WebAuthn requires:
- Server-side challenge generation and verification.
- Client-side JavaScript using the WebAuthn API.
- Storing public keys and credential IDs for each user.
Libraries like SimpleWebAuthn make it easier. While more complex than TOTP, WebAuthn offers the strongest protection.
Rate Limiting and Account Lockout
Even with 2FA, you must prevent brute-force attacks on passwords. Implement:
- Rate limiting per IP and per account (e.g., 5 attempts per minute).
- Exponential backoff after failed attempts.
- Account lockout after repeated failures, with a secure unlock process (e.g., email verification).
- CAPTCHA after a few failures.
Be careful not to lock out legitimate users permanently; use temporary lockouts and notify the user.
Secure Password Reset
Password reset flows are a common attack vector. Follow these rules:
- Use a single-use, time-limited token sent via email.
- Do not reveal whether an email exists in the system.
- Require the user to re-authenticate if they are already logged in.
- Invalidate all sessions after a password reset.
Educating Users Without Blaming Them
Security is a shared responsibility. Provide clear guidance:
- Use a password manager to generate and store unique passwords.
- Enable 2FA wherever possible.
- Be wary of phishing attempts.
In-app tips and a dedicated security page can help.
FAQ
Is SMS-based 2FA better than no 2FA?
Yes, SMS 2FA is better than nothing, but it's vulnerable to SIM swapping and interception. Prefer TOTP or WebAuthn for stronger security.
How often should I rotate password hashing algorithms?
You don't need to rotate unless a vulnerability is found. However, you can rehash passwords on successful login when you upgrade parameters (e.g., increasing bcrypt cost).
Can I use 2FA with OAuth or social login?
Yes, but 2FA is typically managed by the identity provider. If you use social login, ensure your own account recovery process is secure.
Ready to enhance your application's security? Start by implementing strong password hashing and TOTP-based 2FA today. For more developer tools, check out our JSON Formatter to debug API responses quickly.