From 8a366706933e908ff002a1faed28d749a82d3db4 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Tue, 30 Jan 2024 15:02:15 -0800 Subject: [PATCH 1/5] (feat) oauth msft + google --- litellm/proxy/proxy_server.py | 295 +++++++++++++++++++++++----------- 1 file changed, 202 insertions(+), 93 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 4a00db6570d..c03022b2baf 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,235 @@ 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) + return result - 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}" - ) - # 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"] - - # 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) - - # 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 + 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, ) - key = response["token"] # type: ignore - user_id = response["user_id"] # type: ignore - litellm_dashboard_ui = "https://litellm-dashboard.vercel.app/" + 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) + return result - # 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) +# @app.get("/google-callback", tags=["experimental"], response_model=GenerateKeyResponse) +# async def google_callback(code: str, request: Request): +# import httpx - 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) +# GOOGLE_REDIRECT_URI = os.getenv("PROXY_BASE_URL") +# if GOOGLE_REDIRECT_URI 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" +# 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" +# ) + +# GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") +# if GOOGLE_CLIENT_SECRET is None: +# GOOGLE_CLIENT_SECRET = "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" + +# verbose_proxy_logger.info( +# f"/google-callback\n GOOGLE_REDIRECT_URI: {GOOGLE_REDIRECT_URI}\n GOOGLE_CLIENT_ID: {GOOGLE_CLIENT_ID}" +# ) +# # 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"] + +# # 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) + +# # 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) @router.get( From 8836bd0d70d8c2b8a2a6253a56e7ecd4ed134d27 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Tue, 30 Jan 2024 15:12:03 -0800 Subject: [PATCH 2/5] (ui) use new /sso endpoint --- ui/litellm-dashboard/src/components/user_dashboard.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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; From 49a16662bf24bb2beff529a7483a9e84c8ac9bf3 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Tue, 30 Jan 2024 15:18:50 -0800 Subject: [PATCH 3/5] (feat) working sso - google, msft --- litellm/proxy/proxy_server.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c03022b2baf..8e44bc0b12e 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -2996,7 +2996,6 @@ async def auth_callback(request: Request): client_secret=google_client_secret, ) result = await google_sso.verify_and_process(request) - return result elif microsoft_client_id is not None: from fastapi_sso.sso.microsoft import MicrosoftSSO @@ -3026,7 +3025,34 @@ async def auth_callback(request: Request): allow_insecure_http=True, ) result = await microsoft_sso.verify_and_process(request) - return result + + # 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 + ) + + 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) # @app.get("/google-callback", tags=["experimental"], response_model=GenerateKeyResponse) From 7ecfb5c368a94d44a58134f96da2925574e0ac74 Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Tue, 30 Jan 2024 15:20:40 -0800 Subject: [PATCH 4/5] (chore) cleanup proxy server --- litellm/proxy/proxy_server.py | 96 ----------------------------------- 1 file changed, 96 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 8e44bc0b12e..f1ec2744cd6 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -3055,102 +3055,6 @@ async def auth_callback(request: Request): return RedirectResponse(url=litellm_dashboard_ui) -# @app.get("/google-callback", tags=["experimental"], response_model=GenerateKeyResponse) -# async def google_callback(code: str, request: Request): -# import httpx - -# GOOGLE_REDIRECT_URI = os.getenv("PROXY_BASE_URL") -# if GOOGLE_REDIRECT_URI 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" -# 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" -# ) - -# GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") -# if GOOGLE_CLIENT_SECRET is None: -# GOOGLE_CLIENT_SECRET = "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" - -# verbose_proxy_logger.info( -# f"/google-callback\n GOOGLE_REDIRECT_URI: {GOOGLE_REDIRECT_URI}\n GOOGLE_CLIENT_ID: {GOOGLE_CLIENT_ID}" -# ) -# # 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"] - -# # 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) - -# # 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) - - @router.get( "/user/info", tags=["user management"], dependencies=[Depends(user_api_key_auth)] ) From 693c6ec40e46564b81ab68cad9e1b042083c279a Mon Sep 17 00:00:00 2001 From: ishaan-jaff Date: Tue, 30 Jan 2024 15:41:01 -0800 Subject: [PATCH 5/5] (docs) ui + sso callbacks --- docs/my-website/docs/proxy/ui.md | 53 +++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 14 deletions(-) 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/