This commit is contained in:
Timothy Jaeryang Baek 2026-08-24 17:47:10 -04:00
parent 9e7c9360b7
commit a6834f089b
3 changed files with 27 additions and 5 deletions

View file

@ -360,9 +360,14 @@ class UsersTable:
db: AsyncSession | None = None,
) -> UserModel | None:
"""Look up a user by OAuth provider + subject claim."""
sub = str(sub)
async with get_async_db_context(db) as session:
# Subscript, never contains(): on a JSON column contains() degrades to a substring LIKE.
query = select(User).where(User.oauth[provider]['sub'].as_string() == sub)
sub_expr = User.oauth[provider]['sub'].as_string()
query = select(User).where(sub_expr == sub)
# SQLite preserves JSON numeric type here; Postgres ->> already compares numeric JSON as text.
if session.get_bind().dialect.name == 'sqlite' and sub.isdecimal():
query = select(User).where(or_(sub_expr == sub, sub_expr == int(sub)))
row = (await session.execute(query)).scalars().first()
return UserModel.model_validate(row) if row else None
@ -677,7 +682,10 @@ class UsersTable:
if not user:
return None
oauth = dict(user.oauth or {})
oauth[provider] = {'sub': sub}
provider_oauth = oauth.get(provider)
provider_oauth = dict(provider_oauth) if isinstance(provider_oauth, dict) else {}
provider_oauth['sub'] = str(sub)
oauth[provider] = provider_oauth
user.oauth = oauth
await session.commit()
return UserModel.model_validate(user)

View file

@ -1671,6 +1671,7 @@ async def token_exchange(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Token missing required 'sub' claim",
)
sub = str(sub)
email = user_data.get(email_claim, '')
if not email:
@ -1700,7 +1701,13 @@ async def token_exchange(
user = await Users.get_user_by_email(email, db=db)
if user:
# Link the OAuth sub to this user
await Users.update_user_oauth_by_id(user.id, provider, sub, db=db)
user = await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) or user
if user:
provider_oauth = (user.oauth or {}).get(provider) if isinstance(user.oauth, dict) else None
# Lazy repair for legacy rows that stored numeric provider ids as JSON numbers.
if isinstance(provider_oauth, dict) and provider_oauth.get('sub') != sub:
user = await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) or user
if not user:
raise HTTPException(

View file

@ -1929,6 +1929,7 @@ class OAuthManager:
if not sub:
log.warning(f'OAuth callback failed, sub is missing: {user_data}')
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
sub = str(sub)
oauth_data = {}
oauth_data[provider] = {
@ -1993,7 +1994,13 @@ class OAuthManager:
user = await Users.get_user_by_email(email, db=db)
if user:
# Update the user with the new oauth sub
await Users.update_user_oauth_by_id(user.id, provider, sub, db=db)
user = await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) or user
if user:
provider_oauth = (user.oauth or {}).get(provider) if isinstance(user.oauth, dict) else None
# Lazy repair for legacy rows that stored numeric provider ids as JSON numbers.
if isinstance(provider_oauth, dict) and provider_oauth.get('sub') != sub:
user = await Users.update_user_oauth_by_id(user.id, provider, sub, db=db) or user
if user:
user = await self.update_user_role_from_oauth(
@ -2377,7 +2384,7 @@ class OAuthManager:
# 8. Identify users to log out
users_to_logout = []
if sub:
user = await Users.get_user_by_oauth_sub(matched_provider, sub, db=db)
user = await Users.get_user_by_oauth_sub(matched_provider, str(sub), db=db)
if user:
users_to_logout.append(user)