Skip to content

Commit a76ec2f

Browse files
author
Adrien Bestel
committed
improvement: handle azure workload identity authentication
So far the Azure OpenAI integration was handling authentication using Client ID / Client Secret and Managed identity using the IMDS endpoint which is deprecated in favor of Workload Identity (using the public OAuth2 endpoint of Entra ID). This changeset aims at handling this new authentication type. Note that this requires reading environment variables set by the Azure runtime onto the virtual machine / pod using a workload identity. It also needs to read a file on disk (containing an assertion to use to exchange against a JWT).
1 parent f35fddf commit a76ec2f

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed

src/handlers/handlerUtils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,8 @@ export function constructConfigFromRequestHeaders(
888888
azureAuthMode: requestHeaders[`x-${POWERED_BY}-azure-auth-mode`],
889889
azureManagedClientId:
890890
requestHeaders[`x-${POWERED_BY}-azure-managed-client-id`],
891+
azureWorkloadClientId:
892+
requestHeaders[`x-${POWERED_BY}-azure-workload-client-id`],
891893
azureEntraClientId: requestHeaders[`x-${POWERED_BY}-azure-entra-client-id`],
892894
azureEntraClientSecret:
893895
requestHeaders[`x-${POWERED_BY}-azure-entra-client-secret`],

src/middlewares/requestValidator/schema/config.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
VALID_PROVIDERS,
55
GOOGLE_VERTEX_AI,
66
TRITON,
7+
AZURE_OPEN_AI,
78
} from '../../../globals';
89

910
export const configSchema: any = z
@@ -107,6 +108,7 @@ export const configSchema: any = z
107108
openai_organization: z.string().optional(),
108109
// AzureOpenAI specific
109110
azure_model_name: z.string().optional(),
111+
azure_auth_mode: z.string().optional(),
110112
strict_open_ai_compliance: z.boolean().optional(),
111113
})
112114
.refine(
@@ -123,6 +125,8 @@ export const configSchema: any = z
123125
(value.vertex_service_account_json || value.vertex_project_id);
124126
const hasAWSDetails =
125127
value.aws_access_key_id && value.aws_secret_access_key;
128+
const hasAzureAuth =
129+
value.provider == AZURE_OPEN_AI && value.azure_auth_mode;
126130

127131
return (
128132
hasProviderApiKey ||
@@ -137,7 +141,8 @@ export const configSchema: any = z
137141
value.after_request_hooks ||
138142
value.before_request_hooks ||
139143
value.input_guardrails ||
140-
value.output_guardrails
144+
value.output_guardrails ||
145+
hasAzureAuth
141146
);
142147
},
143148
{

src/providers/azure-openai/api.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ import { ProviderAPIConfig } from '../types';
22
import {
33
getAccessTokenFromEntraId,
44
getAzureManagedIdentityToken,
5+
getAzureWorkloadIdentityToken,
56
} from './utils';
7+
import { env } from 'hono/adapter';
8+
import fs from 'fs';
69

710
const AzureOpenAIAPIConfig: ProviderAPIConfig = {
811
getBaseURL: ({ providerOptions }) => {
912
const { resourceName } = providerOptions;
1013
return `https://${resourceName}.openai.azure.com/openai`;
1114
},
12-
headers: async ({ providerOptions, fn }) => {
15+
headers: async ({ c, providerOptions, fn }) => {
1316
const { apiKey, azureAuthMode } = providerOptions;
1417

1518
if (azureAuthMode === 'entra') {
@@ -39,6 +42,32 @@ const AzureOpenAIAPIConfig: ProviderAPIConfig = {
3942
Authorization: `Bearer ${accessToken}`,
4043
};
4144
}
45+
if (azureAuthMode === 'workload') {
46+
const { azureWorkloadClientId } = providerOptions;
47+
48+
const authorityHost = env(c).AZURE_AUTHORITY_HOST;
49+
const tenantId = env(c).AZURE_TENANT_ID;
50+
const clientId = azureWorkloadClientId || env(c).AZURE_CLIENT_ID;
51+
const federatedTokenFile = env(c).AZURE_FEDERATED_TOKEN_FILE;
52+
53+
if (authorityHost && tenantId && clientId && federatedTokenFile) {
54+
const federatedToken = fs.readFileSync(federatedTokenFile, 'utf8');
55+
56+
if (federatedToken) {
57+
const scope = 'https://cognitiveservices.azure.com/.default';
58+
const accessToken = await getAzureWorkloadIdentityToken(
59+
authorityHost,
60+
tenantId,
61+
clientId,
62+
federatedToken,
63+
scope
64+
);
65+
return {
66+
Authorization: `Bearer ${accessToken}`,
67+
};
68+
}
69+
}
70+
}
4271
const headersObj: Record<string, string> = {
4372
'api-key': `${apiKey}`,
4473
};

src/providers/azure-openai/utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,44 @@ export async function getAzureManagedIdentityToken(
6363
}
6464
}
6565

66+
export async function getAzureWorkloadIdentityToken(
67+
authorityHost: string,
68+
tenantId: string,
69+
clientId: string,
70+
federatedToken: string,
71+
scope = 'https://cognitiveservices.azure.com/.default'
72+
) {
73+
try {
74+
const url = `${authorityHost}/${tenantId}/oauth2/v2.0/token`;
75+
const params = new URLSearchParams({
76+
client_id: clientId,
77+
client_assertion: federatedToken,
78+
client_assertion_type:
79+
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
80+
scope: scope,
81+
grant_type: 'client_credentials',
82+
});
83+
84+
const response = await fetch(url, {
85+
method: 'POST',
86+
headers: {
87+
'Content-Type': 'application/x-www-form-urlencoded',
88+
},
89+
body: params,
90+
});
91+
92+
if (!response.ok) {
93+
const errorMessage = await response.text();
94+
console.log({ message: `Error from Entra ${errorMessage}` });
95+
return undefined;
96+
}
97+
const data: { access_token: string } = await response.json();
98+
return data.access_token;
99+
} catch (error) {
100+
console.log(error);
101+
}
102+
}
103+
66104
export const AzureOpenAIResponseTransform: (
67105
response: Response | ErrorResponse,
68106
responseStatus: number

src/types/requestBody.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ export interface Options {
6060
apiVersion?: string;
6161
adAuth?: string;
6262
azureModelName?: string;
63-
azureAuthMode?: string; // can be entra or managed
63+
azureAuthMode?: string; // can be entra, workload or managed
6464
azureManagedClientId?: string;
65+
azureWorkloadClientId?: string;
6566
azureEntraClientId?: string;
6667
azureEntraClientSecret?: string;
6768
azureEntraTenantId?: string;

0 commit comments

Comments
 (0)