feat: Allow Azure OpenAI to authenticate using DefaultAzureCredential

Co-Authored-By: Selene Blok <20491756+selenecodes@users.noreply.github.com>
This commit is contained in:
Timothy Jaeryang Baek 2025-09-17 11:04:47 -05:00
parent 72cd3a54f7
commit caf0a1fbb6
2 changed files with 49 additions and 11 deletions

View file

@ -9,6 +9,8 @@ from aiocache import cached
import requests import requests
from urllib.parse import quote from urllib.parse import quote
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from fastapi import Depends, HTTPException, Request, APIRouter from fastapi import Depends, HTTPException, Request, APIRouter
from fastapi.responses import ( from fastapi.responses import (
FileResponse, FileResponse,
@ -182,12 +184,30 @@ def get_headers_and_cookies(
if oauth_token: if oauth_token:
token = f"{oauth_token.get('access_token', '')}" token = f"{oauth_token.get('access_token', '')}"
elif auth_type in ("azure_ad", "azure_entra_id"):
token = get_azure_entra_id_access_token()
if token: if token:
headers["Authorization"] = f"Bearer {token}" headers["Authorization"] = f"Bearer {token}"
return headers, cookies return headers, cookies
def get_azure_entra_id_access_token():
"""
Get Azure access token using DefaultAzureCredential for Azure OpenAI.
Returns the token string or None if authentication fails.
"""
try:
token_provider = get_bearer_token_provider(
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
)
return token_provider()
except Exception as e:
log.error(f"Error getting Azure access token: {e}")
return None
########################################## ##########################################
# #
# API routes # API routes
@ -641,9 +661,12 @@ async def verify_connection(
) )
if api_config.get("azure", False): if api_config.get("azure", False):
headers["api-key"] = key # Only set api-key header if not using Azure Entra ID authentication
api_version = api_config.get("api_version", "") or "2023-03-15-preview" auth_type = api_config.get("auth_type", "bearer")
if auth_type not in ("azure_ad", "azure_entra_id"):
headers["api-key"] = key
api_version = api_config.get("api_version", "") or "2023-03-15-preview"
async with session.get( async with session.get(
url=f"{url}/openai/models?api-version={api_version}", url=f"{url}/openai/models?api-version={api_version}",
headers=headers, headers=headers,
@ -885,7 +908,12 @@ async def generate_chat_completion(
if api_config.get("azure", False): if api_config.get("azure", False):
api_version = api_config.get("api_version", "2023-03-15-preview") api_version = api_config.get("api_version", "2023-03-15-preview")
request_url, payload = convert_to_azure_payload(url, payload, api_version) request_url, payload = convert_to_azure_payload(url, payload, api_version)
headers["api-key"] = key
# Only set api-key header if not using Azure Entra ID authentication
auth_type = api_config.get("auth_type", "bearer")
if auth_type not in ("azure_ad", "azure_entra_id"):
headers["api-key"] = key
headers["api-version"] = api_version headers["api-version"] = api_version
request_url = f"{request_url}/chat/completions?api-version={api_version}" request_url = f"{request_url}/chat/completions?api-version={api_version}"
else: else:
@ -1058,7 +1086,12 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
if api_config.get("azure", False): if api_config.get("azure", False):
api_version = api_config.get("api_version", "2023-03-15-preview") api_version = api_config.get("api_version", "2023-03-15-preview")
headers["api-key"] = key
# Only set api-key header if not using Azure Entra ID authentication
auth_type = api_config.get("auth_type", "bearer")
if auth_type not in ("azure_ad", "azure_entra_id"):
headers["api-key"] = key
headers["api-version"] = api_version headers["api-version"] = api_version
payload = json.loads(body) payload = json.loads(body)

View file

@ -122,7 +122,7 @@
return; return;
} }
if (!key) { if (!key && !['azure_ad', 'azure_entra_id'].includes(auth_type)) {
loading = false; loading = false;
toast.error($i18n.t('Key is required')); toast.error($i18n.t('Key is required'));
@ -331,6 +331,9 @@
<option value="session">{$i18n.t('Session')}</option> <option value="session">{$i18n.t('Session')}</option>
{#if !direct} {#if !direct}
<option value="system_oauth">{$i18n.t('OAuth')}</option> <option value="system_oauth">{$i18n.t('OAuth')}</option>
{#if azure}
<option value="azure_entra_id">{$i18n.t('Azure Entra ID')}</option>
{/if}
{/if} {/if}
{/if} {/if}
</select> </select>
@ -361,6 +364,12 @@
> >
{$i18n.t('Forwards system user OAuth access token to authenticate')} {$i18n.t('Forwards system user OAuth access token to authenticate')}
</div> </div>
{:else if ['azure_ad', 'azure_entra_id'].includes(auth_type)}
<div
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Uses DefaultAzureCredential to authenticate')}
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -443,7 +452,7 @@
</div> </div>
{/if} {/if}
<div class="flex flex-col w-full"> <div class="flex flex-col w-full mt-2">
<div class="mb-1 flex justify-between"> <div class="mb-1 flex justify-between">
<div <div
class={`mb-0.5 text-xs text-gray-500 class={`mb-0.5 text-xs text-gray-500
@ -499,8 +508,6 @@
{/if} {/if}
</div> </div>
<hr class=" border-gray-100 dark:border-gray-700/10 my-1.5 w-full" />
<div class="flex items-center"> <div class="flex items-center">
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label> <label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
<input <input
@ -528,9 +535,7 @@
</div> </div>
</div> </div>
<hr class=" border-gray-50 dark:border-gray-850 my-2.5 w-full" /> <div class="flex gap-2 mt-2">
<div class="flex gap-2">
<div class="flex flex-col w-full"> <div class="flex flex-col w-full">
<div <div
class={`mb-0.5 text-xs text-gray-500 class={`mb-0.5 text-xs text-gray-500