Add improvements
This commit is contained in:
@@ -343,7 +343,13 @@ def get_current_tenant_id(request: Request) -> Optional[str]:
|
||||
def extract_user_from_headers(request: Request) -> Optional[Dict[str, Any]]:
|
||||
"""Extract user information from forwarded headers (gateway sets these)"""
|
||||
user_id = request.headers.get("x-user-id")
|
||||
logger.info(f"🔍 Extracting user from headers",
|
||||
user_id=user_id,
|
||||
has_user_id=bool(user_id),
|
||||
path=request.url.path)
|
||||
|
||||
if not user_id:
|
||||
logger.warning(f"❌ No x-user-id header found", path=request.url.path)
|
||||
return None
|
||||
|
||||
user_context = {
|
||||
@@ -359,6 +365,10 @@ def extract_user_from_headers(request: Request) -> Optional[Dict[str, Any]]:
|
||||
"demo_account_type": request.headers.get("x-demo-account-type", "")
|
||||
}
|
||||
|
||||
logger.info(f"✅ User context extracted from headers",
|
||||
user_context=user_context,
|
||||
path=request.url.path)
|
||||
|
||||
# ✅ ADD THIS: Handle service tokens properly
|
||||
user_type = request.headers.get("x-user-type", "")
|
||||
service_name = request.headers.get("x-service-name", "")
|
||||
@@ -448,17 +458,18 @@ def extract_user_from_jwt(auth_header: str) -> Optional[Dict[str, Any]]:
|
||||
async def get_current_user_dep(request: Request) -> Dict[str, Any]:
|
||||
"""FastAPI dependency to get current user - ENHANCED with JWT fallback for services"""
|
||||
try:
|
||||
# Log all incoming headers for debugging 401 issues
|
||||
logger.debug(
|
||||
"Authentication attempt",
|
||||
# Enhanced logging for debugging
|
||||
logger.info(
|
||||
"🔐 Authentication attempt",
|
||||
path=request.url.path,
|
||||
method=request.method,
|
||||
has_auth_header=bool(request.headers.get("authorization")),
|
||||
has_x_user_id=bool(request.headers.get("x-user-id")),
|
||||
has_x_user_type=bool(request.headers.get("x-user-type")),
|
||||
has_x_service_name=bool(request.headers.get("x-service-name")),
|
||||
x_user_type=request.headers.get("x-user-type", ""),
|
||||
x_service_name=request.headers.get("x-service-name", ""),
|
||||
has_x_is_demo=bool(request.headers.get("x-is-demo")),
|
||||
has_x_demo_session_id=bool(request.headers.get("x-demo-session-id")),
|
||||
x_user_id=request.headers.get("x-user-id", "MISSING"),
|
||||
x_is_demo=request.headers.get("x-is-demo", "MISSING"),
|
||||
x_demo_session_id=request.headers.get("x-demo-session-id", "MISSING"),
|
||||
client_ip=request.client.host if request.client else "unknown"
|
||||
)
|
||||
|
||||
|
||||
@@ -126,24 +126,47 @@ class JWTHandler:
|
||||
logger.debug(f"Created refresh token for user {user_data['user_id']}")
|
||||
return encoded_jwt
|
||||
|
||||
def create_service_token(self, service_name: str, expires_delta: Optional[timedelta] = None) -> str:
|
||||
def create_service_token(
|
||||
self,
|
||||
service_name: str,
|
||||
expires_delta: Optional[timedelta] = None,
|
||||
tenant_id: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Create JWT SERVICE token for inter-service communication
|
||||
✅ FIXED: Service tokens have proper service account structure
|
||||
✅ UNIFIED: Single source of truth for all service token creation
|
||||
✅ ENHANCED: Supports tenant context for tenant-scoped operations
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'auth-service', 'demo-session')
|
||||
expires_delta: Optional expiration time (defaults to 1 hour for inter-service calls)
|
||||
tenant_id: Optional tenant ID for tenant-scoped service operations
|
||||
|
||||
Returns:
|
||||
Encoded JWT service token
|
||||
"""
|
||||
to_encode = {
|
||||
"sub": service_name,
|
||||
"user_id": f"{service_name}-service",
|
||||
"email": f"{service_name}-service@internal",
|
||||
"service": service_name,
|
||||
"type": "service",
|
||||
"role": "admin",
|
||||
"is_service": True
|
||||
"role": "admin", # Services have admin privileges
|
||||
"is_service": True,
|
||||
"full_name": f"{service_name.title()} Service",
|
||||
"is_verified": True,
|
||||
"is_active": True
|
||||
}
|
||||
|
||||
# Set expiration
|
||||
# Include tenant context when provided for tenant-scoped operations
|
||||
if tenant_id:
|
||||
to_encode["tenant_id"] = tenant_id
|
||||
|
||||
# Set expiration (default to 1 hour for inter-service calls)
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(days=365)
|
||||
expire = datetime.now(timezone.utc) + timedelta(hours=1) # 1 hour default
|
||||
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
@@ -152,7 +175,7 @@ class JWTHandler:
|
||||
})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
|
||||
logger.debug(f"Created service token for service {service_name}")
|
||||
logger.debug(f"Created service token for service {service_name}", tenant_id=tenant_id)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
|
||||
@@ -230,42 +253,7 @@ class JWTHandler:
|
||||
|
||||
return None
|
||||
|
||||
def create_service_token(self, service_name: str, expires_delta: Optional[timedelta] = None) -> str:
|
||||
"""
|
||||
Create JWT token for service-to-service communication
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'auth-service', 'tenant-service')
|
||||
expires_delta: Optional expiration time (defaults to 365 days for services)
|
||||
|
||||
Returns:
|
||||
Encoded JWT service token
|
||||
"""
|
||||
to_encode = {
|
||||
"sub": service_name,
|
||||
"user_id": service_name,
|
||||
"service": service_name,
|
||||
"type": "service",
|
||||
"is_service": True,
|
||||
"role": "admin", # Services have admin privileges
|
||||
"email": f"{service_name}@internal.service"
|
||||
}
|
||||
|
||||
# Set expiration (default to 1 year for service tokens)
|
||||
if expires_delta:
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.now(timezone.utc) + timedelta(days=365)
|
||||
|
||||
to_encode.update({
|
||||
"exp": expire,
|
||||
"iat": datetime.now(timezone.utc),
|
||||
"iss": "bakery-auth"
|
||||
})
|
||||
|
||||
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
|
||||
logger.info(f"Created service token for {service_name}")
|
||||
return encoded_jwt
|
||||
|
||||
def get_token_info(self, token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -27,41 +27,36 @@ class ServiceAuthenticator:
|
||||
self.jwt_handler = JWTHandler(config.JWT_SECRET_KEY)
|
||||
self._cached_token = None
|
||||
self._token_expires_at = 0
|
||||
self._cached_tenant_id = None # Track tenant context for cached tokens
|
||||
|
||||
async def get_service_token(self) -> str:
|
||||
async def get_service_token(self, tenant_id: Optional[str] = None) -> str:
|
||||
"""Get a valid service token, using cache when possible"""
|
||||
current_time = int(time.time())
|
||||
|
||||
# Return cached token if still valid (with 5 min buffer)
|
||||
# Return cached token if still valid (with 5 min buffer) and tenant context matches
|
||||
if (self._cached_token and
|
||||
self._token_expires_at > current_time + 300):
|
||||
self._token_expires_at > current_time + 300 and
|
||||
(tenant_id is None or self._cached_tenant_id == tenant_id)):
|
||||
return self._cached_token
|
||||
|
||||
# Create new service token
|
||||
token_expires_at = current_time + 3600 # 1 hour
|
||||
|
||||
service_payload = {
|
||||
"sub": f"{self.service_name}-service",
|
||||
"user_id": f"{self.service_name}-service",
|
||||
"email": f"{self.service_name}-service@internal",
|
||||
"type": "service",
|
||||
"role": "admin",
|
||||
"exp": token_expires_at,
|
||||
"iat": current_time,
|
||||
"iss": f"{self.service_name}-service",
|
||||
"service": self.service_name,
|
||||
"full_name": f"{self.service_name.title()} Service",
|
||||
"is_verified": True,
|
||||
"is_active": True,
|
||||
"tenant_id": None
|
||||
}
|
||||
|
||||
# Create new service token using unified JWT handler
|
||||
try:
|
||||
token = self.jwt_handler.create_access_token_from_payload(service_payload)
|
||||
token = self.jwt_handler.create_service_token(
|
||||
service_name=self.service_name,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
# Extract expiration from token for caching
|
||||
import json
|
||||
from jose import jwt
|
||||
payload = jwt.decode(token, self.jwt_handler.secret_key, algorithms=[self.jwt_handler.algorithm], options={"verify_signature": False})
|
||||
token_expires_at = payload.get("exp", current_time + 3600)
|
||||
|
||||
self._cached_token = token
|
||||
self._token_expires_at = token_expires_at
|
||||
self._cached_tenant_id = tenant_id # Store tenant context for caching
|
||||
|
||||
logger.debug("Created new service token", service=self.service_name, expires_at=token_expires_at)
|
||||
logger.debug("Created new service token", service=self.service_name, expires_at=token_expires_at, tenant_id=tenant_id)
|
||||
return token
|
||||
|
||||
except Exception as e:
|
||||
@@ -181,8 +176,8 @@ class BaseServiceClient(ABC):
|
||||
Called by _make_request through circuit breaker.
|
||||
"""
|
||||
try:
|
||||
# Get service token
|
||||
token = await self.authenticator.get_service_token()
|
||||
# Get service token with tenant context for tenant-scoped requests
|
||||
token = await self.authenticator.get_service_token(tenant_id)
|
||||
|
||||
# Build headers
|
||||
request_headers = self.authenticator.get_request_headers(tenant_id)
|
||||
|
||||
@@ -21,6 +21,8 @@ INTERNAL_SERVICES: Set[str] = {
|
||||
# Core services
|
||||
"auth-service",
|
||||
"tenant-service",
|
||||
"gateway", # API Gateway
|
||||
"gateway-service", # Alternative name for gateway
|
||||
|
||||
# Business logic services
|
||||
"inventory-service",
|
||||
@@ -30,24 +32,27 @@ INTERNAL_SERVICES: Set[str] = {
|
||||
"pos-service",
|
||||
"orders-service",
|
||||
"sales-service",
|
||||
"procurement-service",
|
||||
|
||||
# ML and analytics services
|
||||
"training-service",
|
||||
"forecasting-service",
|
||||
"ai-insights-service",
|
||||
|
||||
# Orchestration services
|
||||
"orchestrator-service",
|
||||
|
||||
# Support services
|
||||
"notification-service",
|
||||
"alert-service",
|
||||
"alert-processor-service",
|
||||
"alert-processor", # Alternative name (from k8s service name)
|
||||
"demo-session-service",
|
||||
"demo-service", # Alternative name for demo session service
|
||||
"external-service",
|
||||
|
||||
# Enterprise services
|
||||
"distribution-service",
|
||||
|
||||
# Legacy/alternative naming (for backwards compatibility)
|
||||
"data-service", # May be used by older components
|
||||
}
|
||||
|
||||
|
||||
@@ -195,16 +200,14 @@ class BaseServiceSettings(BaseSettings):
|
||||
# ================================================================
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "your-super-secret-jwt-key-change-in-production-min-32-characters-long")
|
||||
# ✅ FIXED: Use production JWT secret key to match auth service
|
||||
# Must be same across all services for inter-service communication
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "usMHw9kQCQoyrc7wPmMi3bClr0lTY9wvzZmcTbADvL0=")
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = int(os.getenv("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "7"))
|
||||
|
||||
# Service-to-Service Authentication
|
||||
SERVICE_API_KEY: str = os.getenv("SERVICE_API_KEY", "service-api-key-change-in-production")
|
||||
INTERNAL_API_KEY: str = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
||||
ENABLE_SERVICE_AUTH: bool = os.getenv("ENABLE_SERVICE_AUTH", "false").lower() == "true"
|
||||
API_GATEWAY_URL: str = os.getenv("API_GATEWAY_URL", "http://gateway-service:8000")
|
||||
|
||||
|
||||
# Password Requirements
|
||||
PASSWORD_MIN_LENGTH: int = int(os.getenv("PASSWORD_MIN_LENGTH", "8"))
|
||||
|
||||
Reference in New Issue
Block a user