diff --git a/backend/open_webui/env.py b/backend/open_webui/env.py index 6c41313499..7b58197c62 100644 --- a/backend/open_webui/env.py +++ b/backend/open_webui/env.py @@ -797,6 +797,13 @@ OAUTH_MAX_SESSIONS_PER_USER = int(os.getenv('OAUTH_MAX_SESSIONS_PER_USER', '10') # Token Exchange Configuration # Allows external apps to exchange OAuth tokens for OpenWebUI tokens ENABLE_OAUTH_TOKEN_EXCHANGE = os.getenv('ENABLE_OAUTH_TOKEN_EXCHANGE', 'False').lower() == 'true' +_oauth_token_exchange_rate_limit = (os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT') or '').strip() +OAUTH_TOKEN_EXCHANGE_RATE_LIMIT = ( + int(_oauth_token_exchange_rate_limit) + if _oauth_token_exchange_rate_limit and _oauth_token_exchange_rate_limit.lower() != 'none' + else None +) +OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW = int(os.getenv('OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW', str(60 * 3))) # Back-Channel Logout Configuration # When enabled, exposes POST /oauth/backchannel-logout for IdP-initiated logout diff --git a/backend/open_webui/routers/auths.py b/backend/open_webui/routers/auths.py index 810d0f4c8d..ca81918b37 100644 --- a/backend/open_webui/routers/auths.py +++ b/backend/open_webui/routers/auths.py @@ -25,6 +25,8 @@ from open_webui.env import ( AIOHTTP_CLIENT_SESSION_SSL, ENABLE_INITIAL_ADMIN_SIGNUP, ENABLE_OAUTH_TOKEN_EXCHANGE, + OAUTH_TOKEN_EXCHANGE_RATE_LIMIT, + OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW, WEBUI_AUTH, WEBUI_AUTH_COOKIE_SAME_SITE, WEBUI_AUTH_COOKIE_SECURE, @@ -84,6 +86,17 @@ log = logging.getLogger(__name__) # Forgive us our failed attempts, as we forgive those # who exceed their allotted rate against this gate. signin_rate_limiter = RateLimiter(redis_client=get_redis_client(), limit=5 * 3, window=60 * 3) +# Best-effort throttle only: there is no caller identity before the provider answers, +# and deployments may derive request.client from proxy headers. +token_exchange_rate_limiter = ( + RateLimiter( + redis_client=get_redis_client(), + limit=OAUTH_TOKEN_EXCHANGE_RATE_LIMIT, + window=OAUTH_TOKEN_EXCHANGE_RATE_LIMIT_WINDOW, + ) + if OAUTH_TOKEN_EXCHANGE_RATE_LIMIT is not None + else None +) ADMIN_CONFIG_KEYS = { 'SHOW_ADMIN_DETAILS': 'auth.admin.show', @@ -1518,6 +1531,14 @@ async def token_exchange( detail='Token exchange is disabled', ) + if token_exchange_rate_limiter and token_exchange_rate_limiter.is_limited( + request.client.host if request.client else 'unknown' + ): + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + ) + provider = provider.lower() # Check if provider is configured