Authentication Security Best Practices

Security best practices for authentication: password security, token storage, CSRF protection, MFA, and common vulnerabilities.

Password Security

Best Practices

 1✅ DO:
 2- Use bcrypt, scrypt, or Argon2 for hashing
 3- Minimum 12 characters
 4- Require complexity (upper, lower, number, symbol)
 5- Implement rate limiting on login
 6- Use HTTPS only
 7- Implement account lockout after failed attempts
 8- Check against breach databases (Have I Been Pwned)
 9
10❌ DON'T:
11- Store passwords in plain text
12- Use MD5 or SHA1 for passwords
13- Email passwords to users
14- Allow common passwords (password123, qwerty)
15- Use reversible encryption

Password Hashing Example

 1import bcrypt
 2
 3# Hashing a password
 4password = "user_password"
 5salt = bcrypt.gensalt(rounds=12)  # Cost factor: 12
 6hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
 7
 8# Verifying a password
 9def verify_password(plain_password, hashed_password):
10    return bcrypt.checkpw(
11        plain_password.encode('utf-8'),
12        hashed_password
13    )
1// Node.js with bcrypt
2const bcrypt = require('bcrypt');
3
4// Hash password
5const saltRounds = 12;
6const hash = await bcrypt.hash(password, saltRounds);
7
8// Verify password
9const match = await bcrypt.compare(password, hash);

Password Policy Example

 1import re
 2
 3def validate_password(password):
 4    if len(password) < 12:
 5        return False, "Password must be at least 12 characters"
 6    
 7    if not re.search(r'[A-Z]', password):
 8        return False, "Password must contain uppercase letter"
 9    
10    if not re.search(r'[a-z]', password):
11        return False, "Password must contain lowercase letter"
12    
13    if not re.search(r'\d', password):
14        return False, "Password must contain number"
15    
16    if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
17        return False, "Password must contain special character"
18    
19    # Check against common passwords
20    if password.lower() in COMMON_PASSWORDS:
21        return False, "Password is too common"
22    
23    return True, "Password is valid"

Token Storage

Storage Options Comparison

LocationXSS VulnerableCSRF VulnerableBest For
LocalStorage✅ Yes❌ No❌ Avoid for auth tokens
SessionStorage✅ Yes❌ NoTemporary data only
Cookie (HttpOnly)❌ No✅ YesSession tokens (with CSRF protection)
Cookie (HttpOnly + Secure + SameSite)❌ No❌ No✅ Best for web apps
Memory only❌ No❌ No✅ SPAs (lost on refresh)
1Set-Cookie: token=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=3600; Path=/

Flags Explained:

  • HttpOnly: Prevents JavaScript access (XSS protection)
  • Secure: Only sent over HTTPS
  • SameSite=Strict: Prevents CSRF attacks
  • Max-Age: Token lifetime in seconds
  • Path=/: Cookie scope

Implementation Examples

1// Express.js (Node.js)
2res.cookie('token', jwtToken, {
3    httpOnly: true,
4    secure: process.env.NODE_ENV === 'production',
5    sameSite: 'strict',
6    maxAge: 3600000 // 1 hour in milliseconds
7});
1# Flask (Python)
2response.set_cookie(
3    'token',
4    jwt_token,
5    httponly=True,
6    secure=True,
7    samesite='Strict',
8    max_age=3600
9)
 1// Go
 2http.SetCookie(w, &http.Cookie{
 3    Name:     "token",
 4    Value:    jwtToken,
 5    HttpOnly: true,
 6    Secure:   true,
 7    SameSite: http.SameSiteStrictMode,
 8    MaxAge:   3600,
 9    Path:     "/",
10})

CSRF Protection

What is CSRF?

Cross-Site Request Forgery: Attacker tricks user into making unwanted requests to a site where they're authenticated.

Attack Example

1<!-- Malicious site -->
2<img src="https://bank.com/transfer?to=attacker&amount=1000">
3<!-- Browser automatically sends cookies! -->

Protection Methods

1. CSRF Tokens (Synchronizer Token Pattern)

sequenceDiagram
    participant User
    participant Browser
    participant Server
    
    User->>Browser: Load form
    Browser->>Server: GET /form
    Server->>Server: Generate CSRF token
    Server->>Browser: Form with hidden token
    
    User->>Browser: Submit form
    Browser->>Server: POST /submit (data + CSRF token)
    Server->>Server: Validate CSRF token
    alt Token valid
        Server->>Browser: Success
    else Token invalid
        Server->>Browser: 403 Forbidden
    end

Implementation:

 1# Flask with Flask-WTF
 2from flask_wtf.csrf import CSRFProtect
 3
 4app = Flask(__name__)
 5app.config['SECRET_KEY'] = 'your-secret-key'
 6csrf = CSRFProtect(app)
 7
 8# In template
 9<form method="POST">
