From d5ce85f34a3578f393c32b6d6b1adfcde65dad45 Mon Sep 17 00:00:00 2001 From: Zaiban Ali Date: Sat, 7 Dec 2024 13:49:12 +0100 Subject: [PATCH 1/5] feat: implement OAuth logout functionality for keyclock to terminate sso session --- backend/open_webui/apps/webui/routers/auths.py | 17 +++++++++++++++-- backend/open_webui/config.py | 6 ++++++ backend/open_webui/utils/oauth.py | 14 ++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/backend/open_webui/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py index 8f175f366..509ad7483 100644 --- a/backend/open_webui/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -29,7 +29,11 @@ from open_webui.env import ( SRC_LOG_LEVELS, ) from fastapi import APIRouter, Depends, HTTPException, Request, status -from fastapi.responses import Response +from fastapi.responses import RedirectResponse, Response +from open_webui.config import ( + OAUTH_PROVIDER_NAME, + OAUTH_LOGOUT_URL, +) from pydantic import BaseModel from open_webui.utils.misc import parse_duration, validate_email_format from open_webui.utils.utils import ( @@ -498,8 +502,17 @@ async def signup(request: Request, response: Response, form_data: SignupForm): @router.get("/signout") -async def signout(response: Response): +async def signout(request: Request, response: Response): response.delete_cookie("token") + + if OAUTH_PROVIDER_NAME.value == "keycloak" and OAUTH_LOGOUT_URL: + id_token = request.cookies.get("id_token", None) + if id_token: + logout_url = f"{OAUTH_LOGOUT_URL}?id_token_hint={id_token}" + response.delete_cookie("id_token") + return RedirectResponse(url=logout_url) + + # Fall back to the default signout return {"status": True} diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 15d209941..17acc2e02 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -384,6 +384,12 @@ OAUTH_PROVIDER_NAME = PersistentConfig( os.environ.get("OAUTH_PROVIDER_NAME", "SSO"), ) +OAUTH_LOGOUT_URL = PersistentConfig( + "OAUTH_LOGOUT_URL", + "oauth.oidc.logout_url", + os.environ.get("OAUTH_LOGOUT_URL", ""), +) + OAUTH_USERNAME_CLAIM = PersistentConfig( "OAUTH_USERNAME_CLAIM", "oauth.oidc.username_claim", diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 722b1ea73..9030a026b 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -20,6 +20,7 @@ from open_webui.config import ( OAUTH_MERGE_ACCOUNTS_BY_EMAIL, OAUTH_PROVIDERS, ENABLE_OAUTH_ROLE_MANAGEMENT, + OAUTH_PROVIDER_NAME, OAUTH_ROLES_CLAIM, OAUTH_EMAIL_CLAIM, OAUTH_PICTURE_CLAIM, @@ -252,10 +253,19 @@ class OAuthManager: samesite=WEBUI_SESSION_COOKIE_SAME_SITE, secure=WEBUI_SESSION_COOKIE_SECURE, ) - + + if OAUTH_PROVIDER_NAME.value == "keycloak": + id_token = token.get("id_token") + response.set_cookie( + key="id_token", + value=id_token, + httponly=True, + samesite=WEBUI_SESSION_COOKIE_SAME_SITE, + secure=WEBUI_SESSION_COOKIE_SECURE, + ) # Redirect back to the frontend with the JWT token redirect_url = f"{request.base_url}auth#token={jwt_token}" - return RedirectResponse(url=redirect_url) + return RedirectResponse(url=redirect_url, headers=response.headers) oauth_manager = OAuthManager() From 9918ec6246f8f6e20fd13389155b44ae51b33758 Mon Sep 17 00:00:00 2001 From: Zaiban Ali Date: Sat, 7 Dec 2024 15:13:13 +0100 Subject: [PATCH 2/5] feat: update signout functionality to use OpenID configuration for logout URL and remove the logout variable from config --- .../open_webui/apps/webui/routers/auths.py | 28 +++++++++++++------ backend/open_webui/config.py | 6 ---- backend/open_webui/utils/oauth.py | 2 +- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/backend/open_webui/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py index 509ad7483..622e118b2 100644 --- a/backend/open_webui/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -3,6 +3,7 @@ import uuid import time import datetime import logging +import httpx from open_webui.apps.webui.models.auths import ( AddUserForm, @@ -31,8 +32,7 @@ from open_webui.env import ( from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import RedirectResponse, Response from open_webui.config import ( - OAUTH_PROVIDER_NAME, - OAUTH_LOGOUT_URL, + OPENID_PROVIDER_URL, ) from pydantic import BaseModel from open_webui.utils.misc import parse_duration, validate_email_format @@ -504,13 +504,23 @@ async def signup(request: Request, response: Response, form_data: SignupForm): @router.get("/signout") async def signout(request: Request, response: Response): response.delete_cookie("token") - - if OAUTH_PROVIDER_NAME.value == "keycloak" and OAUTH_LOGOUT_URL: - id_token = request.cookies.get("id_token", None) - if id_token: - logout_url = f"{OAUTH_LOGOUT_URL}?id_token_hint={id_token}" - response.delete_cookie("id_token") - return RedirectResponse(url=logout_url) + + id_token = request.cookies.get("id_token", None) + if id_token: + async with httpx.AsyncClient() as client: + try: + openid_config = await client.get(OPENID_PROVIDER_URL.value) + openid_config.raise_for_status() + openid_data = openid_config.json() + end_session_endpoint = openid_data.get("end_session_endpoint") + if end_session_endpoint: + logout_url = f"{end_session_endpoint}?id_token_hint={id_token}" + response.delete_cookie("id_token") + return RedirectResponse(url=logout_url) + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch OpenID configuration") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) # Fall back to the default signout return {"status": True} diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 17acc2e02..15d209941 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -384,12 +384,6 @@ OAUTH_PROVIDER_NAME = PersistentConfig( os.environ.get("OAUTH_PROVIDER_NAME", "SSO"), ) -OAUTH_LOGOUT_URL = PersistentConfig( - "OAUTH_LOGOUT_URL", - "oauth.oidc.logout_url", - os.environ.get("OAUTH_LOGOUT_URL", ""), -) - OAUTH_USERNAME_CLAIM = PersistentConfig( "OAUTH_USERNAME_CLAIM", "oauth.oidc.username_claim", diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 9030a026b..3c2e3a90c 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -254,7 +254,7 @@ class OAuthManager: secure=WEBUI_SESSION_COOKIE_SECURE, ) - if OAUTH_PROVIDER_NAME.value == "keycloak": + if OAUTH_PROVIDER_NAME.value: id_token = token.get("id_token") response.set_cookie( key="id_token", From 48d604a525be76add87319ac4a8764aef173808f Mon Sep 17 00:00:00 2001 From: Zaiban Ali Date: Sat, 7 Dec 2024 15:21:05 +0100 Subject: [PATCH 3/5] feat: enable OAuth signup configuration for signout functionality --- .../open_webui/apps/webui/routers/auths.py | 34 ++++++++++--------- backend/open_webui/utils/oauth.py | 3 +- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/backend/open_webui/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py index 622e118b2..1a671d0bf 100644 --- a/backend/open_webui/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -33,6 +33,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.responses import RedirectResponse, Response from open_webui.config import ( OPENID_PROVIDER_URL, + ENABLE_OAUTH_SIGNUP, ) from pydantic import BaseModel from open_webui.utils.misc import parse_duration, validate_email_format @@ -505,22 +506,23 @@ async def signup(request: Request, response: Response, form_data: SignupForm): async def signout(request: Request, response: Response): response.delete_cookie("token") - id_token = request.cookies.get("id_token", None) - if id_token: - async with httpx.AsyncClient() as client: - try: - openid_config = await client.get(OPENID_PROVIDER_URL.value) - openid_config.raise_for_status() - openid_data = openid_config.json() - end_session_endpoint = openid_data.get("end_session_endpoint") - if end_session_endpoint: - logout_url = f"{end_session_endpoint}?id_token_hint={id_token}" - response.delete_cookie("id_token") - return RedirectResponse(url=logout_url) - except httpx.HTTPStatusError as e: - raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch OpenID configuration") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + if ENABLE_OAUTH_SIGNUP.value: + id_token = request.cookies.get("id_token", None) + if id_token: + async with httpx.AsyncClient() as client: + try: + openid_config = await client.get(OPENID_PROVIDER_URL.value) + openid_config.raise_for_status() + openid_data = openid_config.json() + end_session_endpoint = openid_data.get("end_session_endpoint") + if end_session_endpoint: + logout_url = f"{end_session_endpoint}?id_token_hint={id_token}" + response.delete_cookie("id_token") + return RedirectResponse(url=logout_url) + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch OpenID configuration") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) # Fall back to the default signout return {"status": True} diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index 3c2e3a90c..e7a6b167f 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -20,7 +20,6 @@ from open_webui.config import ( OAUTH_MERGE_ACCOUNTS_BY_EMAIL, OAUTH_PROVIDERS, ENABLE_OAUTH_ROLE_MANAGEMENT, - OAUTH_PROVIDER_NAME, OAUTH_ROLES_CLAIM, OAUTH_EMAIL_CLAIM, OAUTH_PICTURE_CLAIM, @@ -254,7 +253,7 @@ class OAuthManager: secure=WEBUI_SESSION_COOKIE_SECURE, ) - if OAUTH_PROVIDER_NAME.value: + if ENABLE_OAUTH_SIGNUP.value: id_token = token.get("id_token") response.set_cookie( key="id_token", From 899424b3716d7c48ca0c2e3110b72e570dfc91ac Mon Sep 17 00:00:00 2001 From: Zaiban Ali Date: Sun, 8 Dec 2024 04:57:57 +0100 Subject: [PATCH 4/5] feat: refactor signout functionality to use aiohttp for OpenID configuration retrieval --- .../open_webui/apps/webui/routers/auths.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/backend/open_webui/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py index 1a671d0bf..a077f662a 100644 --- a/backend/open_webui/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -3,7 +3,7 @@ import uuid import time import datetime import logging -import httpx +from aiohttp import ClientSession from open_webui.apps.webui.models.auths import ( AddUserForm, @@ -507,24 +507,25 @@ async def signout(request: Request, response: Response): response.delete_cookie("token") if ENABLE_OAUTH_SIGNUP.value: - id_token = request.cookies.get("id_token", None) + id_token = request.cookies.get("id_token") if id_token: - async with httpx.AsyncClient() as client: - try: - openid_config = await client.get(OPENID_PROVIDER_URL.value) - openid_config.raise_for_status() - openid_data = openid_config.json() - end_session_endpoint = openid_data.get("end_session_endpoint") - if end_session_endpoint: - logout_url = f"{end_session_endpoint}?id_token_hint={id_token}" - response.delete_cookie("id_token") - return RedirectResponse(url=logout_url) - except httpx.HTTPStatusError as e: - raise HTTPException(status_code=e.response.status_code, detail="Failed to fetch OpenID configuration") - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + try: + async with ClientSession() as session: + async with session.get(OPENID_PROVIDER_URL.value) as resp: + if resp.status == 200: + openid_data = await resp.json() + logout_url = openid_data.get("end_session_endpoint") + if logout_url: + response.delete_cookie("id_token") + return RedirectResponse(url=f"{logout_url}?id_token_hint={id_token}") + else: + raise HTTPException( + status_code=resp.status, + detail="Failed to fetch OpenID configuration" + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) - # Fall back to the default signout return {"status": True} From a3ca6329215ccc6a8b9a43ef44467add7ec12e67 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Mon, 9 Dec 2024 16:25:56 -0800 Subject: [PATCH 5/5] refac: id_token -> oauth_id_token --- backend/open_webui/apps/webui/routers/auths.py | 12 +++++++----- backend/open_webui/utils/oauth.py | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/backend/open_webui/apps/webui/routers/auths.py b/backend/open_webui/apps/webui/routers/auths.py index a077f662a..8569a2fe5 100644 --- a/backend/open_webui/apps/webui/routers/auths.py +++ b/backend/open_webui/apps/webui/routers/auths.py @@ -507,8 +507,8 @@ async def signout(request: Request, response: Response): response.delete_cookie("token") if ENABLE_OAUTH_SIGNUP.value: - id_token = request.cookies.get("id_token") - if id_token: + oauth_id_token = request.cookies.get("oauth_id_token") + if oauth_id_token: try: async with ClientSession() as session: async with session.get(OPENID_PROVIDER_URL.value) as resp: @@ -516,12 +516,14 @@ async def signout(request: Request, response: Response): openid_data = await resp.json() logout_url = openid_data.get("end_session_endpoint") if logout_url: - response.delete_cookie("id_token") - return RedirectResponse(url=f"{logout_url}?id_token_hint={id_token}") + response.delete_cookie("oauth_id_token") + return RedirectResponse( + url=f"{logout_url}?id_token_hint={oauth_id_token}" + ) else: raise HTTPException( status_code=resp.status, - detail="Failed to fetch OpenID configuration" + detail="Failed to fetch OpenID configuration", ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/open_webui/utils/oauth.py b/backend/open_webui/utils/oauth.py index e7a6b167f..3bab0fc42 100644 --- a/backend/open_webui/utils/oauth.py +++ b/backend/open_webui/utils/oauth.py @@ -252,12 +252,12 @@ class OAuthManager: samesite=WEBUI_SESSION_COOKIE_SAME_SITE, secure=WEBUI_SESSION_COOKIE_SECURE, ) - + if ENABLE_OAUTH_SIGNUP.value: - id_token = token.get("id_token") + oauth_id_token = token.get("id_token") response.set_cookie( - key="id_token", - value=id_token, + key="oauth_id_token", + value=oauth_id_token, httponly=True, samesite=WEBUI_SESSION_COOKIE_SAME_SITE, secure=WEBUI_SESSION_COOKIE_SECURE,