Aug 20 - Session tenant binding
This commit is contained in:
@@ -62,6 +62,20 @@ def jwt_required(f):
|
||||
if payload is None:
|
||||
return api_error('Access token is invalid or expired', 401)
|
||||
|
||||
# MT-21: a token signed for another tenant verifies fine here (shared
|
||||
# SECRET_KEY), so check the tenant claim before 'sub' is resolved
|
||||
# against the bound database. No-op in single-tenant mode.
|
||||
from app.tenancy.session_binding import current_tenant_id
|
||||
tenant_id = current_tenant_id()
|
||||
if tenant_id is not None:
|
||||
token_tid = payload.get('tid')
|
||||
if token_tid != tenant_id:
|
||||
logger.warning(
|
||||
'API tenant mismatch | token_tid=%s resolved=%s endpoint=%s',
|
||||
token_tid, tenant_id, request.endpoint,
|
||||
)
|
||||
return api_error('Access token is not valid for this workspace', 401)
|
||||
|
||||
user_id = int(payload.get('sub', 0))
|
||||
user = db.session.get(User, user_id)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ claims needed to identify the caller:
|
||||
{
|
||||
"sub": "42", # user.id as string
|
||||
"role": "inspector", # user.role
|
||||
"tid": 3, # issuing tenant id (multi-tenant mode only)
|
||||
"iat": 1710000000, # issued-at (UTC epoch)
|
||||
"exp": 1710003600, # expiry (UTC epoch, 60 min later)
|
||||
}
|
||||
@@ -33,6 +34,12 @@ def _secret():
|
||||
return current_app.config['SECRET_KEY']
|
||||
|
||||
|
||||
def _current_tenant_id():
|
||||
"""Resolved tenant id, or None in single-tenant / unbound contexts."""
|
||||
from app.tenancy.session_binding import current_tenant_id
|
||||
return current_tenant_id()
|
||||
|
||||
|
||||
def generate_access_token(user, lifetime_minutes: int = ACCESS_TOKEN_LIFETIME_MINUTES) -> str:
|
||||
"""
|
||||
Create and sign a new access token for the given user.
|
||||
@@ -54,6 +61,14 @@ def generate_access_token(user, lifetime_minutes: int = ACCESS_TOKEN_LIFETIME_MI
|
||||
'iat': now,
|
||||
'exp': now + timedelta(minutes=lifetime_minutes),
|
||||
}
|
||||
# MT-21: bind the token to the issuing tenant. Every tenant is signed with
|
||||
# the same SECRET_KEY, so without this claim a token minted at one tenant
|
||||
# host verifies at another and 'sub' resolves against whichever database
|
||||
# the middleware bound. Omitted in single-tenant mode so token shape is
|
||||
# unchanged there.
|
||||
tid = _current_tenant_id()
|
||||
if tid is not None:
|
||||
payload['tid'] = tid
|
||||
return jwt.encode(payload, _secret(), algorithm='HS256')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user