mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-02 02:12:25 +00:00
fix: refresh an expiring OAuth token once when requests race (#30426)
With an OIDC provider that rotates refresh tokens, sending a chat to a system_oauth connection often logged the user's OAuth session out. Two requests reached the refresh at the same time and both sent the same refresh token. The provider rejected the second one with invalid_grant, and Open WebUI deleted the session, so every following request lost its token until the user logged in again. Refreshes now take a per-session lock. A request that waited for another one re-reads the session and uses the token that was just stored, so the provider sees one refresh per rotation. Tested with real sqlite sessions and a rotating mock provider: 2 and 5 concurrent callers now cause 1 refresh, all callers get the new token and the session is kept (before: one refresh per caller, all callers got nothing, session deleted). Single refresh, failed refresh and valid-token paths are unchanged. The lock is per process, so deployments with several workers or replicas can still race across processes. Fixes #30416
This commit is contained in:
parent
d66e5dc403
commit
9db1a518d3
1 changed files with 21 additions and 14 deletions
|
|
@ -1333,6 +1333,7 @@ class OAuthManager:
|
|||
self.app = app
|
||||
|
||||
self._clients = {}
|
||||
self._refresh_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
for name, provider_config in OAUTH_PROVIDERS.items():
|
||||
if 'register' not in provider_config:
|
||||
|
|
@ -1428,23 +1429,29 @@ class OAuthManager:
|
|||
Returns:
|
||||
dict: Refreshed token data, or None if refresh failed
|
||||
"""
|
||||
try:
|
||||
# Perform the actual refresh
|
||||
refreshed_token = await self._perform_token_refresh(session)
|
||||
async with self._refresh_locks.setdefault(session.id, asyncio.Lock()):
|
||||
# Another request may have refreshed while we waited; its refresh token is now spent
|
||||
current_session = await OAuthSessions.get_session_by_id(session.id)
|
||||
if current_session and current_session.token != session.token:
|
||||
return current_session.token
|
||||
|
||||
if refreshed_token:
|
||||
# Update the session with new token data
|
||||
session = await OAuthSessions.update_session_by_id(session.id, refreshed_token)
|
||||
log.info('Successfully refreshed token for session %s', session.id)
|
||||
return session.token
|
||||
else:
|
||||
log.error(f'Failed to refresh token for session {session.id}')
|
||||
try:
|
||||
# Perform the actual refresh
|
||||
refreshed_token = await self._perform_token_refresh(session)
|
||||
|
||||
if refreshed_token:
|
||||
# Update the session with new token data
|
||||
session = await OAuthSessions.update_session_by_id(session.id, refreshed_token)
|
||||
log.info('Successfully refreshed token for session %s', session.id)
|
||||
return session.token
|
||||
else:
|
||||
log.error(f'Failed to refresh token for session {session.id}')
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
log.error(f'Error refreshing token for session {session.id}: {e}')
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
log.error(f'Error refreshing token for session {session.id}: {e}')
|
||||
return None
|
||||
|
||||
async def _perform_token_refresh(self, session) -> dict:
|
||||
"""
|
||||
Perform the actual OAuth token refresh.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue