- Fix Python formatting (sync no longer strips blank lines from .py files) - Remove test_community_build.py (tests private sync module) - Fix ruff warnings in test files - Add CODE_OF_CONDUCT.md - Add .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
127 lines
3.4 KiB
Python
127 lines
3.4 KiB
Python
"""
|
|
CSRF Protection for OAuth Authorization
|
|
|
|
Implements token-based CSRF protection for the OAuth authorization flow.
|
|
Tokens are stored in-memory with 10-minute expiry.
|
|
|
|
Part of Phase E: Custom OAuth Authorization Page
|
|
"""
|
|
|
|
import secrets
|
|
import time
|
|
|
|
|
|
class CSRFTokenManager:
|
|
"""
|
|
Manages CSRF tokens for OAuth authorization requests.
|
|
|
|
Features:
|
|
- Generates cryptographically secure random tokens
|
|
- In-memory storage with automatic expiry
|
|
- 10-minute token lifetime
|
|
- Automatic cleanup of expired tokens
|
|
"""
|
|
|
|
def __init__(self, token_lifetime_seconds: int = 600):
|
|
"""
|
|
Initialize CSRF token manager.
|
|
|
|
Args:
|
|
token_lifetime_seconds: Token lifetime in seconds (default: 600 = 10 minutes)
|
|
"""
|
|
self._tokens: dict[str, float] = {} # token -> expiry_timestamp
|
|
self._token_lifetime = token_lifetime_seconds
|
|
|
|
def generate_token(self) -> str:
|
|
"""
|
|
Generate a new CSRF token.
|
|
|
|
Returns:
|
|
Secure random token (32 bytes, hex-encoded = 64 characters)
|
|
"""
|
|
token = secrets.token_hex(32)
|
|
expiry = time.time() + self._token_lifetime
|
|
self._tokens[token] = expiry
|
|
|
|
# Cleanup expired tokens (lazy cleanup)
|
|
self._cleanup_expired()
|
|
|
|
return token
|
|
|
|
def validate_token(self, token: str, consume: bool = True) -> bool:
|
|
"""
|
|
Validate a CSRF token.
|
|
|
|
Args:
|
|
token: CSRF token to validate
|
|
consume: If True, token is removed after validation (one-time use)
|
|
|
|
Returns:
|
|
True if token is valid and not expired, False otherwise
|
|
"""
|
|
if not token or token not in self._tokens:
|
|
return False
|
|
|
|
expiry = self._tokens[token]
|
|
now = time.time()
|
|
|
|
# Check if token is expired
|
|
if now > expiry:
|
|
# Remove expired token
|
|
self._tokens.pop(token, None)
|
|
return False
|
|
|
|
# Token is valid
|
|
if consume:
|
|
# Remove token (one-time use)
|
|
self._tokens.pop(token, None)
|
|
|
|
return True
|
|
|
|
def _cleanup_expired(self):
|
|
"""
|
|
Remove expired tokens from storage.
|
|
|
|
Called automatically during token generation to prevent memory leaks.
|
|
"""
|
|
now = time.time()
|
|
expired_tokens = [token for token, expiry in self._tokens.items() if now > expiry]
|
|
|
|
for token in expired_tokens:
|
|
self._tokens.pop(token, None)
|
|
|
|
def get_stats(self) -> dict[str, int]:
|
|
"""
|
|
Get statistics about stored tokens.
|
|
|
|
Returns:
|
|
Dictionary with token statistics
|
|
"""
|
|
now = time.time()
|
|
active_tokens = sum(1 for expiry in self._tokens.values() if expiry > now)
|
|
expired_tokens = len(self._tokens) - active_tokens
|
|
|
|
return {
|
|
"total_tokens": len(self._tokens),
|
|
"active_tokens": active_tokens,
|
|
"expired_tokens": expired_tokens,
|
|
"token_lifetime_seconds": self._token_lifetime,
|
|
}
|
|
|
|
|
|
# Global CSRF token manager instance
|
|
_csrf_manager: CSRFTokenManager | None = None
|
|
|
|
|
|
def get_csrf_manager() -> CSRFTokenManager:
|
|
"""
|
|
Get the global CSRF token manager instance.
|
|
|
|
Returns:
|
|
Global CSRFTokenManager instance (singleton)
|
|
"""
|
|
global _csrf_manager
|
|
if _csrf_manager is None:
|
|
_csrf_manager = CSRFTokenManager()
|
|
return _csrf_manager
|