""" Custom authentication classes for DRF """ from rest_framework.authentication import SessionAuthentication, BaseAuthentication from rest_framework.exceptions import AuthenticationFailed import jwt class CSRFExemptSessionAuthentication(SessionAuthentication): """ Session authentication that doesn't enforce CSRF for API endpoints. This is safe for API usage since we're using session cookies and proper CORS settings. """ def enforce_csrf(self, request): """ Override to skip CSRF enforcement for API endpoints. """ return # Skip CSRF check class JWTAuthentication(BaseAuthentication): """ JWT token authentication for DRF. Extracts JWT token from Authorization header and validates it. """ def authenticate(self, request): """ Authenticate the request and return a two-tuple of (user, token). """ auth_header = request.META.get('HTTP_AUTHORIZATION', '') if not auth_header.startswith('Bearer '): return None # No JWT token, let other auth classes handle it token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None if not token: return None try: from igny8_core.auth.utils import decode_token from igny8_core.auth.models import User, Account # Decode and validate token payload = decode_token(token) # Verify it's an access token if payload.get('type') != 'access': # Invalid token type - return None to allow other auth classes to try return None # Get user user_id = payload.get('user_id') if not user_id: # Invalid token payload - return None to allow other auth classes to try return None try: user = User.objects.get(id=user_id) except User.DoesNotExist: # User not found - return None to allow other auth classes to try return None # Get account from token account_id = payload.get('account_id') account = None if account_id: try: account = Account.objects.get(id=account_id) except Account.DoesNotExist: # Account from token doesn't exist - don't fallback, set to None account = None # Set account on request (only if account_id was in token and account exists) request.account = account return (user, token) except jwt.InvalidTokenError: # Invalid or expired token - return None to allow other auth classes (session) to try return None except Exception as e: # Other errors - return None to allow other auth classes to try # This allows session authentication to work if JWT fails return None class APIKeyAuthentication(BaseAuthentication): """ API Key authentication for WordPress integration. Validates API keys stored in Site.wp_api_key field. """ def authenticate(self, request): """ Authenticate using WordPress API key. Returns (user, api_key) tuple if valid. """ auth_header = request.META.get('HTTP_AUTHORIZATION', '') if not auth_header.startswith('Bearer '): return None # Not an API key request api_key = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None if not api_key or len(api_key) < 20: # API keys should be at least 20 chars return None # Don't try to authenticate JWT tokens (they start with 'ey') if api_key.startswith('ey'): return None # Let JWTAuthentication handle it try: from igny8_core.auth.models import Site, User from igny8_core.auth.utils import validate_account_and_plan from rest_framework.exceptions import AuthenticationFailed # Find site by API key site = Site.objects.select_related('account', 'account__owner', 'account__plan').filter( wp_api_key=api_key, is_active=True ).first() if not site: return None # API key not found or site inactive # Get account and validate it account = site.account if not account: raise AuthenticationFailed('No account associated with this API key.') # CRITICAL FIX: Validate account and plan status is_valid, error_message, http_status = validate_account_and_plan(account) if not is_valid: raise AuthenticationFailed(error_message) # Get user (prefer owner but gracefully fall back) user = account.owner if not user or not getattr(user, 'is_active', False): # Fall back to any active developer/owner/admin in the account user = account.users.filter( is_active=True, role__in=['developer', 'owner', 'admin'] ).order_by('role').first() or account.users.filter(is_active=True).first() if not user: raise AuthenticationFailed('No active user available for this account.') if not user.is_active: raise AuthenticationFailed('User account is disabled.') # Set account on request for tenant isolation request.account = account # Set site on request for WordPress integration context request.site = site return (user, api_key) except Exception as e: # Log the error but return None to allow other auth classes to try import logging logger = logging.getLogger(__name__) logger.debug(f'APIKeyAuthentication error: {str(e)}') return None