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)

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

Login Flow with MFA

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

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