From af2d9db383524d447afeb077d1836dea446a54c8 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 4 Dec 2025 09:49:36 -0800 Subject: [PATCH] fix: manually handle Letta OAuth token exchange for non-standard token_type --- backend/open_webui/utils/oauth.py | 70 +++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 77a2ebd46..2ccc906c6 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -1253,6 +1253,50 @@ class OAuthManager: log.error(f"Error processing profile picture '{picture_url}': {e}") return "/user.png" + async def _letta_token_exchange(self, request, client) -> dict: + """ + Manually exchange authorization code for Letta OAuth token. + Letta returns non-standard token_type: "access_token" instead of "Bearer". + """ + from open_webui.config import LETTA_BASE_URL, LETTA_REDIRECT_URI + + code = request.query_params.get("code") + if not code: + raise ValueError("Missing authorization code") + + # Get code_verifier from session (PKCE) - authlib stores it as _{name}_code_verifier_ + code_verifier = request.session.get("_letta_code_verifier_") or request.session.get("code_verifier") + + token_url = f"{LETTA_BASE_URL.value}/api/oauth/token" + redirect_uri = LETTA_REDIRECT_URI.value or "https://chatlettacom-production.up.railway.app/oauth/letta/callback" + + data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": redirect_uri, + "client_id": client.client_id, + } + if code_verifier: + data["code_verifier"] = code_verifier + + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.post( + token_url, + data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ssl=AIOHTTP_CLIENT_SESSION_SSL, + ) as resp: + if resp.status != 200: + error_text = await resp.text() + log.error(f"Letta token exchange failed: {resp.status} - {error_text}") + raise ValueError(f"Token exchange failed: {error_text}") + + token = await resp.json() + # Fix non-standard token_type + if token.get("token_type") == "access_token": + token["token_type"] = "Bearer" + return token + async def handle_login(self, request, provider): if provider not in OAUTH_PROVIDERS: raise HTTPException(404) @@ -1285,14 +1329,24 @@ class OAuthManager: try: token = await client.authorize_access_token(request, **auth_params) except Exception as e: - detailed_error = _build_oauth_callback_error_message(e) - log.warning( - "OAuth callback error during authorize_access_token for provider %s: %s", - provider, - detailed_error, - exc_info=True, - ) - raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + # Letta returns non-standard token_type: "access_token" instead of "Bearer" + # Handle this by manually exchanging the token + if provider == "letta" and "unsupported_token_type" in str(e): + log.info("Letta OAuth: handling non-standard token_type, manually exchanging token") + try: + token = await self._letta_token_exchange(request, client) + except Exception as letta_e: + log.error(f"Letta manual token exchange failed: {letta_e}") + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + else: + detailed_error = _build_oauth_callback_error_message(e) + log.warning( + "OAuth callback error during authorize_access_token for provider %s: %s", + provider, + detailed_error, + exc_info=True, + ) + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) # Try to get userinfo from the token first, some providers include it there user_data: UserInfo = token.get("userinfo") -- 2.51.2