10    {{ csrf_token() }}
11    <!-- form fields -->
12</form>
 1// Express.js with csurf
 2const csrf = require('csurf');
 3const csrfProtection = csrf({ cookie: true });
 4
 5app.get('/form', csrfProtection, (req, res) => {
 6    res.render('form', { csrfToken: req.csrfToken() });
 7});
 8
 9app.post('/submit', csrfProtection, (req, res) => {
10    // CSRF token automatically validated
11});

2. SameSite Cookies

1Set-Cookie: session=abc; SameSite=Strict
  • Strict: Cookie never sent on cross-site requests
  • Lax: Cookie sent on top-level navigation (GET only)
  • None: Cookie sent on all requests (requires Secure flag)
11. Server sets CSRF token in cookie
22. Client reads cookie, sends in custom header
33. Server validates cookie matches header
1// Client-side
2const csrfToken = getCookie('csrf_token');
3fetch('/api/data', {
4    method: 'POST',
5    headers: {
6        'X-CSRF-Token': csrfToken
7    },
8    body: JSON.stringify(data)
9});

4. Custom Headers (for APIs)

1// CORS prevents cross-origin custom headers
2fetch('/api/data', {
3    method: 'POST',
4    headers: {
5        'X-Requested-With': 'XMLHttpRequest'
6    }
7});

Multi-Factor Authentication (MFA)

Authentication Factors

  1. Something you know: Password, PIN
  2. Something you have: Phone, hardware token, authenticator app
  3. Something you are: Fingerprint, face recognition

TOTP (Time-based One-Time Password)

1Shared Secret → HMAC-SHA1(Secret, Time) → 6-digit code

Setup Flow

sequenceDiagram
    participant User
    participant App
    participant Server
    participant Authenticator
    
    User->>App: Enable MFA
    App->>Server: Request MFA setup
    Server->>Server: Generate secret key
    Server->>App: Secret + QR code
    App->>User: Display QR code
    User->>Authenticator: Scan QR code
    Authenticator->>Authenticator: Store secret
    Authenticator->>User: Show 6-digit code
    User->>App: Enter code
    App->>Server: Verify code
    Server->>Server: Validate TOTP
    Server->>App: MFA enabled

Login Flow with MFA

sequenceDiagram
    participant User
    participant App
    participant Server
    participant Authenticator
    
    User->>App: Enter username/password
    App->>Server: POST /login
    Server->>Server: Validate credentials
    Server->>App: Request MFA code
    App->>User: Prompt for MFA code
    User->>Authenticator: Open app
    Authenticator->>User: Show current code
    User->>App: Enter 6-digit code
    App->>Server: Submit code
    Server->>Server: Validate TOTP
    alt Code valid
        Server->>App: Access token
        App->>User: Login successful
    else Code invalid
        Server->>App: Invalid code
        App->>User: Try again
    end

Implementation Example

 1import pyotp
 2import qrcode
 3
 4# Generate secret
 5secret = pyotp.random_base32()
 6
 7# Generate QR code
 8totp_uri = pyotp.totp.TOTP(secret).provisioning_uri(
 9    name='user@example.com',
10    issuer_name='MyApp'
11)
12qrcode.make(totp_uri).save('qr_code.png')
13
14# Verify code
15def verify_totp(secret, code):
16    totp = pyotp.TOTP(secret)
17    return totp.verify(code, valid_window=1)  # Allow 30s window
 1// Node.js with speakeasy
 2const speakeasy = require('speakeasy');
 3const QRCode = require('qrcode');
 4
 5// Generate secret
 6const secret = speakeasy.generateSecret({
 7    name: 'MyApp (user@example.com)'
 8});
 9
10// Generate QR code
11QRCode.toDataURL(secret.otpauth_url, (err, dataUrl) => {
12    // Display dataUrl as image
13});
14
15// Verify code
16const verified = speakeasy.totp.verify({
17    secret: secret.base32,
18    encoding: 'base32',
19    token: userCode,
20    window: 1
21});

Backup Codes

 1import secrets
 2
 3def generate_backup_codes(count=10):
 4    codes = []
 5    for _ in range(count):
 6        code = '-'.join([
 7            secrets.token_hex(2).upper()
 8            for _ in range(3)
 9        ])
10        codes.append(code)
11    return codes
12
13# Example output: ['A3-F7-2B', 'D9-1C-8E', ...]
14
15# Store hashed
16import bcrypt
17hashed_codes = [bcrypt.hashpw(code.encode(), bcrypt.gensalt()) 
18                for code in codes]

Token Refresh Pattern

sequenceDiagram
    participant User
    participant Client
    participant Server
    
    Note over User,Server: Initial Login
    User->>Client: Login
    Client->>Server: POST /login (credentials)
    Server->>Client: Access Token (15min) + Refresh Token (7d)
    
    Note over User,Server: Using Access Token
    User->>Client: Request data
    Client->>Server: GET /api/data (Access Token)
    Server->>Client: Data
    
    Note over User,Server: Access Token Expired
    User->>Client: Request data
    Client->>Server: GET /api/data (Expired Access Token)
    Server->>Client: 401 Unauthorized
    Client->>Server: POST /refresh (Refresh Token)
    Server->>Client: New Access Token (15min)
    Client->>Server: GET /api/data (New Access Token)
    Server->>Client: Data
    
    Note over User,Server: Refresh Token Expired
    Client->>Server: POST /refresh (Expired Refresh Token)
    Server->>Client: 401 Unauthorized
    Client->>User: Redirect to login

