New token arch
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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]]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user