mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-25 05:54:29 +00:00
fix: stop fabricating 1-hour expiry for non-refreshable OAuth tokens
Providers like Slack issue long-lived user tokens whose token response contains neither expires_in nor expires_at and no refresh_token. The _normalize_token_expiry fallback unconditionally stamped such tokens with a 3600s expiry, so roughly an hour later get_oauth_token triggered a refresh that could never succeed (there is no refresh token) and the failed refresh deleted the OAuth session, forcing the user to re-authenticate every hour. Only apply the conservative 1-hour fallback when the token actually carries a refresh_token so the proactive refresh can rotate it. A token with no expiry metadata and no refresh_token is long-lived by definition, so stamp it with a one-year expiry instead (the sessions table requires a non-null expires_at). Tokens whose provider declares a real expiry but issues no refresh token are intentionally unaffected: those genuinely expire and re-authentication is the only recovery. Fixes #26141
This commit is contained in:
parent
10724d057a
commit
d4101ab0b2
1 changed files with 25 additions and 8 deletions
|
|
@ -188,6 +188,10 @@ async def get_oauth_runtime_config() -> SimpleNamespace:
|
|||
# Matches the value recommended by Authlib's compliance_fix documentation.
|
||||
DEFAULT_TOKEN_EXPIRY_SECONDS = 3600
|
||||
|
||||
# Fallback for tokens without expiry metadata AND without a refresh_token
|
||||
# (e.g. Slack xoxp user tokens): long-lived until revoked, cannot be refreshed.
|
||||
NON_REFRESHABLE_TOKEN_EXPIRY_SECONDS = 365 * 24 * 3600
|
||||
|
||||
|
||||
# Apereo CAS includes client_id in ID token JWS headers; Authlib 1.7/joserfc
|
||||
# rejects unknown headers unless we register the provider extension.
|
||||
|
|
@ -203,8 +207,12 @@ def _normalize_token_expiry(token: dict) -> dict:
|
|||
Resolution order:
|
||||
1. If *expires_at* is already present and non-None, trust it.
|
||||
2. Else if *expires_in* is present and non-None, compute *expires_at*.
|
||||
3. Otherwise fall back to ``DEFAULT_TOKEN_EXPIRY_SECONDS`` and log a
|
||||
warning so operators can identify providers that omit expiration.
|
||||
3. Else if a *refresh_token* is present, fall back to
|
||||
``DEFAULT_TOKEN_EXPIRY_SECONDS`` and log a warning so operators can
|
||||
identify providers that omit expiration.
|
||||
4. Otherwise the token is non-expiring and non-refreshable; use
|
||||
``NON_REFRESHABLE_TOKEN_EXPIRY_SECONDS`` so the refresh path (which
|
||||
would inevitably fail and delete the session) is not triggered.
|
||||
|
||||
Also stamps *issued_at* for auditing.
|
||||
"""
|
||||
|
|
@ -218,12 +226,21 @@ def _normalize_token_expiry(token: dict) -> dict:
|
|||
token['expires_at'] = int(datetime.now().timestamp() + token['expires_in'])
|
||||
return token
|
||||
|
||||
# Neither field present — conservative fallback
|
||||
log.warning(
|
||||
"OAuth token response missing both 'expires_in' and 'expires_at'; "
|
||||
f'defaulting to {DEFAULT_TOKEN_EXPIRY_SECONDS}s from now'
|
||||
)
|
||||
token['expires_at'] = int(datetime.now().timestamp() + DEFAULT_TOKEN_EXPIRY_SECONDS)
|
||||
# Neither field present — conservative fallback, but only if the token
|
||||
# can actually be refreshed; otherwise a short fabricated expiry would
|
||||
# guarantee session deletion once the impossible refresh fails (#26141)
|
||||
if token.get('refresh_token'):
|
||||
log.warning(
|
||||
"OAuth token response missing both 'expires_in' and 'expires_at'; "
|
||||
f'defaulting to {DEFAULT_TOKEN_EXPIRY_SECONDS}s from now'
|
||||
)
|
||||
token['expires_at'] = int(datetime.now().timestamp() + DEFAULT_TOKEN_EXPIRY_SECONDS)
|
||||
else:
|
||||
log.info(
|
||||
"OAuth token response missing 'expires_in', 'expires_at' and 'refresh_token'; "
|
||||
'treating token as long-lived'
|
||||
)
|
||||
token['expires_at'] = int(datetime.now().timestamp() + NON_REFRESHABLE_TOKEN_EXPIRY_SECONDS)
|
||||
return token
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue