diff --git a/docs/my-website/docs/proxy/ui.md b/docs/my-website/docs/proxy/ui.md index faad799df32..0a19c427c2f 100644 --- a/docs/my-website/docs/proxy/ui.md +++ b/docs/my-website/docs/proxy/ui.md @@ -1,4 +1,6 @@ import Image from '@theme/IdealImage'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; # [BETA] Admin UI @@ -26,22 +28,15 @@ general_settings: allow_user_auth: true ``` -## 2. Setup Google SSO - Use this to Authenticate Team Members to the UI -- Create an Oauth 2.0 Client - +## 2. Setup SSO/Auth for UI - - Navigate to Google `Credenentials` - - Create a new Oauth client ID - - Set the `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` in your Proxy .env -- Set Redirect URL on your Oauth 2.0 Client - - Click on your Oauth 2.0 client on https://console.cloud.google.com/ - - Set a redirect url = `/google-callback` - ``` - https://litellm-production-7002.up.railway.app/google-callback - ``` - -## 3. Required env variables on your Proxy + + + +- Create a new Oauth 2.0 Client on https://console.cloud.google.com/ + +**Required .env variables on your Proxy** ```shell PROXY_BASE_URL="" example PROXY_BASE_URL=https://litellm-production-7002.up.railway.app/ @@ -50,6 +45,36 @@ GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= ``` +- Set Redirect URL on your Oauth 2.0 Client on https://console.cloud.google.com/ + - Set a redirect url = `/sso/callback` + ```shell + https://litellm-production-7002.up.railway.app/sso/callback + ``` + + + + + +- Create a new App Registration on https://portal.azure.com/ +- Create a client Secret for your App Registration + +**Required .env variables on your Proxy** +```shell +PROXY_BASE_URL="" example PROXY_BASE_URL=https://litellm-production-7002.up.railway.app/ + +MICROSOFT_CLIENT_ID="84583a4d-" +MICROSOFT_CLIENT_SECRET="nbk8Q~" +MICROSOFT_TENANT="5a39737 +``` +- Set Redirect URI on your App Registration on https://portal.azure.com/ + - Set a redirect url = `/sso/callback` + ```shell + http://localhost:4000/sso/callback + ``` + + + + ## 4. Use UI 👉 Get Started here: https://litellm-dashboard.vercel.app/ diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4a00db6570d..f1ec2744cd6 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2885,7 +2885,7 @@ async def user_auth(request: Request): return "Email sent!" -@app.get("/google-login/key/generate", tags=["experimental"]) +@app.get("/sso/key/generate", tags=["experimental"]) async def google_login(request: Request): """ Create Proxy API Keys using Google Workspace SSO. Requires setting GOOGLE_REDIRECT_URI in .env @@ -2894,126 +2894,165 @@ async def google_login(request: Request): Example: """ - GOOGLE_REDIRECT_URI = os.getenv("PROXY_BASE_URL") - if GOOGLE_REDIRECT_URI is None: + microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None) + google_client_id = os.getenv("GOOGLE_CLIENT_ID", None) + redirect_url = os.getenv("PROXY_BASE_URL", None) + if redirect_url is None: raise ProxyException( message="PROXY_BASE_URL not set. Set it in .env file", type="auth_error", param="PROXY_BASE_URL", code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - if GOOGLE_REDIRECT_URI.endswith("/"): - GOOGLE_REDIRECT_URI += "google-callback" - else: - GOOGLE_REDIRECT_URI += "/google-callback" - GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") - if GOOGLE_CLIENT_ID is None: - GOOGLE_CLIENT_ID = ( - "246483686424-clje5sggkjma26ilktj6qssakqhoon0m.apps.googleusercontent.com" + if redirect_url.endswith("/"): + redirect_url += "sso/callback" + else: + redirect_url += "/sso/callback" + # Google SSO Auth + if google_client_id is not None: + from fastapi_sso.sso.google import GoogleSSO + + google_client_secret = os.getenv("GOOGLE_CLIENT_SECRET", None) + if google_client_secret is None: + raise ProxyException( + message="GOOGLE_CLIENT_SECRET not set. Set it in .env file", + type="auth_error", + param="GOOGLE_CLIENT_SECRET", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + google_sso = GoogleSSO( + client_id=google_client_id, + client_secret=google_client_secret, + redirect_uri=redirect_url, ) - verbose_proxy_logger.info( - f"In /google-login/key/generate, \nGOOGLE_REDIRECT_URI: {GOOGLE_REDIRECT_URI}\nGOOGLE_CLIENT_ID: {GOOGLE_CLIENT_ID}" - ) - google_auth_url = f"https://accounts.google.com/o/oauth2/auth?client_id={GOOGLE_CLIENT_ID}&redirect_uri={GOOGLE_REDIRECT_URI}&response_type=code&scope=openid%20profile%20email" - return RedirectResponse(url=google_auth_url) + verbose_proxy_logger.info( + f"In /google-login/key/generate, \nGOOGLE_REDIRECT_URI: {redirect_url}\nGOOGLE_CLIENT_ID: {google_client_id}" + ) + + with google_sso: + return await google_sso.get_login_redirect() + + # Microsoft SSO Auth + elif microsoft_client_id is not None: + from fastapi_sso.sso.microsoft import MicrosoftSSO + + microsoft_client_secret = os.getenv("MICROSOFT_CLIENT_SECRET", None) + microsoft_tenant = os.getenv("MICROSOFT_TENANT", None) + if microsoft_client_secret is None: + raise ProxyException( + message="MICROSOFT_CLIENT_SECRET not set. Set it in .env file", + type="auth_error", + param="MICROSOFT_CLIENT_SECRET", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + microsoft_sso = MicrosoftSSO( + client_id=microsoft_client_id, + client_secret=microsoft_client_secret, + tenant=microsoft_tenant, + redirect_uri=redirect_url, + allow_insecure_http=True, + ) + with microsoft_sso: + return await microsoft_sso.get_login_redirect() -@app.get("/google-callback", tags=["experimental"], response_model=GenerateKeyResponse) -async def google_callback(code: str, request: Request): - import httpx +@app.get("/sso/callback", tags=["experimental"]) +async def auth_callback(request: Request): + """Verify login""" + microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None) + google_client_id = os.getenv("GOOGLE_CLIENT_ID", None) - GOOGLE_REDIRECT_URI = os.getenv("PROXY_BASE_URL") - if GOOGLE_REDIRECT_URI is None: + redirect_url = os.getenv("PROXY_BASE_URL", None) + if redirect_url is None: raise ProxyException( message="PROXY_BASE_URL not set. Set it in .env file", type="auth_error", param="PROXY_BASE_URL", code=status.HTTP_500_INTERNAL_SERVER_ERROR, ) - # Add "/google-callback"" to your callback URL - if GOOGLE_REDIRECT_URI.endswith("/"): - GOOGLE_REDIRECT_URI += "google-callback" + if redirect_url.endswith("/"): + redirect_url += "sso/callback" else: - GOOGLE_REDIRECT_URI += "/google-callback" + redirect_url += "/sso/callback" - GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") - if GOOGLE_CLIENT_ID is None: - GOOGLE_CLIENT_ID = ( - "246483686424-clje5sggkjma26ilktj6qssakqhoon0m.apps.googleusercontent.com" + if google_client_id is not None: + from fastapi_sso.sso.google import GoogleSSO + + google_client_secret = os.getenv("GOOGLE_CLIENT_SECRET", None) + if google_client_secret is None: + raise ProxyException( + message="GOOGLE_CLIENT_SECRET not set. Set it in .env file", + type="auth_error", + param="GOOGLE_CLIENT_SECRET", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + google_sso = GoogleSSO( + client_id=google_client_id, + redirect_uri=redirect_url, + client_secret=google_client_secret, ) + result = await google_sso.verify_and_process(request) - GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") - if GOOGLE_CLIENT_SECRET is None: - GOOGLE_CLIENT_SECRET = "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" + elif microsoft_client_id is not None: + from fastapi_sso.sso.microsoft import MicrosoftSSO - verbose_proxy_logger.info( - f"/google-callback\n GOOGLE_REDIRECT_URI: {GOOGLE_REDIRECT_URI}\n GOOGLE_CLIENT_ID: {GOOGLE_CLIENT_ID}" + microsoft_client_secret = os.getenv("MICROSOFT_CLIENT_SECRET", None) + microsoft_tenant = os.getenv("MICROSOFT_TENANT", None) + if microsoft_client_secret is None: + raise ProxyException( + message="MICROSOFT_CLIENT_SECRET not set. Set it in .env file", + type="auth_error", + param="MICROSOFT_CLIENT_SECRET", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + if microsoft_tenant is None: + raise ProxyException( + message="MICROSOFT_TENANT not set. Set it in .env file", + type="auth_error", + param="MICROSOFT_TENANT", + code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + microsoft_sso = MicrosoftSSO( + client_id=microsoft_client_id, + client_secret=microsoft_client_secret, + tenant=microsoft_tenant, + redirect_uri=redirect_url, + allow_insecure_http=True, + ) + result = await microsoft_sso.verify_and_process(request) + + # User is Authe'd in - generate key for the UI to access Proxy + user_id = getattr(result, "email", None) + if user_id is None: + user_id = getattr(result, "first_name", "") + getattr(result, "last_name", "") + + response = await generate_key_helper_fn( + **{"duration": "24hr", "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_id, "team_id": "litellm-dashboard"} # type: ignore ) - # Exchange code for access token - async with httpx.AsyncClient() as client: - token_url = f"https://oauth2.googleapis.com/token" - data = { - "code": code, - "client_id": GOOGLE_CLIENT_ID, - "client_secret": GOOGLE_CLIENT_SECRET, - "redirect_uri": GOOGLE_REDIRECT_URI, - "grant_type": "authorization_code", - } - response = await client.post(token_url, data=data) - # Process the response, extract user info, etc. - if response.status_code == 200: - access_token = response.json()["access_token"] + key = response["token"] # type: ignore + user_id = response["user_id"] # type: ignore + litellm_dashboard_ui = "https://litellm-dashboard.vercel.app/" - # Fetch user info using the access token - async with httpx.AsyncClient() as client: - user_info_url = "https://www.googleapis.com/oauth2/v1/userinfo" - headers = {"Authorization": f"Bearer {access_token}"} - user_info_response = await client.get(user_info_url, headers=headers) + # if user set LITELLM_UI_LINK in .env, use that + litellm_ui_link_in_env = os.getenv("LITELLM_UI_LINK", None) + if litellm_ui_link_in_env is not None: + litellm_dashboard_ui = litellm_ui_link_in_env - # Process user info response - if user_info_response.status_code == 200: - user_info = user_info_response.json() - user_email = user_info.get("email") - user_name = user_info.get("name") - - # we can use user_email on litellm proxy now - - # TODO: Handle user info as needed, for example, store it in a database, authenticate the user, etc. - response = await generate_key_helper_fn( - **{"duration": "24hr", "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": user_email, "team_id": "litellm-dashboard"} # type: ignore - ) - - key = response["token"] # type: ignore - user_id = response["user_id"] # type: ignore - litellm_dashboard_ui = "https://litellm-dashboard.vercel.app/" - - # if user set LITELLM_UI_LINK in .env, use that - litellm_ui_link_in_env = os.getenv("LITELLM_UI_LINK", None) - if litellm_ui_link_in_env is not None: - litellm_dashboard_ui = litellm_ui_link_in_env - - litellm_dashboard_ui += ( - "?userID=" - + user_id - + "&accessToken=" - + key - + "&proxyBaseUrl=" - + os.getenv("PROXY_BASE_URL") - ) - return RedirectResponse(url=litellm_dashboard_ui) - - else: - # Handle user info retrieval error - raise HTTPException( - status_code=user_info_response.status_code, - detail=user_info_response.text, - ) - else: - # Handle the error from the token exchange - raise HTTPException(status_code=response.status_code, detail=response.text) + litellm_dashboard_ui += ( + "?userID=" + + user_id + + "&accessToken=" + + key + + "&proxyBaseUrl=" + + os.getenv("PROXY_BASE_URL") + ) + return RedirectResponse(url=litellm_dashboard_ui) @router.get( diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index fadad17b501..f66d39fba58 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -43,11 +43,10 @@ const UserDashboard = () => { ); } else if (userID == null || accessToken == null) { - // redirect to page: ProxyBaseUrl/google-login/key/generate const baseUrl = proxyBaseUrl.endsWith('/') ? proxyBaseUrl : proxyBaseUrl + '/'; // Now you can construct the full URL - const url = `${baseUrl}google-login/key/generate`; + const url = `${baseUrl}sso/key/generate`; window.location.href = url;