Benefits

  • Short-lived access tokens limit damage if stolen
  • Can revoke refresh tokens server-side
  • Balance security and UX

Storage Strategy

  • Access Token: Memory (SPA) or HttpOnly cookie
  • Refresh Token: HttpOnly, Secure, SameSite cookie (longer TTL)

Implementation Example

 1// Client-side token refresh
 2async function fetchWithAuth(url, options = {}) {
 3    let response = await fetch(url, {
 4        ...options,
 5        headers: {
 6            ...options.headers,
 7            'Authorization': `Bearer ${accessToken}`
 8        }
 9    });
10    
11    if (response.status === 401) {
12        // Try to refresh token
13        const refreshed = await refreshAccessToken();
14        if (refreshed) {
15            // Retry original request
16            response = await fetch(url, {
17                ...options,
18                headers: {
19                    ...options.headers,
20                    'Authorization': `Bearer ${accessToken}`
21                }
22            });
23        } else {
24            // Redirect to login
25            window.location.href = '/login';
26        }
27    }
28    
29    return response;
30}
31
32async function refreshAccessToken() {
33    const response = await fetch('/api/refresh', {
34        method: 'POST',
35        credentials: 'include' // Send refresh token cookie
36    });
37    
38    if (response.ok) {
39        const data = await response.json();
40        accessToken = data.access_token;
41        return true;
42    }
43    return false;
44}

Common Vulnerabilities

1. JWT Vulnerabilities

Algorithm Confusion Attack:

1// Attacker changes header
2{
3  "alg": "none",  // Changed from "RS256"
4  "typ": "JWT"
5}

Mitigation:

 1# Always specify and validate algorithm
 2import jwt
 3
 4def verify_token(token):
 5    try:
 6        payload = jwt.decode(
 7            token,
 8            public_key,
 9            algorithms=['RS256']  # Whitelist specific algorithm
10        )
11        return payload
12    except jwt.InvalidAlgorithmError:
13        return None

Other JWT Issues:

1❌ Weak signing keys (< 256 bits)
2❌ No expiration validation
3❌ Storing sensitive data in payload (it's base64, not encrypted!)
4❌ Not validating issuer (iss) and audience (aud)

Best Practices:

 1# Generate strong key
 2import secrets
 3secret_key = secrets.token_urlsafe(32)  # 256 bits
 4
 5# Create JWT with all claims
 6token = jwt.encode({
 7    'sub': user_id,
 8    'iat': datetime.utcnow(),
 9    'exp': datetime.utcnow() + timedelta(minutes=15),
10    'iss': 'myapp.com',
11    'aud': 'myapp.com'
12}, secret_key, algorithm='HS256')
13
14# Verify with all checks
15payload = jwt.decode(
16    token,
17    secret_key,
18    algorithms=['HS256'],
19    issuer='myapp.com',
20    audience='myapp.com'
21)

2. Broken Access Control (IDOR)

Insecure Direct Object References:

1❌ BAD: GET /api/users/123/orders
2→ Attacker changes to /api/users/456/orders

Mitigation:

1@app.route('/api/users/<user_id>/orders')
2@login_required
3def get_orders(user_id):
4    # Always check authorization!
5    if current_user.id != user_id and not current_user.is_admin:
6        abort(403)
7    
8    return Orders.query.filter_by(user_id=user_id).all()

Better: Use UUIDs instead of sequential IDs:

1import uuid
2
3# Generate UUID
4user_id = str(uuid.uuid4())  # e.g., '550e8400-e29b-41d4-a716-446655440000'

3. Session Fixation

Attack: Attacker sets victim's session ID before login

Mitigation:

1# Regenerate session ID after login
2@app.route('/login', methods=['POST'])
3def login():
4    user = authenticate(request.form['username'], request.form['password'])
5    if user:
6        # Regenerate session ID
7        session.regenerate()
8        session['user_id'] = user.id
9        return redirect('/dashboard')

Security Checklist

 1✅ Passwords hashed with bcrypt/Argon2 (cost factor ≥ 12)
 2✅ HTTPS enforced (HSTS header)
 3✅ Rate limiting on login/API endpoints
 4✅ Account lockout after failed attempts
 5✅ CSRF protection enabled
 6✅ Secure cookie flags (HttpOnly, Secure, SameSite)
 7✅ JWT with short expiration (15 min) + refresh tokens
 8✅ MFA available for sensitive operations
 9✅ Authorization checks on every endpoint
10✅ Input validation and sanitization
11✅ Security headers (CSP, X-Frame-Options, etc.)
12✅ Regular security audits and penetration testing
13✅ Logging and monitoring for suspicious activity

Related Snippets