---
# === IDENTITY ===
id: software/security/auth-security-checklist/2026
canonical_question: "What is the authentication security checklist?"
aliases:
  - "authentication best practices checklist"
  - "secure login implementation guide"
  - "password security and MFA checklist"
  - "auth hardening checklist"
  - "OWASP authentication security"
  - "NIST 800-63 authentication requirements"
  - "how to secure user authentication"
  - "login security best practices 2026"
entity_type: software_reference
domain: software > security > Authentication Security Checklist
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.94
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "NIST SP 800-63-4 finalized August 2025 — replaces 800-63-3, mandates phishing-resistant MFA at AAL2+; Argon2id now OWASP first-choice over bcrypt"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "NEVER store passwords in plaintext, reversible encryption, or unsalted hashes -- always use Argon2id, bcrypt, or scrypt"
  - "NEVER expose whether a username exists via login error messages, registration, or password reset -- use generic responses"
  - "All authentication endpoints MUST be served over TLS 1.2+ -- no exceptions"
  - "MFA MUST be offered for all user accounts and REQUIRED for admin/privileged accounts"
  - "Session tokens MUST be cryptographically random (>=128 bits entropy), HttpOnly, Secure, SameSite=Lax"
  - "Rate limiting MUST be applied per-account (not just per-IP) to prevent credential stuffing"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need OAuth 2.0 / OpenID Connect implementation details"
    use_instead: "software/security/oauth2-oidc-guide/2026"
  - condition: "Need JWT token security specifically"
    use_instead: "software/patterns/jwt-implementation/2026"
  - condition: "Need API key or machine-to-machine authentication"
    use_instead: "software/security/api-key-management/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "auth_type"
    question: "What type of authentication does your application use?"
    type: choice
    options: ["Username/password", "OAuth/OIDC", "Passkeys/FIDO2", "API keys", "Mixed"]
  - key: "framework"
    question: "What backend framework are you using?"
    type: choice
    options: ["Express/Node.js", "Django/Python", "Spring Boot/Java", "Rails", "Go", "ASP.NET", "Other"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/security/auth-security-checklist/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/security/xss-prevention/2026"
      label: "XSS Prevention Guide"
    - id: "software/patterns/session-management/2026"
      label: "Session Management Patterns"
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Implementation Guide"
  solves: []
  alternative_to: []
  often_confused_with:
    - id: "software/security/oauth2-oidc-guide/2026"
      label: "OAuth 2.0 / OIDC Guide"
    - id: "software/security/csrf-prevention/2026"
      label: "CSRF Prevention"

# === SOURCES (7 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Authentication Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src2
    title: "Password Storage Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src3
    title: "NIST SP 800-63B-4: Digital Identity Guidelines -- Authentication and Authenticator Management"
    author: NIST
    url: https://csrc.nist.gov/pubs/sp/800/63/b/4/final
    type: official_docs
    published: 2025-08-01
    reliability: authoritative
  - id: src4
    title: "Session Management Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src5
    title: "Multifactor Authentication Cheat Sheet"
    author: OWASP Foundation
    url: https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html
    type: community_resource
    published: 2024-06-15
    reliability: authoritative
  - id: src6
    title: "Blocking Brute Force Attacks"
    author: OWASP Foundation
    url: https://owasp.org/www-community/controls/Blocking_Brute_Force_Attacks
    type: community_resource
    published: 2024-01-01
    reliability: high
  - id: src7
    title: "Phishing-Resistant MFA in 2025: Buyer's Guide to NIST SP 800-63-4"
    author: WWPass
    url: https://www.wwpass.com/blog/phishing-resistant-mfa-in-2025-buyer-s-guide-to-nist-sp-800-63-4-omb-m-22-09/
    type: technical_blog
    published: 2025-03-15
    reliability: moderate_high
---

# Authentication Security Checklist

## TL;DR

- **Bottom line**: Secure authentication requires layered defenses -- Argon2id password hashing, phishing-resistant MFA, secure session management, brute force protection, and generic error messages that prevent user enumeration.
- **Key tool/command**: `argon2.hash(password, {type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 1})` for password hashing; FIDO2/Passkeys for phishing-resistant MFA.
- **Watch out for**: Storing passwords with MD5/SHA-256 (no key stretching), exposing username existence in error messages, and missing rate limiting on login endpoints.
- **Works with**: All modern web frameworks (Express, Django, Spring Boot, Rails, ASP.NET). NIST SP 800-63-4 compliant. FIDO2/Passkeys supported in Chrome 67+, Firefox 60+, Safari 14+, Edge 79+.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- NEVER store passwords in plaintext, reversible encryption, or unsalted hashes -- always use Argon2id, bcrypt, or scrypt
- NEVER expose whether a username exists via login error messages, registration, or password reset -- use generic responses
- All authentication endpoints MUST be served over TLS 1.2+ -- no exceptions
- MFA MUST be offered for all user accounts and REQUIRED for admin/privileged accounts
- Session tokens MUST be cryptographically random (>=128 bits entropy), HttpOnly, Secure, SameSite=Lax
- Rate limiting MUST be applied per-account (not just per-IP) to prevent credential stuffing

## Quick Reference

| # | Vulnerability | Risk | Vulnerable Code | Secure Code |
|---|---|---|---|---|
| 1 | Plaintext/weak password storage | Critical | `INSERT INTO users(pass) VALUES('${password}')` | `argon2.hash(password, {type: argon2id, memoryCost: 65536})` |
| 2 | No MFA on privileged accounts | Critical | Password-only admin login | TOTP/FIDO2 required for all admin accounts |
| 3 | Username enumeration via login | High | `"Invalid password"` / `"User not found"` | `"Login failed; invalid user ID or password"` |
| 4 | Username enumeration via registration | High | `"This email is already registered"` | `"A link to activate your account has been emailed"` |
| 5 | No rate limiting on login | High | Unlimited login attempts | 5 attempts per account per 15min, then exponential backoff |
| 6 | Session fixation | High | Reusing session ID after login | Regenerate session ID on authentication state change |
| 7 | Insecure session cookies | High | `Set-Cookie: sid=abc123` | `Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax` |
| 8 | Password length too short | Medium | `minLength: 6` | `minLength: 8` (with MFA) or `minLength: 15` (without MFA) |
| 9 | Bcrypt with raw long passwords | Medium | `bcrypt.hash(longPassword)` (silently truncates at 72 bytes) | `bcrypt(base64(hmac-sha384(password, pepper)))` |
| 10 | No session timeout | Medium | Sessions never expire | Idle timeout: 15-30min; absolute timeout: 8-24hr |
| 11 | Missing breached password check | Medium | Accept any password meeting length rule | Check against HaveIBeenPwned Pwned Passwords API |
| 12 | Credential recovery reveals info | Medium | `"No account with that email"` | `"If that email is in our database, we will send a reset link"` |

## Decision Tree

```
START: What authentication concern are you addressing?
├── Password storage?
│   ├── New project → Use Argon2id (memoryCost: 65536, timeCost: 3, parallelism: 1)
│   ├── Existing bcrypt → Keep bcrypt (cost factor 10+), plan migration to Argon2id
│   └── Legacy MD5/SHA → Implement layered hashing: bcrypt(md5(password)), re-hash on next login
├── Multi-factor authentication?
│   ├── Consumer app → Offer TOTP + Passkeys, require for account recovery changes
│   ├── Enterprise/admin → Require FIDO2/Passkeys (phishing-resistant, NIST AAL2+)
│   └── FIPS-140 required → Use NIST-approved authenticators at AAL2/AAL3
├── Brute force protection?
│   ├── Public-facing login → Rate limit per-account: 5 attempts/15min + CAPTCHA after 3
│   ├── API authentication → Rate limit per-key + per-IP, return 429 with Retry-After
│   └── Admin login → Lock after 3 failures + email alert + MFA mandatory
├── Session management?
│   ├── Web app → HttpOnly + Secure + SameSite=Lax cookies, regenerate on login
│   ├── SPA/API → Short-lived access token (15min) + HttpOnly refresh token (7d)
│   └── Mobile app → Secure storage (Keychain/Keystore) + biometric unlock
└── DEFAULT → Apply all checklist items from Quick Reference table above
```

## Step-by-Step Guide

### 1. Configure password hashing with Argon2id

Argon2id is the OWASP first-choice algorithm. It resists both GPU attacks (memory-hard) and side-channel attacks (hybrid mode). OWASP recommends a minimum of 19 MiB memory and 2 iterations, but 64 MiB with 3 iterations provides a better security margin. [src2]

```javascript
const argon2 = require('argon2');  // ^0.41.0

async function hashPassword(password) {
  return argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536,  // 64 MiB
    timeCost: 3,        // 3 iterations
    parallelism: 1,     // 1 thread (scale with server cores)
    hashLength: 32      // 256-bit output
  });
}

async function verifyPassword(hash, password) {
  return argon2.verify(hash, password);
  // Returns true/false -- constant-time comparison built in
}
```

**Verify**: `node -e "const a=require('argon2'); a.hash('test',{type:a.argon2id,memoryCost:65536,timeCost:3}).then(h=>console.log(h))"` -- should output `$argon2id$v=19$m=65536,t=3,p=1$...`

### 2. Implement generic error messages to prevent enumeration

Every authentication response must be identical regardless of whether the account exists. This prevents attackers from enumerating valid usernames. [src1]

```javascript
// Login endpoint -- SAME response for invalid user AND invalid password
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await db.findByEmail(email);

  // Perform hash comparison even if user doesn't exist (constant-time defense)
  const dummyHash = '$argon2id$v=19$m=65536,t=3,p=1$dW5rbm93bg$dW5rbm93bg';
  const isValid = user
    ? await argon2.verify(user.passwordHash, password)
    : await argon2.verify(dummyHash, password);  // Always compare

  if (!user || !isValid) {
    return res.status(401).json({
      error: 'Login failed; invalid user ID or password'
    });
  }
  // ... proceed to MFA check, session creation
});
```

**Verify**: Attempt login with non-existent email and wrong password for existing email -- both must return the identical error message and similar response time.

### 3. Add rate limiting and brute force protection

Apply rate limiting per-account (not just per-IP) because credential stuffing attacks use distributed IPs. Use exponential backoff starting at 1 second, doubling after each failure. [src6]

```javascript
const rateLimit = require('express-rate-limit');  // ^7.0.0

// Global rate limit on auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15-minute window
  max: 20,                    // 20 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many login attempts. Try again later.' }
});

app.use('/auth/', authLimiter);

// Per-account lockout (store in Redis or DB)
async function checkAccountLockout(email) {
  const attempts = await redis.get(`login:fail:${email}`);
  if (parseInt(attempts) >= 5) {
    const lockoutTTL = await redis.ttl(`login:fail:${email}`);
    throw new Error(`Account temporarily locked. Try again in ${lockoutTTL}s`);
  }
}

async function recordFailedAttempt(email) {
  const key = `login:fail:${email}`;
  const attempts = await redis.incr(key);
  // Exponential backoff: 2^attempts seconds, max 15 minutes
  const lockoutSeconds = Math.min(Math.pow(2, attempts), 900);
  await redis.expire(key, lockoutSeconds);
}
```

**Verify**: Attempt 6 rapid logins with wrong password -- 6th attempt should return 429 or lockout message.

### 4. Enforce MFA with TOTP and Passkeys

Offer TOTP as baseline MFA and FIDO2/Passkeys as the phishing-resistant option. NIST SP 800-63-4 requires phishing-resistant MFA at AAL2+. [src5] [src3]

```javascript
const speakeasy = require('speakeasy');  // ^2.0.0

// Generate TOTP secret for user enrollment
function generateTOTPSecret(userEmail) {
  return speakeasy.generateSecret({
    name: `MyApp:${userEmail}`,
    issuer: 'MyApp',
    length: 32  // 256-bit secret
  });
}

// Verify TOTP token with +-1 window tolerance
function verifyTOTP(secret, token) {
  return speakeasy.totp.verify({
    secret: secret,
    encoding: 'base32',
    token: token,
    window: 1  // Allow 1 step before/after (30s tolerance)
  });
}
```

**Verify**: Generate a secret, scan the QR code with Google Authenticator, enter the 6-digit code -- should return `true`.

### 5. Configure secure session cookies

Session cookies must use HttpOnly (prevents XSS theft), Secure (HTTPS only), and SameSite=Lax (CSRF protection). Regenerate the session ID after every authentication state change. [src4]

```javascript
const session = require('express-session');  // ^1.18.0
const crypto = require('crypto');

app.use(session({
  secret: process.env.SESSION_SECRET,    // 256-bit random secret
  name: '__Host-sid',                     // __Host- prefix enforces Secure + path=/
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,       // Blocks document.cookie access
    secure: true,         // HTTPS only
    sameSite: 'lax',      // CSRF protection
    maxAge: 30 * 60 * 1000,  // 30-min idle timeout
    path: '/'
  }
}));

// Regenerate session ID after login
app.post('/login', async (req, res) => {
  // ... authenticate user ...
  req.session.regenerate((err) => {
    if (err) return res.status(500).json({ error: 'Session error' });
    req.session.userId = user.id;
    req.session.loginTime = Date.now();
    res.json({ success: true });
  });
});
```

**Verify**: `curl -v https://your-app.com/login` -- response headers should include `Set-Cookie: __Host-sid=...; HttpOnly; Secure; SameSite=Lax`.

### 6. Check passwords against breached databases

Block known-compromised passwords by checking the HaveIBeenPwned Pwned Passwords API using k-anonymity (only sends first 5 chars of SHA-1 hash prefix). [src1]

```javascript
const crypto = require('crypto');

async function isPasswordBreached(password) {
  const sha1 = crypto.createHash('sha1')
    .update(password).digest('hex').toUpperCase();
  const prefix = sha1.slice(0, 5);
  const suffix = sha1.slice(5);

  const res = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`);
  const body = await res.text();
  return body.split('\n').some(line =>
    line.startsWith(suffix)
  );
}

