This commit is contained in:
Timothy Jaeryang Baek
2026-09-19 17:26:18 -04:00
parent c1a35b4691
commit 10d1cfe637
2 changed files with 27 additions and 14 deletions
+1
View File
@@ -1752,6 +1752,7 @@ async def token_exchange(
user=user,
user_data=user_data,
provider=provider,
access_token=form_data.token,
db=db,
)
if await Config.get('oauth.enable_group_mapping'):
+26 -14
View File
@@ -188,6 +188,16 @@ def _default_value(value):
return getattr(value, 'value', value)
def _get_roles_claim(claims: dict, claim: str) -> list | str | int | None:
"""Read nested or flat claims, preserving explicit empty values and zero."""
value = claims
for key in claim.split('.'):
value = value.get(key) if isinstance(value, dict) else None
if not isinstance(value, (list, str, int)):
value = claims.get(claim)
return value if isinstance(value, (list, str, int)) else None
async def get_oauth_runtime_config() -> SimpleNamespace:
keys = [key for key, _default in OAUTH_RUNTIME_CONFIG.values()]
stored = await Config.get_many(*keys)
@@ -1505,7 +1515,7 @@ class OAuthManager:
log.error(f'Exception during token refresh for provider {provider}: {e}')
return None
async def get_user_role(self, user, user_data):
async def get_user_role(self, user, user_data, *, access_token: str | None = None):
auth_config = await get_oauth_runtime_config()
user_count = await Users.get_num_users()
if user and user_count == 1:
@@ -1529,18 +1539,15 @@ class OAuthManager:
# Keep existing users at their current role unless the provider sent roles.
role = user.role if user else auth_config.DEFAULT_USER_ROLE
# Next block extracts the roles from the user data, accepting nested claims of any depth
if oauth_claim and oauth_allowed_roles and oauth_admin_roles:
claim_data = user_data
nested_claims = oauth_claim.split('.')
for nested_claim in nested_claims:
claim_data = claim_data.get(nested_claim, {})
# Try flat claim structure as alternative
if not claim_data:
claim_data = user_data.get(oauth_claim, {})
oauth_roles = []
if oauth_claim:
claim_data = _get_roles_claim(user_data, oauth_claim)
if claim_data is None and access_token is not None:
# The exchange endpoint has already validated this token with the provider's userinfo endpoint.
try:
token_claims = jwt.decode(access_token, options={'verify_signature': False})
claim_data = _get_roles_claim(token_claims, oauth_claim)
except jwt.PyJWTError as e:
log.debug('Token exchange: cannot decode token claims: %s', e)
if isinstance(claim_data, list):
oauth_roles = claim_data
@@ -1553,6 +1560,10 @@ class OAuthManager:
elif isinstance(claim_data, int):
oauth_roles = [str(claim_data)]
if access_token is not None and not oauth_roles and oauth_allowed_roles and '*' not in oauth_allowed_roles:
log.warning('Token exchange denied: no readable roles claim in userinfo or the token')
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED)
log.debug('Oauth Roles claim: %s', oauth_claim)
log.debug('User roles from oauth: %s', oauth_roles)
log.debug('Accepted user roles: %s', oauth_allowed_roles)
@@ -1599,9 +1610,10 @@ class OAuthManager:
user_data,
provider,
*,
access_token: str | None = None,
db=None,
):
determined_role = await self.get_user_role(user, user_data)
determined_role = await self.get_user_role(user, user_data, access_token=access_token)
if user.role == determined_role:
return user