Add improvements
This commit is contained in:
@@ -190,6 +190,197 @@ graph TD
|
||||
- Gateway validates access to requested tenant
|
||||
- Prevents tenant ID spoofing attacks
|
||||
|
||||
## JWT Service Token Architecture
|
||||
|
||||
### Overview
|
||||
The Auth Service implements **JWT service tokens** for secure service-to-service (S2S) authentication across all microservices. This eliminates the need for internal API keys and provides a unified, secure authentication mechanism for both user and service requests.
|
||||
|
||||
### Service Token vs User Token
|
||||
|
||||
**User Tokens** (for frontend/API consumers):
|
||||
- `type: "access"` - Regular user authentication
|
||||
- Contains user ID, email, tenant membership
|
||||
- Expires in 15-30 minutes
|
||||
- Used by browsers and mobile apps
|
||||
|
||||
**Service Tokens** (for microservice communication):
|
||||
- `type: "service"` - Internal service authentication
|
||||
- Contains service name, optional tenant context
|
||||
- Expires in 1 hour (longer for batch operations)
|
||||
- Used by backend services calling other services
|
||||
|
||||
### Service Token Payload Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"sub": "demo-session",
|
||||
"user_id": "demo-session-service",
|
||||
"email": "demo-session-service@internal",
|
||||
"service": "demo-session",
|
||||
"type": "service",
|
||||
"role": "admin",
|
||||
"tenant_id": "optional-tenant-uuid",
|
||||
"exp": 1735693199,
|
||||
"iat": 1735689599,
|
||||
"iss": "bakery-auth"
|
||||
}
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
#### 1. Unified JWT Handler
|
||||
- **File**: `shared/auth/jwt_handler.py`
|
||||
- **Purpose**: Single source of truth for token creation and validation
|
||||
- **Method**: `create_service_token(service_name, tenant_id=None)`
|
||||
- **Shared JWT Secret**: All services use same `JWT_SECRET_KEY` from `shared/config/base.py`
|
||||
|
||||
#### 2. Internal Service Registry
|
||||
- **File**: `shared/config/base.py`
|
||||
- **Constant**: `INTERNAL_SERVICES` set containing all 21 microservice names
|
||||
- **Purpose**: Automatic access grants for registered services
|
||||
- **Services**: gateway, auth, tenant, inventory, production, recipes, suppliers, orders, sales, procurement, pos, forecasting, training, ai-insights, orchestrator, notification, alert-processor, demo-session, external, distribution
|
||||
|
||||
#### 3. Service Authentication Flow
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Service A │
|
||||
│ (e.g., demo) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│ 1. Create service token
|
||||
│ jwt_handler.create_service_token(
|
||||
│ service_name="demo-session",
|
||||
│ tenant_id=tenant_id
|
||||
│ )
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ HTTP Request │
|
||||
│ -------------------------------- │
|
||||
│ POST /api/v1/tenant/clone │
|
||||
│ Headers: │
|
||||
│ Authorization: Bearer {token} │
|
||||
│ X-Service: demo-session-service │
|
||||
└────────┬────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Service B │
|
||||
│ (e.g., tenant) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
│ 2. Validate token
|
||||
│ jwt_handler.verify_token(token)
|
||||
│
|
||||
│ 3. Check internal service
|
||||
│ if is_internal_service(user_id):
|
||||
│ grant_admin_access()
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Authorized │
|
||||
│ Response │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
#### 4. Automatic Admin Privileges
|
||||
- Services in `INTERNAL_SERVICES` registry get automatic admin access
|
||||
- No need for tenant membership checks
|
||||
- Optimizes database queries (skips membership lookups)
|
||||
- Used in:
|
||||
- `shared/auth/decorators.py` - JWT authentication decorator
|
||||
- `services/tenant/app/api/tenant_operations.py` - Tenant access verification
|
||||
- `services/tenant/app/repositories/tenant_member_repository.py` - Skip membership queries
|
||||
|
||||
### Migration from Internal API Keys
|
||||
|
||||
**Previous System (Deprecated):**
|
||||
```python
|
||||
# Old approach - REMOVED
|
||||
headers = {
|
||||
"X-Internal-API-Key": "dev-internal-key-change-in-production"
|
||||
}
|
||||
```
|
||||
|
||||
**New System (Current):**
|
||||
```python
|
||||
# New approach - JWT service tokens
|
||||
from shared.auth.jwt_handler import JWTHandler
|
||||
|
||||
jwt_handler = JWTHandler(settings.JWT_SECRET_KEY, settings.JWT_ALGORITHM)
|
||||
service_token = jwt_handler.create_service_token(
|
||||
service_name="my-service",
|
||||
tenant_id=tenant_id # Optional tenant context
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {service_token}",
|
||||
"X-Service": "my-service"
|
||||
}
|
||||
```
|
||||
|
||||
### Security Benefits
|
||||
|
||||
1. **Token Expiration** - Service tokens expire (1 hour), unlike permanent API keys
|
||||
2. **Signature Verification** - JWT signatures prevent token forgery
|
||||
3. **Tenant Context** - Service tokens can include tenant scope for proper authorization
|
||||
4. **Audit Trail** - All service requests are authenticated and logged
|
||||
5. **No Secret Distribution** - Shared JWT secret is managed via environment variables
|
||||
6. **Rotation Ready** - JWT secret can be rotated without changing code
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
- **Token Creation**: <1ms (in-memory JWT signing)
|
||||
- **Token Validation**: <1ms (in-memory JWT verification)
|
||||
- **Cache Enabled**: Gateway caches validated tokens for 5 minutes
|
||||
- **No HTTP Calls**: Service-to-service auth happens locally
|
||||
|
||||
### Implementation Examples
|
||||
|
||||
#### Example 1: Demo Session Service Cloning Data
|
||||
```python
|
||||
# services/demo_session/app/services/clone_orchestrator.py
|
||||
service_token = self.jwt_handler.create_service_token(
|
||||
service_name="demo-session",
|
||||
tenant_id=virtual_tenant_id
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
f"{service.url}/internal/demo/clone",
|
||||
params={...},
|
||||
headers={
|
||||
"Authorization": f"Bearer {service_token}",
|
||||
"X-Service": "demo-session-service"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
#### Example 2: Gateway Validating Demo Sessions
|
||||
```python
|
||||
# gateway/app/middleware/auth.py
|
||||
service_token = jwt_handler.create_service_token(service_name="gateway")
|
||||
|
||||
response = await client.get(
|
||||
f"http://demo-session-service:8000/api/v1/demo/sessions/{session_id}",
|
||||
headers={"Authorization": f"Bearer {service_token}"}
|
||||
)
|
||||
```
|
||||
|
||||
#### Example 3: Deletion Orchestrator
|
||||
```python
|
||||
# services/auth/app/services/deletion_orchestrator.py
|
||||
service_token = self.jwt_handler.create_service_token(
|
||||
service_name="auth",
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {service_token}",
|
||||
"X-Service": "auth-service"
|
||||
}
|
||||
```
|
||||
|
||||
### API Endpoints (Key Routes)
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -367,26 +367,47 @@ async def get_profile(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get user profile - works for JWT auth AND demo sessions"""
|
||||
logger.info(f"📋 Profile request received",
|
||||
user_id=current_user.get("user_id"),
|
||||
is_demo=current_user.get("is_demo", False),
|
||||
demo_session_id=current_user.get("demo_session_id", ""),
|
||||
email=current_user.get("email", ""),
|
||||
path="/api/v1/auth/me")
|
||||
try:
|
||||
user_id = current_user.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
logger.error(f"❌ No user_id in current_user context for profile request",
|
||||
current_user=current_user)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid user context"
|
||||
)
|
||||
|
||||
logger.info(f"🔎 Fetching user profile for user_id: {user_id}",
|
||||
is_demo=current_user.get("is_demo", False),
|
||||
demo_session_id=current_user.get("demo_session_id", ""))
|
||||
|
||||
# Fetch user from database
|
||||
from app.repositories import UserRepository
|
||||
user_repo = UserRepository(User, db)
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
logger.error(f"🚨 User not found in database",
|
||||
user_id=user_id,
|
||||
is_demo=current_user.get("is_demo", False))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User profile not found"
|
||||
)
|
||||
|
||||
logger.info(f"🎉 User profile found",
|
||||
user_id=user.id,
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
is_active=user.is_active)
|
||||
|
||||
return UserResponse(
|
||||
id=str(user.id),
|
||||
email=user.email,
|
||||
|
||||
@@ -30,14 +30,6 @@ router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
DEMO_TENANT_PROFESSIONAL = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
|
||||
|
||||
def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)):
|
||||
"""Verify internal API key for service-to-service communication"""
|
||||
if x_internal_api_key != settings.INTERNAL_API_KEY:
|
||||
logger.warning("Unauthorized internal API access attempted")
|
||||
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/clone")
|
||||
async def clone_demo_data(
|
||||
base_tenant_id: str,
|
||||
@@ -45,8 +37,7 @@ async def clone_demo_data(
|
||||
demo_account_type: str,
|
||||
session_id: Optional[str] = None,
|
||||
session_created_at: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(verify_internal_api_key)
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Clone auth service data for a virtual demo tenant
|
||||
@@ -226,7 +217,7 @@ async def clone_demo_data(
|
||||
|
||||
|
||||
@router.get("/clone/health")
|
||||
async def clone_health_check(_: bool = Depends(verify_internal_api_key)):
|
||||
async def clone_health_check():
|
||||
"""
|
||||
Health check for internal cloning endpoint
|
||||
Used by orchestrator to verify service availability
|
||||
|
||||
@@ -239,24 +239,26 @@ class SecurityManager:
|
||||
return hashlib.sha256(data.encode()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def create_service_token(service_name: str) -> str:
|
||||
def create_service_token(service_name: str, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Create JWT service token for inter-service communication
|
||||
✅ FIXED: Proper service token creation with JWT
|
||||
✅ UNIFIED: Uses shared JWT handler for consistent token creation
|
||||
✅ ENHANCED: Supports tenant context for tenant-scoped operations
|
||||
|
||||
Args:
|
||||
service_name: Name of the service (e.g., 'auth-service', 'tenant-service')
|
||||
tenant_id: Optional tenant ID for tenant-scoped service operations
|
||||
|
||||
Returns:
|
||||
Encoded JWT service token
|
||||
"""
|
||||
try:
|
||||
# Create service token payload
|
||||
payload = {
|
||||
"sub": service_name,
|
||||
"service": service_name,
|
||||
"type": "service",
|
||||
"role": "admin",
|
||||
"is_service": True
|
||||
}
|
||||
|
||||
# Use JWT handler to create service token
|
||||
token = jwt_handler.create_service_token(service_name)
|
||||
logger.debug(f"Created service token for {service_name}")
|
||||
# Use unified JWT handler to create service token
|
||||
token = jwt_handler.create_service_token(
|
||||
service_name=service_name,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
logger.debug(f"Created service token for {service_name}", tenant_id=tenant_id)
|
||||
return token
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -517,6 +517,22 @@ class EnhancedAuthService:
|
||||
detail="Invalid token"
|
||||
)
|
||||
|
||||
# Handle service tokens (used for inter-service communication)
|
||||
if payload.get("type") == "service":
|
||||
logger.debug("Service token verified successfully",
|
||||
service=payload.get("service"),
|
||||
tenant_id=payload.get("tenant_id"))
|
||||
return {
|
||||
"valid": True,
|
||||
"user_id": payload.get("user_id", f"{payload.get('service')}-service"),
|
||||
"email": payload.get("email", f"{payload.get('service')}-service@internal"),
|
||||
"role": payload.get("role", "admin"),
|
||||
"exp": payload.get("exp"),
|
||||
"service": payload.get("service"),
|
||||
"tenant_id": payload.get("tenant_id")
|
||||
}
|
||||
|
||||
# Handle regular user tokens
|
||||
return payload
|
||||
|
||||
except Exception as e:
|
||||
@@ -689,16 +705,22 @@ class EnhancedAuthService:
|
||||
error=str(e))
|
||||
return False
|
||||
|
||||
async def _get_service_token(self) -> str:
|
||||
async def _get_service_token(self, tenant_id: Optional[str] = None) -> str:
|
||||
"""
|
||||
Get service token for inter-service communication.
|
||||
This is used to fetch subscription data from tenant service.
|
||||
|
||||
Args:
|
||||
tenant_id: Optional tenant ID for tenant-scoped service operations
|
||||
|
||||
Returns:
|
||||
JWT service token
|
||||
"""
|
||||
try:
|
||||
# Create a proper service token with JWT using SecurityManager
|
||||
service_token = SecurityManager.create_service_token("auth-service")
|
||||
service_token = SecurityManager.create_service_token("auth-service", tenant_id)
|
||||
|
||||
logger.debug("Generated service token for tenant service communication")
|
||||
logger.debug("Generated service token for tenant service communication", tenant_id=tenant_id)
|
||||
return service_token
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get service token: {e}")
|
||||
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.deletion_job import DeletionJob as DeletionJobModel
|
||||
from app.repositories.deletion_job_repository import DeletionJobRepository
|
||||
from shared.auth.jwt_handler import JWTHandler
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -145,13 +146,17 @@ class DeletionOrchestrator:
|
||||
Initialize orchestrator
|
||||
|
||||
Args:
|
||||
auth_token: JWT token for service-to-service authentication
|
||||
auth_token: JWT token for service-to-service authentication (deprecated - will be auto-generated)
|
||||
db: Database session for persistence (optional for backward compatibility)
|
||||
"""
|
||||
self.auth_token = auth_token
|
||||
self.auth_token = auth_token # Deprecated: kept for backward compatibility
|
||||
self.db = db
|
||||
self.jobs: Dict[str, DeletionJob] = {} # In-memory cache for active jobs
|
||||
|
||||
# Initialize JWT handler for creating service tokens
|
||||
from app.core.config import settings
|
||||
self.jwt_handler = JWTHandler(settings.JWT_SECRET_KEY, settings.JWT_ALGORITHM)
|
||||
|
||||
async def _save_job_to_db(self, job: DeletionJob) -> None:
|
||||
"""Save or update job to database"""
|
||||
if not self.db:
|
||||
@@ -406,14 +411,18 @@ class DeletionOrchestrator:
|
||||
tenant_id=tenant_id)
|
||||
|
||||
try:
|
||||
# Always create a service token with tenant context for secure service-to-service communication
|
||||
service_token = self.jwt_handler.create_service_token(
|
||||
service_name="auth",
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
headers = {
|
||||
"X-Internal-Service": "auth-service",
|
||||
"Authorization": f"Bearer {service_token}",
|
||||
"X-Service": "auth-service",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
if self.auth_token:
|
||||
headers["Authorization"] = f"Bearer {self.auth_token}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
response = await client.delete(endpoint, headers=headers)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user