GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
156
apps/control-panel-backend/app/core/auth.py
Normal file
156
apps/control-panel-backend/app/core/auth.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""
|
||||
Authentication and authorization utilities
|
||||
"""
|
||||
import jwt
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import HTTPException, Security, Depends, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.database import get_db
|
||||
from app.models.user import User
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
"""JWT token handler"""
|
||||
|
||||
@staticmethod
|
||||
def create_access_token(
|
||||
user_id: int,
|
||||
user_email: str,
|
||||
user_type: str,
|
||||
current_tenant: Optional[dict] = None,
|
||||
available_tenants: Optional[list] = None,
|
||||
capabilities: Optional[list] = None,
|
||||
# For token refresh: preserve original login time and absolute expiry
|
||||
original_iat: Optional[datetime] = None,
|
||||
original_absolute_exp: Optional[float] = None,
|
||||
# Server-side session token (Issue #264)
|
||||
session_token: Optional[str] = None
|
||||
) -> str:
|
||||
"""Create a JWT access token with tenant context
|
||||
|
||||
NIST SP 800-63B AAL2 Compliant Session Management (Issues #242, #264):
|
||||
- exp: 12 hours (matches absolute timeout) - serves as JWT-level backstop
|
||||
- absolute_exp: Absolute timeout (12 hours) - NOT refreshable, forces re-login
|
||||
- iat: Original login time - preserved across token refreshes
|
||||
- session_id: Server-side session token for authoritative validation
|
||||
|
||||
The server-side session (via SessionService) enforces the 30-minute idle timeout
|
||||
by tracking last_activity_at. JWT exp is set to 12 hours so it doesn't block
|
||||
requests before the server-side session validation can check activity-based idle timeout.
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Use original iat if refreshing, otherwise current time (new login)
|
||||
iat = original_iat if original_iat else now
|
||||
|
||||
# Calculate absolute expiry: iat + absolute timeout hours (only set on initial login)
|
||||
if original_absolute_exp is not None:
|
||||
absolute_exp = original_absolute_exp
|
||||
else:
|
||||
absolute_exp = (iat + timedelta(hours=settings.JWT_ABSOLUTE_TIMEOUT_HOURS)).timestamp()
|
||||
|
||||
payload = {
|
||||
"sub": str(user_id),
|
||||
"email": user_email,
|
||||
"user_type": user_type,
|
||||
|
||||
# Current tenant context (most important)
|
||||
"current_tenant": current_tenant or {},
|
||||
|
||||
# Available tenants for switching
|
||||
"available_tenants": available_tenants or [],
|
||||
|
||||
# Base capabilities (rarely used - tenant-specific capabilities are in current_tenant)
|
||||
"capabilities": capabilities or [],
|
||||
|
||||
# NIST/OWASP Session Timeouts (Issues #242, #264)
|
||||
# exp: Idle timeout - 4 hours from now (refreshable)
|
||||
"exp": now + timedelta(minutes=settings.JWT_EXPIRES_MINUTES),
|
||||
# iat: Original login time (preserved across refreshes)
|
||||
"iat": iat,
|
||||
# absolute_exp: Absolute timeout from original login (NOT refreshable)
|
||||
"absolute_exp": absolute_exp,
|
||||
# session_id: Server-side session token for authoritative validation (Issue #264)
|
||||
# The server-side session is the source of truth - JWT expiry is secondary
|
||||
"session_id": session_token
|
||||
}
|
||||
|
||||
# Use HS256 with JWT_SECRET from settings (auto-generated by installer)
|
||||
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
|
||||
|
||||
@staticmethod
|
||||
def decode_token(token: str) -> Dict[str, Any]:
|
||||
"""Decode and validate a JWT token"""
|
||||
try:
|
||||
# Use HS256 with JWT_SECRET from settings (auto-generated by installer)
|
||||
payload = jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has expired"
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> User:
|
||||
"""Get the current authenticated user"""
|
||||
|
||||
token = credentials.credentials
|
||||
payload = JWTHandler.decode_token(token)
|
||||
|
||||
user_id = int(payload["sub"])
|
||||
|
||||
# Get user from database
|
||||
result = await db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User account is inactive"
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""Require the current user to be a super admin (control panel access)"""
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin access required"
|
||||
)
|
||||
return current_user
|
||||
|
||||
|
||||
async def require_super_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||
"""Require the current user to be a super admin"""
|
||||
if current_user.user_type != "super_admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Super admin access required"
|
||||
)
|
||||
return current_user
|
||||
Reference in New Issue
Block a user