// Usage in registration
const breached = await isPasswordBreached(password);
if (breached) {
  return res.status(400).json({
    error: 'This password has appeared in a data breach. Choose a different password.'
  });
}
```

**Verify**: `isPasswordBreached('password123')` should return `true`; a strong random password should return `false`.

## Code Examples

### Python/Django: Argon2id Password Hashing

```python
# settings.py -- set Argon2id as primary hasher
# pip install django[argon2]  # or: pip install argon2-cffi
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.Argon2PasswordHasher',  # Primary
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',  # Fallback
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',  # Legacy
]

# views.py -- secure login with generic errors
from django.contrib.auth import authenticate, login
from django.http import JsonResponse

def login_view(request):
    email = request.POST.get('email')
    password = request.POST.get('password')
    user = authenticate(request, username=email, password=password)
    if user is not None:
        login(request)  # Regenerates session ID automatically
        return JsonResponse({'success': True})
    # Generic error -- same message whether user exists or not
    return JsonResponse(
        {'error': 'Login failed; invalid user ID or password'},
        status=401
    )
```

### Node.js/Express: Complete Auth Middleware

```javascript
const argon2 = require('argon2');       // ^0.41.0
const rateLimit = require('express-rate-limit');  // ^7.0.0
const helmet = require('helmet');       // ^7.1.0

