New token arch

This commit is contained in:
Urtzi Alfaro
2026-01-10 21:45:37 +01:00
parent cc53037552
commit bf1db7cb9e
26 changed files with 1751 additions and 107 deletions

View File

@@ -406,3 +406,73 @@ def service_only_access(func: Callable) -> Callable:
return await func(*args, **kwargs)
return wrapper
def require_verified_subscription_tier(
allowed_tiers: List[str],
verify_in_database: bool = False
):
"""
Subscription tier enforcement with optional database verification.
Args:
allowed_tiers: List of allowed subscription tiers
verify_in_database: If True, verify against database (for critical operations)
"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
request = kwargs.get('request') or args[0]
# Get tier from gateway-injected header (from verified JWT)
header_tier = request.headers.get("x-subscription-tier", "starter").lower()
if header_tier not in [t.lower() for t in allowed_tiers]:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "subscription_required",
"message": f"This feature requires {', '.join(allowed_tiers)} subscription",
"current_tier": header_tier,
"required_tiers": allowed_tiers
}
)
# For critical operations, verify against database
if verify_in_database:
tenant_id = request.headers.get("x-tenant-id")
if tenant_id:
db_tier = await _verify_subscription_in_database(tenant_id)
if db_tier.lower() != header_tier:
logger.error(
"Subscription tier mismatch detected!",
header_tier=header_tier,
db_tier=db_tier,
tenant_id=tenant_id,
user_id=request.headers.get("x-user-id")
)
# Use database tier (authoritative)
if db_tier.lower() not in [t.lower() for t in allowed_tiers]:
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "subscription_verification_failed",
"message": "Subscription tier verification failed"
}
)
return await func(*args, **kwargs)
return wrapper
return decorator
async def _verify_subscription_in_database(tenant_id: str) -> str:
"""
Direct database verification of subscription tier.
Used for critical operations as defense-in-depth.
"""
from shared.clients.subscription_client import SubscriptionClient
client = SubscriptionClient()
subscription = await client.get_subscription(tenant_id)
return subscription.get("plan", "starter")

View File

@@ -125,6 +125,35 @@ class JWTHandler:
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
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:
"""
Create JWT SERVICE token for inter-service communication
✅ FIXED: Service tokens have proper service account structure
"""
to_encode = {
"sub": service_name,
"service": service_name,
"type": "service",
"role": "admin",
"is_service": True
}
# Set expiration
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.debug(f"Created service token for service {service_name}")
return encoded_jwt
def verify_token(self, token: str) -> Optional[Dict[str, Any]]:
"""