647928a0a1
Fundamentals: secure-defaults, input-validation, credential-handling, audit-logging Identity: authentication, authorization Attack Prevention: injection-prevention, dos-prevention, prompt-injection
160 lines
4.1 KiB
Markdown
160 lines
4.1 KiB
Markdown
# Authentication
|
|
|
|
## Rule
|
|
|
|
Verify identity before granting access. Use proven libraries, not DIY crypto.
|
|
|
|
**Source:** [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
|
|
|
## Password Handling
|
|
|
|
### Correct Pattern
|
|
|
|
```python
|
|
import bcrypt
|
|
import secrets
|
|
|
|
def hash_password(password: str) -> bytes:
|
|
"""Hash password using bcrypt with automatic salt."""
|
|
return bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
|
|
|
def verify_password(password: str, hashed: bytes) -> bool:
|
|
"""Verify password against hash. Constant-time comparison."""
|
|
return bcrypt.checkpw(password.encode(), hashed)
|
|
|
|
# Password requirements
|
|
MIN_PASSWORD_LENGTH = 12
|
|
COMMON_PASSWORDS = load_common_passwords() # Top 10k list
|
|
|
|
def validate_password(password: str) -> list[str]:
|
|
"""Return list of validation errors."""
|
|
errors = []
|
|
if len(password) < MIN_PASSWORD_LENGTH:
|
|
errors.append(f"Password must be at least {MIN_PASSWORD_LENGTH} characters")
|
|
if password.lower() in COMMON_PASSWORDS:
|
|
errors.append("Password is too common")
|
|
return errors
|
|
```
|
|
|
|
### Incorrect Pattern
|
|
|
|
```python
|
|
# Wrong: plain text storage
|
|
user.password = password
|
|
|
|
# Wrong: weak hashing
|
|
user.password = hashlib.md5(password.encode()).hexdigest()
|
|
|
|
# Wrong: SHA without salt
|
|
user.password = hashlib.sha256(password.encode()).hexdigest()
|
|
|
|
# Wrong: reversible encryption
|
|
user.password = encrypt(password, key)
|
|
|
|
# Wrong: timing attack vulnerable
|
|
if user.password == submitted_password:
|
|
grant_access()
|
|
```
|
|
|
|
## Token Management
|
|
|
|
### Correct Pattern
|
|
|
|
```python
|
|
import secrets
|
|
from datetime import datetime, timedelta
|
|
|
|
def generate_token() -> str:
|
|
"""Generate cryptographically secure token."""
|
|
return secrets.token_urlsafe(32)
|
|
|
|
def generate_session(user_id: str) -> dict:
|
|
"""Create session with expiration."""
|
|
return {
|
|
"token": generate_token(),
|
|
"user_id": user_id,
|
|
"created_at": datetime.utcnow(),
|
|
"expires_at": datetime.utcnow() + timedelta(hours=24),
|
|
}
|
|
|
|
def validate_session(session: dict) -> bool:
|
|
"""Check session validity."""
|
|
if datetime.utcnow() > session["expires_at"]:
|
|
return False
|
|
return True
|
|
```
|
|
|
|
### Incorrect Pattern
|
|
|
|
```python
|
|
# Wrong: predictable tokens
|
|
token = f"session_{user_id}_{int(time.time())}"
|
|
|
|
# Wrong: no expiration
|
|
session = {"token": token, "user_id": user_id}
|
|
|
|
# Wrong: client-controlled expiration
|
|
if request.cookies.get("expires") > now: # User can modify!
|
|
grant_access()
|
|
```
|
|
|
|
## Multi-Factor Authentication
|
|
|
|
```python
|
|
import pyotp
|
|
|
|
def setup_totp(user_id: str) -> str:
|
|
"""Generate TOTP secret for user."""
|
|
secret = pyotp.random_base32()
|
|
store_totp_secret(user_id, secret)
|
|
return secret
|
|
|
|
def verify_totp(user_id: str, code: str) -> bool:
|
|
"""Verify TOTP code with time window."""
|
|
secret = get_totp_secret(user_id)
|
|
totp = pyotp.TOTP(secret)
|
|
return totp.verify(code, valid_window=1) # ±30 seconds
|
|
```
|
|
|
|
## Brute Force Protection
|
|
|
|
```python
|
|
from collections import defaultdict
|
|
import time
|
|
|
|
class LoginRateLimiter:
|
|
def __init__(self):
|
|
self.attempts = defaultdict(list)
|
|
self.lockouts = {}
|
|
|
|
def record_attempt(self, identifier: str, success: bool):
|
|
now = time.time()
|
|
|
|
if not success:
|
|
self.attempts[identifier].append(now)
|
|
# Clean old attempts
|
|
self.attempts[identifier] = [
|
|
t for t in self.attempts[identifier]
|
|
if now - t < 3600 # 1 hour window
|
|
]
|
|
|
|
# Lockout after 5 failures
|
|
if len(self.attempts[identifier]) >= 5:
|
|
self.lockouts[identifier] = now + 900 # 15 min lockout
|
|
else:
|
|
self.attempts[identifier] = []
|
|
self.lockouts.pop(identifier, None)
|
|
|
|
def is_locked(self, identifier: str) -> bool:
|
|
lockout_until = self.lockouts.get(identifier, 0)
|
|
return time.time() < lockout_until
|
|
```
|
|
|
|
## Edge Cases
|
|
|
|
- Timing attacks on username enumeration
|
|
- Account lockout as DOS vector
|
|
- Session fixation attacks
|
|
- Token leakage in logs/URLs
|
|
- Password reset token reuse
|