// 1. Helmet security headers
app.use(helmet());

// 2. Rate limit auth routes (per IP)
app.use('/auth', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 20,
  standardHeaders: true
}));

// 3. Hash + verify with Argon2id
const hashOpts = {
  type: argon2.argon2id,
  memoryCost: 65536,
  timeCost: 3,
  parallelism: 1
};

app.post('/auth/register', async (req, res) => {
  const hash = await argon2.hash(req.body.password, hashOpts);
  await db.createUser(req.body.email, hash);
  res.json({ message: 'A link to activate your account has been emailed' });
});
```

### Java/Spring Boot: BCrypt Configuration

```java
// SecurityConfig.java -- Spring Security 6.x
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
public PasswordEncoder passwordEncoder() {
    // Argon2id: saltLength=16, hashLength=32, parallelism=1,
    //           memory=65536 KiB, iterations=3
    return new Argon2PasswordEncoder(16, 32, 1, 65536, 3);
}

// For legacy bcrypt migration:
// @Bean
// public PasswordEncoder passwordEncoder() {
//     return new BCryptPasswordEncoder(12);  // cost factor 12
// }
```

## Anti-Patterns

### Wrong: MD5/SHA-256 for password storage

```python
# BAD -- fast hashes enable billions of guesses per second
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
# GPU can compute ~10 billion SHA-256 hashes/second
```

### Correct: Argon2id with proper parameters

```python
# GOOD -- memory-hard hash, ~300ms per attempt on server
from argon2 import PasswordHasher
ph = PasswordHasher(
    time_cost=3,       # 3 iterations
    memory_cost=65536,  # 64 MiB
    parallelism=1
)
password_hash = ph.hash(password)
# Attacker needs 64 MiB per guess -- massively slows GPU attacks
```

### Wrong: Revealing username existence

```javascript
// BAD -- different messages leak whether account exists
if (!user) return res.json({ error: 'User not found' });
if (!validPassword) return res.json({ error: 'Wrong password' });
```

### Correct: Generic error for all auth failures

```javascript
// GOOD -- identical response regardless of failure reason
if (!user || !validPassword) {
  return res.status(401).json({
    error: 'Login failed; invalid user ID or password'
  });
}
```

### Wrong: Session cookie without security flags

```javascript
// BAD -- cookie accessible to XSS, sent over HTTP, vulnerable to CSRF
res.cookie('session', token);
// Equivalent to: Set-Cookie: session=abc123 (no flags)
```

### Correct: Fully secured session cookie

```javascript
// GOOD -- defense-in-depth cookie configuration
res.cookie('__Host-session', token, {
  httpOnly: true,      // No document.cookie access
  secure: true,        // HTTPS only
  sameSite: 'lax',     // CSRF protection
  maxAge: 1800000,     // 30-min timeout
  path: '/'
});
```

### Wrong: Rate limiting by IP only

```javascript
// BAD -- credential stuffing uses thousands of IPs
const limiter = rateLimit({ windowMs: 60000, max: 100 });
// An attacker with a botnet bypasses this trivially
```

### Correct: Per-account rate limiting

```javascript
// GOOD -- track failures per account, not just per IP
const key = `login:fail:${email}`;
const failures = await redis.incr(key);
await redis.expire(key, Math.min(Math.pow(2, failures), 900));
if (failures >= 5) throw new Error('Account temporarily locked');
```

## Common Pitfalls

- **Bcrypt 72-byte truncation**: Bcrypt silently truncates passwords at 72 bytes. Long passphrases may produce identical hashes. Fix: Pre-hash with `bcrypt(base64(hmac-sha384(password, pepper)))` or migrate to Argon2id. [src2]
- **TOTP secret stored unencrypted**: TOTP shared secrets in plaintext DB are equivalent to storing passwords in plaintext -- an attacker with DB access can generate valid codes. Fix: Encrypt TOTP secrets at rest with a key stored in a KMS or HSM. [src5]
- **Timing attack on user lookup**: Skipping hash comparison when user doesn't exist creates a timing difference that reveals account existence. Fix: Always compare against a dummy hash even for non-existent users. [src1]
- **Missing session regeneration**: Not regenerating the session ID after login enables session fixation attacks -- attacker sets a known session ID before victim authenticates. Fix: Call `req.session.regenerate()` (Express) or equivalent after every authentication state change. [src4]
- **Password reset token reuse**: Password reset tokens that remain valid after use allow replayed resets. Fix: Invalidate token immediately on use, set 15-60 minute expiry, use cryptographically random tokens (>=128 bits). [src1]
- **Account lockout as DoS vector**: Permanent account lockout after N failures lets attackers lock out legitimate users. Fix: Use temporary lockout with exponential backoff (1s, 2s, 4s, ..., max 15min) and always allow password recovery. [src6]

## Diagnostic Commands

```bash
# Check if Argon2 is available in your Node.js environment
node -e "try{require('argon2');console.log('argon2: OK')}catch(e){console.log('argon2: MISSING')}"

# Verify password hash format is Argon2id
echo '$argon2id$v=19$m=65536,t=3,p=1$...' | grep -o 'argon2id' && echo "Algorithm: OK"

# Check bcrypt cost factor (should be >= 10)
echo '$2b$12$...' | cut -d'$' -f3  # Should output 10, 12, or higher

# Test session cookie security headers
curl -sI https://your-app.com/login -X POST \
  -d 'email=test@test.com&password=test' | grep -i 'set-cookie'
# Should contain: HttpOnly; Secure; SameSite=Lax

# Scan for hardcoded passwords in codebase
grep -rn 'password\s*=\s*["\x27]' --include="*.js" --include="*.py" --include="*.java" .

# Check TLS configuration
nmap --script ssl-enum-ciphers -p 443 your-app.com

# Verify HaveIBeenPwned API connectivity
curl -s https://api.pwnedpasswords.com/range/5BAA6 | head -5

# Check for missing rate limiting on auth endpoints
ab -n 100 -c 10 https://your-app.com/auth/login
# If all 100 succeed, rate limiting is missing
```

## Version History & Compatibility

| Standard/Tool | Version | Status | Key Feature |
|---|---|---|---|
| NIST SP 800-63-4 | Rev 4 | Current (Aug 2025) | Risk-based DIRM, phishing-resistant MFA at AAL2+ |
| NIST SP 800-63-3 | Rev 3 | Withdrawn | AAL1/2/3 framework, memorized secrets |
| OWASP ASVS | v4.0.3 | Current | Comprehensive auth verification standard |
| Argon2 | RFC 9106 | Current | IRTF-published, OWASP first-choice |
| bcrypt | — | Maintained | 72-byte limit, cost factor 10+ |
| FIDO2/WebAuthn | Level 2 | W3C Rec | Phishing-resistant, passkey support |
| Passkeys | — | GA | Apple (iOS 16+), Google (Android 9+), Windows (10+) |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building any web app with user accounts | Machine-to-machine API calls only | API key management + mTLS |
| Need NIST/SOC2/PCI compliance for authentication | Building a static site with no user accounts | No authentication needed |
| Adding MFA to existing password-based system | Already using a managed auth provider (Auth0, Clerk, Cognito) | Provider's built-in MFA/security features |
| Storing user credentials in your own database | Using OAuth/OIDC exclusively (no password storage) | OAuth2/OIDC implementation guide |
| Internal admin tools need hardening | Single-user personal project (non-public) | Basic authentication may suffice |

## Important Caveats

- Argon2id memory parameters must be tuned to your server's available RAM -- using 64 MiB per hash on a 512 MiB server will cause OOM under concurrent requests
- Bcrypt's 72-byte limit is applied to the raw byte representation of the password, not the character count -- UTF-8 multibyte characters reduce the effective limit
- TOTP has a built-in vulnerability window of +-30 seconds (one step) and TOTP codes can be phished -- for high-security use cases, prefer FIDO2/Passkeys which are phishing-resistant
- Rate limiting per-account can be weaponized as a denial-of-service attack against specific users -- always allow password recovery to bypass lockout
- "Remember me" functionality must use a separate persistent token (not the session cookie) with its own rotation and revocation mechanism
- Password strength meters (zxcvbn) run client-side for UX but server-side validation of minimum length and breached status is still required

## Related Units
<!-- Generated from related_kos frontmatter -->

- [XSS Prevention Guide](/software/security/xss-prevention/2026)
- [Session Management Patterns](/software/patterns/session-management/2026)
- [JWT Implementation Guide](/software/patterns/jwt-implementation/2026)
- [OAuth 2.0 / OIDC Guide](/software/security/oauth2-oidc-guide/2026)
- [CSRF Prevention](/software/security/csrf-prevention/2026)
