Repository Public
1# Configuring workload identity federation for Microsoft Azure23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Use Microsoft Azure as a Workload Identity Provider in either of these scenarios:67- **Azure managed identity:** Exchange a Microsoft Entra ID access token issued for a managed identity for a short-lived OpenAI access token.8- **AKS:** Exchange a projected Azure Kubernetes Service (AKS) service account token for a short-lived OpenAI access token.9101112## Azure managed identity1314Azure managed identities let Azure-hosted workloads request Microsoft Entra tokens without storing long-lived secrets. In OpenAI workload identity federation, the managed identity token is the subject token that OpenAI validates before issuing an OpenAI access token.1516### Setting up Azure managed identity1718Create or use a Microsoft Entra application registration that represents the token audience OpenAI should trust. Configure its **Application ID URI**; this URI is the `resource` value your workload requests from Azure Instance Metadata Service (IMDS), and it appears as the `aud` claim in the issued token. For the Microsoft setup steps, see the Microsoft Entra guide to [create a new Entra ID application and service principal](https://learn.microsoft.com/en-au/entra/identity-platform/howto-create-service-principal-portal#register-an-application-with-azure-ad-and-create-a-service-principal).1920The Application ID URI configured in Microsoft Entra ID, the IMDS `resource`21 parameter, the resulting token's `aud` claim, and the OpenAI Workload Identity22 Provider audience must all match.2324[Create](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/manage-user-assigned-managed-identities-azure-portal?pivots=identity-mi-methods-azp) a managed identity, then [assign](https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm#user-assigned-managed-identity) that managed identity to the Azure resource running your application, such as a virtual machine. The resource must be able to call IMDS at runtime. For Azure setup details, see Microsoft's [managed identities overview](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) and the relevant Azure resource documentation for assigning the identity.2526### Getting an Azure managed identity token2728From the Azure resource with the managed identity assigned, request a token from IMDS with the Application ID URI as the `resource` parameter. This token is the subject token that OpenAI exchanges for an OpenAI-issued access token.2930```bash31APPLICATION_ID_URI="api://<application-client-id>"3233TOKEN=$(curl -sS -G -H "Metadata: true" \34 "http://169.254.169.254/metadata/identity/oauth2/token" \35 --data-urlencode "api-version=2018-02-01" \36 --data-urlencode "resource=${APPLICATION_ID_URI}" \37 | jq -r .access_token)38export TOKEN39```4041If the resource has multiple user-assigned managed identities, add the `client_id`, `object_id`, or `msi_res_id` query parameter for the managed identity you want to use. Microsoft documents the IMDS token request parameters in [Use managed identities on a virtual machine to acquire access token](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/how-to-use-vm-token).4243### Verify the token4445Before configuring workload identity federation, export the Microsoft Entra token as `TOKEN`, then run this script locally to inspect its claims:4647```javascript48const parts = process.env.TOKEN?.split(".") ?? [];49if (parts.length !== 3) {50 throw new Error("Expected a compact JWT with three segments");51}52if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {53 throw new Error("JWT payload is not valid Base64URL");54}5556const bytes = Buffer.from(parts[1], "base64url");57if (bytes.toString("base64url") !== parts[1]) {58 throw new Error("JWT payload is not valid Base64URL");59}60const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);61const claims = JSON.parse(decoded);62if (claims === null || Array.isArray(claims) || typeof claims !== "object") {63 throw new Error("JWT payload is not a JSON object");64}65console.log(decoded);66```6768```python69import base6470import json71import os72import re737475def reject_non_json_constant(value):76 raise ValueError(f"JWT payload contains non-JSON constant: {value}")777879parts = os.environ.get("TOKEN", "").split(".")80if len(parts) != 3:81 raise ValueError("Expected a compact JWT with three segments")8283payload = parts[1]84if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:85 raise ValueError("JWT payload is not valid Base64URL")86padded_payload = payload + "=" * (-len(payload) % 4)87decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)88if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:89 raise ValueError("JWT payload is not valid Base64URL")90decoded_text = decoded.decode("utf-8")91claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)92if not isinstance(claims, dict):93 raise ValueError("JWT payload is not a JSON object")94print(decoded_text)95```9697```go98package main99100import (101 "bytes"102 "encoding/base64"103 "encoding/json"104 "fmt"105 "os"106 "strings"107 "unicode/utf8"108)109110func decodeSegment(segment string) (json.RawMessage, error) {111 if !isBase64URLSegment(segment) {112 return nil, fmt.Errorf("JWT segment is not valid Base64URL")113 }114 decoded, err := base64.RawURLEncoding.DecodeString(segment)115 if err != nil {116 return nil, err117 }118 if base64.RawURLEncoding.EncodeToString(decoded) != segment {119 return nil, fmt.Errorf("JWT segment is not valid Base64URL")120 }121 if !utf8.Valid(decoded) {122 return nil, fmt.Errorf("JWT segment is not valid UTF-8")123 }124125 var value json.RawMessage126 if err := json.Unmarshal(decoded, &value); err != nil {127 return nil, err128 }129 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {130 return nil, fmt.Errorf("JWT segment is not a JSON object")131 }132 return value, nil133}134135func isBase64URLSegment(segment string) bool {136 if segment == "" || len(segment)%4 == 1 {137 return false138 }139 for _, character := range segment {140 if !('A' <= character && character <= 'Z') &&141 !('a' <= character && character <= 'z') &&142 !('0' <= character && character <= '9') &&143 character != '-' &&144 character != '_' {145 return false146 }147 }148 return true149}150151func main() {152 parts := strings.Split(os.Getenv("TOKEN"), ".")153 if len(parts) != 3 {154 panic("Expected a compact JWT with three segments")155 }156157 payload, err := decodeSegment(parts[1])158 if err != nil {159 panic(err)160 }161 formatted, err := json.MarshalIndent(payload, "", " ")162 if err != nil {163 panic(err)164 }165 fmt.Println(string(formatted))166}167```168169```java170// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.171import com.fasterxml.jackson.databind.DeserializationFeature;172import com.fasterxml.jackson.databind.JsonNode;173import com.fasterxml.jackson.databind.ObjectMapper;174import java.io.IOException;175import java.nio.ByteBuffer;176import java.nio.charset.CharacterCodingException;177import java.nio.charset.CodingErrorAction;178import java.nio.charset.StandardCharsets;179import java.util.Base64;180181public final class DecodeJwtPayloadExample {182 private static final ObjectMapper JSON =183 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);184185 private DecodeJwtPayloadExample() {}186187 static String decodeUtf8(byte[] bytes) throws IOException {188 try {189 return StandardCharsets.UTF_8190 .newDecoder()191 .onMalformedInput(CodingErrorAction.REPORT)192 .onUnmappableCharacter(CodingErrorAction.REPORT)193 .decode(ByteBuffer.wrap(bytes))194 .toString();195 } catch (CharacterCodingException exception) {196 throw new IOException("JWT segment is not valid UTF-8", exception);197 }198 }199200 static String decodeSegment(String segment) throws IOException {201 if (!isBase64UrlSegment(segment)) {202 throw new IllegalArgumentException("JWT segment is not valid Base64URL");203 }204 byte[] bytes = Base64.getUrlDecoder().decode(segment);205 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {206 throw new IllegalArgumentException("JWT segment is not valid Base64URL");207 }208 String decoded = decodeUtf8(bytes);209 JsonNode value = JSON.readTree(decoded);210 if (value == null || value.isMissingNode() || !value.isObject()) {211 throw new IOException("JWT segment is not a JSON object");212 }213 return decoded;214 }215216 static boolean isBase64UrlSegment(String segment) {217 if (segment.isEmpty() || segment.length() % 4 == 1) {218 return false;219 }220 return segment221 .chars()222 .allMatch(223 character ->224 character >= 'A' && character <= 'Z'225 || character >= 'a' && character <= 'z'226 || character >= '0' && character <= '9'227 || character == '-'228 || character == '_');229 }230231 static String[] requireCompactJwt(String token) {232 if (token == null) {233 throw new IllegalArgumentException("Expected a compact JWT with three segments");234 }235 String[] parts = token.split("\\.", -1);236 if (parts.length != 3) {237 throw new IllegalArgumentException("Expected a compact JWT with three segments");238 }239 return parts;240 }241242 public static void main(String[] args) throws IOException {243 String[] parts = requireCompactJwt(System.getenv("TOKEN"));244 System.out.println(decodeSegment(parts[1]));245 }246}247```248249```csharp250using System.Text;251using System.Text.Json;252253static string DecodeSegment(string segment)254{255 if (256 segment.Length % 4 == 1 ||257 segment.Any(258 character =>259 !(260 character is >= 'A' and <= 'Z' ||261 character is >= 'a' and <= 'z' ||262 character is >= '0' and <= '9' ||263 character is '-' or '_'264 )265 )266 )267 {268 throw new FormatException("JWT segment is not valid Base64URL");269 }270271 byte[] decoded = Convert.FromBase64String(272 segment.Replace('-', '+').Replace('_', '/') +273 new string('=', (4 - segment.Length % 4) % 4)274 );275 string canonicalSegment = Convert276 .ToBase64String(decoded)277 .TrimEnd('=')278 .Replace('+', '-')279 .Replace('/', '_');280 if (canonicalSegment != segment)281 {282 throw new FormatException("JWT segment is not valid Base64URL");283 }284 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);285 using JsonDocument document = JsonDocument.Parse(decodedJson);286 if (document.RootElement.ValueKind is not JsonValueKind.Object)287 {288 throw new FormatException("JWT segment is not a JSON object");289 }290 return decodedJson;291}292293string? token = Environment.GetEnvironmentVariable("TOKEN");294if (token is null)295{296 throw new InvalidOperationException(297 "Expected a compact JWT with three segments"298 );299}300string[] parts = token.Split('.');301if (parts.Length != 3)302{303 throw new InvalidOperationException(304 "Expected a compact JWT with three segments"305 );306}307308Console.WriteLine(DecodeSegment(parts[1]));309```310311```ruby312require "base64"313require "json"314315parts = ENV.fetch("TOKEN", "").split(".", -1)316raise "Expected a compact JWT with three segments" unless parts.length == 3317318unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1319 raise "JWT payload is not valid Base64URL"320end321322begin323 payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))324rescue ArgumentError325 raise "JWT payload is not valid Base64URL"326end327unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]328 raise "JWT payload is not valid Base64URL"329end330331payload.force_encoding(Encoding::UTF_8)332raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?333334claims = JSON.parse(payload)335raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)336337puts(payload)338```339340341This command decodes the JWT payload without verifying the token signature. Use a local decoder for production tokens, and avoid pasting production tokens into third-party tools.342343A decoded Microsoft Entra ID managed identity token will look similar to:344345```json346{347 "iss": "https://login.microsoftonline.com/11111111-2222-3333-4444-555555555555/v2.0",348 "aud": "api://00000000-1111-2222-3333-444444444444",349 "tid": "11111111-2222-3333-4444-555555555555",350 "appid": "22222222-3333-4444-5555-666666666666",351 "oid": "33333333-4444-5555-6666-777777777777",352 "sub": "33333333-4444-5555-6666-777777777777",353 "xms_mirid": "/subscriptions/<subscription-id>/resourcegroups/my-resource-group/providers/Microsoft.Compute/virtualMachines/openai-wif-vm",354 "iat": 1716235422,355 "exp": 1716239022356}357```358359Verify the claims you plan to configure in OpenAI:360361- `iss`: Use the exact issuer value from the token. The issuer may be `https://login.microsoftonline.com/<tenant-id>/v2.0`, but do not assume that suffix.362- `aud`: Must match the Application ID URI, the IMDS `resource` parameter, and the OpenAI Workload Identity Provider audience.363- `tid`: The Microsoft Entra tenant ID.364- `appid`: The managed identity's application/client ID, when present.365- `iat` and `exp`: Check the token's full lifetime, `exp - iat`, in seconds.366367For Codex, set the provider's `max_assertion_lifetime_seconds` to an approved368limit that covers the issuer's expected token-lifetime range. Do not use the369token's remaining validity or assume that every Entra token lasts one hour.370Microsoft documents [variable access-token371lifetimes](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens#token-lifetime)372and does not support [configuring managed-identity token373lifetimes](https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes).374375Managed identity tokens can also contain claims such as `azp`, `oid`, `sub`, or `xms_mirid`. Use the decoded token as the source of truth, and choose claims that identify the exact managed identity and resource boundary you trust.376377Use the decoded payload to compare the token you received with the issuer, audience, and mapping values configured in OpenAI. Most configuration issues are visible in the `iss`, `aud`, `tid`, and managed identity claims before you exchange the token.378379### Setting up workload identity federation380381Create a Workload Identity Provider in OpenAI for the Microsoft Entra ID issuer, then add a service account mapping that matches stable claims from the managed identity token.382383Configure the Workload Identity Provider first, then create the service account mapping.384385#### Set up the Workload Identity Provider3863871. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `azure-managed-identity-prod`. Use **Description**, such as `Production Azure managed identity workloads`, to help admins identify the provider.3883892. **Set the issuer and audience.** Set **OIDC Issuer URL** to the exact value of the token's `iss` claim. Obtain a sample managed identity token and inspect its claims first. For example, the issuer may be `https://login.microsoftonline.com/<tenant-id>/v2.0`. Set **Audience** to the Microsoft Entra Application ID URI you configured, such as `api://<application-client-id>`. This value must match the token's `aud` claim.3903913. **Use Microsoft Entra token verification.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses Microsoft Entra issuer metadata and JWKS to verify the managed identity token.3923934. **Add attribute transformations if you need derived mapping attributes.** For example, enter `managed_identity_client_id` with expression `assertion.appid` to create `openai.managed_identity_client_id` from the managed identity application/client ID claim. The dashboard applies the `openai.` prefix automatically. Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.394395#### Set up the service account mapping3963971. **Create a service account mapping.** Set **Name** to a value that is unique within that Workload Identity Provider, such as `vm-openai-wif`. Use **Description**, such as `Production VM Azure managed identity workload`, to explain which workload can use the mapping.3983992. **Match stable managed identity claims.** Add one **Key** and **Value** row for each claim that must match. If the token contains `appid`, set **Key** to `appid` and **Value** to the managed identity client ID. The `appid` claim identifies the managed identity's application/client ID and is generally the most stable claim for binding a mapping to a specific managed identity. If your token does not contain `appid`, use another stable claim from the decoded token, such as `azp`, `oid`, `sub`, or `xms_mirid`. To bind the mapping to one tenant, also set **Key** to `tid` and **Value** to the Microsoft Entra tenant ID. Decode a sample token from IMDS and use claims that are stable for the managed identity and resource you trust.4004013. **Choose the OpenAI target.** Set **Project** to the OpenAI project that owns the target service account. Set **Service account** to the OpenAI service account the Azure workload can use, such as `azure-managed-identity-prod-openai-wif`.4024034. **Narrow API permissions if needed.** Select appropriate **Permissions** such as `api.model.request` and `api.vector_store.read` to further narrow access tokens minted from this mapping. Leave permissions blank to avoid adding a WIF-specific scope restriction; the token still authorizes as the mapped service account.404405### Using the token in code406407Configure your OpenAI SDK client to request an Azure managed identity token from IMDS and exchange it for an OpenAI-issued access token.408409Set `OPENAI_WIF_AUDIENCE` to the Microsoft Entra Application ID URI configured as the Workload Identity Provider audience. The SDK requests a managed identity token for that audience, exchanges it for an OpenAI-issued access token, and uses the OpenAI token to authenticate API requests.410411Authenticate from an Azure managed identity token412413```javascript414import OpenAI from "openai";415416const imdsEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token";417418const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;419const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;420const audience = process.env.OPENAI_WIF_AUDIENCE;421422if (!identityProviderId || !serviceAccountId || !audience) {423 throw new Error(424 "Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, and OPENAI_WIF_AUDIENCE"425 );426}427428function azureManagedIdentityTokenProvider(resource) {429 return {430 tokenType: "jwt",431 getToken: async () => {432 const url = new URL(imdsEndpoint);433 url.searchParams.set("api-version", "2018-02-01");434 url.searchParams.set("resource", resource);435436 const clientId = process.env.AZURE_CLIENT_ID;437 if (clientId) {438 url.searchParams.set("client_id", clientId);439 }440441 const response = await fetch(url, {442 headers: { Metadata: "true" },443 });444445 if (!response.ok) {446 throw new Error(447 `Azure IMDS token request failed with status ${response.status}.`448 );449 }450451 const body = await response.json();452 if (!body.access_token) {453 throw new Error("Azure IMDS did not return an access token.");454 }455456 return body.access_token;457 },458 };459}460461const client = new OpenAI({462 workloadIdentity: {463 identityProviderId,464 serviceAccountId,465 provider: azureManagedIdentityTokenProvider(audience),466 },467});468469const response = await client.responses.create({470 model: "gpt-5.6-terra",471 input: "Say hello from Azure managed identity workload identity federation.",472});473474console.log(response.output_text);475```476477```python478import json479import os480from urllib.parse import urlencode481from urllib.request import Request, urlopen482483from openai import OpenAI484from openai.auth import SubjectTokenProvider485486IMDS_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"487488489def azure_managed_identity_token_provider(resource: str) -> SubjectTokenProvider:490 def get_token() -> str:491 params = {492 "api-version": "2018-02-01",493 "resource": resource,494 }495496 client_id = os.environ.get("AZURE_CLIENT_ID")497 if client_id:498 params["client_id"] = client_id499500 request = Request(501 f"{IMDS_ENDPOINT}?{urlencode(params)}",502 headers={"Metadata": "true"},503 )504505 with urlopen(request, timeout=10) as response:506 body = json.loads(response.read().decode("utf-8"))507508 token = body.get("access_token", "")509 if not token:510 raise RuntimeError("Azure IMDS did not return an access token.")511 return token512513 return {"token_type": "jwt", "get_token": get_token}514515516client = OpenAI(517 workload_identity={518 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],519 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],520 "provider": azure_managed_identity_token_provider(521 os.environ["OPENAI_WIF_AUDIENCE"]522 ),523 },524)525526response = client.responses.create(527 model="gpt-5.6-terra",528 input="Say hello from Azure managed identity workload identity federation.",529)530531print(response.output_text)532```533534```go535package main536537import (538 "context"539 "encoding/json"540 "fmt"541 "log"542 "net/http"543 "net/url"544 "os"545546 "github.com/openai/openai-go/v3"547 "github.com/openai/openai-go/v3/auth"548 "github.com/openai/openai-go/v3/option"549 "github.com/openai/openai-go/v3/responses"550)551552const azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"553554type azureManagedIdentityTokenProvider struct {555 resource string556}557558func (p azureManagedIdentityTokenProvider) TokenType() auth.SubjectTokenType {559 return auth.SubjectTokenTypeJWT560}561562func (p azureManagedIdentityTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) {563 values := url.Values{}564 values.Set("api-version", "2018-02-01")565 values.Set("resource", p.resource)566 if clientID := os.Getenv("AZURE_CLIENT_ID"); clientID != "" {567 values.Set("client_id", clientID)568 }569570 req, err := http.NewRequestWithContext(ctx, http.MethodGet, azureIMDSEndpoint+"?"+values.Encode(), nil)571 if err != nil {572 return "", &auth.SubjectTokenProviderError{573 Provider: "azure-managed-identity",574 Message: "failed to build Azure IMDS token request",575 Cause: err,576 }577 }578 req.Header.Set("Metadata", "true")579580 resp, err := httpClient.Do(req)581 if err != nil {582 return "", &auth.SubjectTokenProviderError{583 Provider: "azure-managed-identity",584 Message: "failed to request Azure managed identity token",585 Cause: err,586 }587 }588 defer resp.Body.Close()589590 if resp.StatusCode < 200 || resp.StatusCode >= 300 {591 return "", &auth.SubjectTokenProviderError{592 Provider: "azure-managed-identity",593 Message: fmt.Sprintf("Azure IMDS token request failed with status %d", resp.StatusCode),594 }595 }596597 var body struct {598 AccessToken string `json:"access_token"`599 }600 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {601 return "", &auth.SubjectTokenProviderError{602 Provider: "azure-managed-identity",603 Message: "failed to decode Azure IMDS token response",604 Cause: err,605 }606 }607 if body.AccessToken == "" {608 return "", &auth.SubjectTokenProviderError{609 Provider: "azure-managed-identity",610 Message: "Azure IMDS did not return an access token",611 }612 }613614 return body.AccessToken, nil615}616617func main() {618 audience := os.Getenv("OPENAI_WIF_AUDIENCE")619 if audience == "" {620 log.Fatal("Set OPENAI_WIF_AUDIENCE")621 }622623 client := openai.NewClient(624 option.WithWorkloadIdentity(auth.WorkloadIdentity{625 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),626 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),627 Provider: azureManagedIdentityTokenProvider{628 resource: audience,629 },630 }),631 )632633 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{634 Model: openai.ChatModelGPT4_1Mini,635 Input: responses.ResponseNewParamsInputUnion{636 OfString: openai.String("Say hello from Azure managed identity workload identity federation."),637 },638 })639 if err != nil {640 log.Fatal(err)641 }642643 fmt.Println(response.OutputText())644}645```646647```java648import com.fasterxml.jackson.databind.JsonNode;649import com.fasterxml.jackson.databind.json.JsonMapper;650import com.openai.auth.SubjectTokenProvider;651import com.openai.auth.SubjectTokenType;652import com.openai.auth.WorkloadIdentity;653import com.openai.client.OpenAIClient;654import com.openai.client.okhttp.OpenAIOkHttpClient;655import com.openai.core.http.HttpClient;656import com.openai.errors.SubjectTokenProviderException;657import com.openai.models.responses.ResponseCreateParams;658import java.net.URI;659import java.net.URLEncoder;660import java.net.http.HttpRequest;661import java.net.http.HttpResponse;662import java.nio.charset.StandardCharsets;663import java.util.concurrent.CompletableFuture;664665public final class AzureManagedIdentityWorkloadIdentityExample {666 private static final String IMDS_ENDPOINT =667 "http://169.254.169.254/metadata/identity/oauth2/token";668669 private AzureManagedIdentityWorkloadIdentityExample() {}670671 static final class AzureManagedIdentityTokenProvider implements SubjectTokenProvider {672 private final String resource;673674 AzureManagedIdentityTokenProvider(String resource) {675 this.resource = resource;676 }677678 @Override679 public SubjectTokenType tokenType() {680 return SubjectTokenType.JWT;681 }682683 @Override684 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {685 try {686 String query =687 "api-version=2018-02-01&resource="688 + URLEncoder.encode(resource, StandardCharsets.UTF_8);689 String clientId = System.getenv("AZURE_CLIENT_ID");690 if (clientId != null && !clientId.isEmpty()) {691 query += "&client_id=" + URLEncoder.encode(clientId, StandardCharsets.UTF_8);692 }693694 HttpRequest request =695 HttpRequest.newBuilder()696 .uri(URI.create(IMDS_ENDPOINT + "?" + query))697 .header("Metadata", "true")698 .GET()699 .build();700701 HttpResponse<String> response =702 java.net.http.HttpClient.newHttpClient()703 .send(request, HttpResponse.BodyHandlers.ofString());704 if (response.statusCode() < 200 || response.statusCode() >= 300) {705 throw new SubjectTokenProviderException(706 "azure-managed-identity",707 "Azure IMDS token request failed with status " + response.statusCode(),708 null);709 }710711 JsonNode body = jsonMapper.readTree(response.body());712 String token = body.path("access_token").asText();713 if (token.isEmpty()) {714 throw new SubjectTokenProviderException(715 "azure-managed-identity", "Azure IMDS did not return an access token", null);716 }717718 return token;719 } catch (SubjectTokenProviderException e) {720 throw e;721 } catch (Exception e) {722 throw new SubjectTokenProviderException(723 "azure-managed-identity", "failed to request Azure managed identity token", e);724 }725 }726727 @Override728 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {729 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));730 }731 }732733 public static void main(String[] args) {734 WorkloadIdentity workloadIdentity =735 WorkloadIdentity.builder()736 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))737 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))738 .provider(new AzureManagedIdentityTokenProvider(System.getenv("OPENAI_WIF_AUDIENCE")))739 .build();740741 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();742743 ResponseCreateParams params =744 ResponseCreateParams.builder()745 .model("gpt-5.6-terra")746 .input("Say hello from Azure managed identity workload identity federation.")747 .build();748749 client.responses().create(params).output().stream()750 .flatMap(item -> item.message().stream())751 .flatMap(message -> message.content().stream())752 .flatMap(content -> content.outputText().stream())753 .forEach(outputText -> System.out.println(outputText.text()));754 }755}756```757758```ruby759require "json"760require "net/http"761require "openai"762require "uri"763764class AzureManagedIdentityTokenProvider765 include OpenAI::Auth::SubjectTokenProvider766767 IMDS_ENDPOINT = "http://169.254.169.254/metadata/identity/oauth2/token"768769 def initialize(resource:)770 @resource = resource771 end772773 def token_type774 OpenAI::Auth::TokenType::JWT775 end776777 def get_token778 uri = URI(IMDS_ENDPOINT)779 params = {780 "api-version" => "2018-02-01",781 "resource" => @resource782 }783 params["client_id"] = ENV["AZURE_CLIENT_ID"] if ENV["AZURE_CLIENT_ID"]784 uri.query = URI.encode_www_form(params)785786 request = Net::HTTP::Get.new(uri)787 request["Metadata"] = "true"788789 response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: 10) do |http|790 http.request(request)791 end792793 unless response.is_a?(Net::HTTPSuccess)794 raise OpenAI::Errors::SubjectTokenProviderError.new(795 message: "Azure IMDS token request failed with status #{response.code}",796 provider: "azure-managed-identity"797 )798 end799800 token = JSON.parse(response.body).fetch("access_token", "")801 if token.empty?802 raise OpenAI::Errors::SubjectTokenProviderError.new(803 message: "Azure IMDS did not return an access token",804 provider: "azure-managed-identity"805 )806 end807 token808 rescue JSON::ParserError, SystemCallError => e809 raise OpenAI::Errors::SubjectTokenProviderError.new(810 message: "Failed to request Azure managed identity token: #{e.message}",811 provider: "azure-managed-identity",812 cause: e813 )814 end815end816817provider = AzureManagedIdentityTokenProvider.new(818 resource: ENV.fetch("OPENAI_WIF_AUDIENCE")819)820821workload_identity = OpenAI::Auth::WorkloadIdentity.new(822 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),823 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),824 provider: provider825)826827client = OpenAI::Client.new(workload_identity: workload_identity)828829response = client.responses.create(830 model: "gpt-5.6-terra",831 input: "Say hello from Azure managed identity workload identity federation."832)833834puts(response.output_text)835```836837838 839840841 842843844## Azure Kubernetes Service (AKS)845846Use AKS as a Workload Identity Provider by exchanging an AKS-issued projected service account token for a short-lived OpenAI access token.847848AKS workloads can also use Azure Workload Identity to obtain a Microsoft Entra849 ID access token for a managed identity attached to the workload. In that850 configuration, OpenAI validates the Microsoft Entra token rather than the851 projected Kubernetes service account token. Configure OpenAI workload identity852 federation using the steps in [Azure managed853 identity](#azure-managed-identity), and configure Azure Workload Identity854 according to Microsoft's documentation.855856### Setting up AKS857858Retrieve the OIDC issuer URL associated with the AKS cluster:859860```bash861az aks show \862 --name <cluster-name> \863 --resource-group <resource-group> \864 --query "oidcIssuerProfile.issuerUrl" \865 --output tsv866```867868If the issuer URL is empty, enable the AKS OIDC issuer for the cluster. Use the following command:869870```bash871az aks update \872 --resource-group <resource-group> \873 --name <cluster-name> \874 --enable-oidc-issuer875```876877The issuer you configure in the OpenAI Workload Identity Provider must match this issuer URL and the `iss` claim in the projected AKS service account token.878879Use a Kubernetes `ServiceAccount` for the AKS workload that needs to call the OpenAI API. If you do not already have one, create it:880881```bash882kubectl create serviceaccount openai-wif --namespace default883```884885Configure the projected service account token with the audience OpenAI expects and an expiration suitable for your workload. OpenAI validates the token's issuer, signature, audience, and expiration. In this example, the token file is mounted at `/var/run/secrets/tokens/token`, uses the audience `https://api.openai.com/v1`, and expires after 3600 seconds. You may use a different audience if the projected token audience and OpenAI Workload Identity Provider audience match.886887```yaml888apiVersion: v1889kind: Pod890metadata:891 name: openai-wif-app892 namespace: default893spec:894 serviceAccountName: openai-wif895 containers:896 - name: app897 image: my-image898 volumeMounts:899 - name: aks-sa-token900 mountPath: /var/run/secrets/tokens901 readOnly: true902 volumes:903 - name: aks-sa-token904 projected:905 sources:906 - serviceAccountToken:907 path: token908 audience: "https://api.openai.com/v1"909 expirationSeconds: 3600910```911912### Verify the token913914Before configuring workload identity federation, decode a sample projected service account token locally and inspect its claims. From a running pod with the projected token mounted, retrieve the token and export it as `TOKEN`:915916```bash917TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)918export TOKEN919```920921Then run this script:922923```javascript924const parts = process.env.TOKEN?.split(".") ?? [];925if (parts.length !== 3) {926 throw new Error("Expected a compact JWT with three segments");927}928if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {929 throw new Error("JWT payload is not valid Base64URL");930}931932const bytes = Buffer.from(parts[1], "base64url");933if (bytes.toString("base64url") !== parts[1]) {934 throw new Error("JWT payload is not valid Base64URL");935}936const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);937const claims = JSON.parse(decoded);938if (claims === null || Array.isArray(claims) || typeof claims !== "object") {939 throw new Error("JWT payload is not a JSON object");940}941console.log(decoded);942```943944```python945import base64946import json947import os948import re949950951def reject_non_json_constant(value):952 raise ValueError(f"JWT payload contains non-JSON constant: {value}")953954955parts = os.environ.get("TOKEN", "").split(".")956if len(parts) != 3:957 raise ValueError("Expected a compact JWT with three segments")958959payload = parts[1]960if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:961 raise ValueError("JWT payload is not valid Base64URL")962padded_payload = payload + "=" * (-len(payload) % 4)963decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)964if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:965 raise ValueError("JWT payload is not valid Base64URL")966decoded_text = decoded.decode("utf-8")967claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)968if not isinstance(claims, dict):969 raise ValueError("JWT payload is not a JSON object")970print(decoded_text)971```972973```go974package main975976import (977 "bytes"978 "encoding/base64"979 "encoding/json"980 "fmt"981 "os"982 "strings"983 "unicode/utf8"984)985986func decodeSegment(segment string) (json.RawMessage, error) {987 if !isBase64URLSegment(segment) {988 return nil, fmt.Errorf("JWT segment is not valid Base64URL")989 }990 decoded, err := base64.RawURLEncoding.DecodeString(segment)991 if err != nil {992 return nil, err993 }994 if base64.RawURLEncoding.EncodeToString(decoded) != segment {995 return nil, fmt.Errorf("JWT segment is not valid Base64URL")996 }997 if !utf8.Valid(decoded) {998 return nil, fmt.Errorf("JWT segment is not valid UTF-8")999 }10001001 var value json.RawMessage1002 if err := json.Unmarshal(decoded, &value); err != nil {1003 return nil, err1004 }1005 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {1006 return nil, fmt.Errorf("JWT segment is not a JSON object")1007 }1008 return value, nil1009}10101011func isBase64URLSegment(segment string) bool {1012 if segment == "" || len(segment)%4 == 1 {1013 return false1014 }1015 for _, character := range segment {1016 if !('A' <= character && character <= 'Z') &&1017 !('a' <= character && character <= 'z') &&1018 !('0' <= character && character <= '9') &&1019 character != '-' &&1020 character != '_' {1021 return false1022 }1023 }1024 return true1025}10261027func main() {1028 parts := strings.Split(os.Getenv("TOKEN"), ".")1029 if len(parts) != 3 {1030 panic("Expected a compact JWT with three segments")1031 }10321033 payload, err := decodeSegment(parts[1])1034 if err != nil {1035 panic(err)1036 }1037 formatted, err := json.MarshalIndent(payload, "", " ")1038 if err != nil {1039 panic(err)1040 }1041 fmt.Println(string(formatted))1042}1043```10441045```java1046// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.1047import com.fasterxml.jackson.databind.DeserializationFeature;1048import com.fasterxml.jackson.databind.JsonNode;1049import com.fasterxml.jackson.databind.ObjectMapper;1050import java.io.IOException;1051import java.nio.ByteBuffer;1052import java.nio.charset.CharacterCodingException;1053import java.nio.charset.CodingErrorAction;1054import java.nio.charset.StandardCharsets;1055import java.util.Base64;10561057public final class DecodeJwtPayloadExample {1058 private static final ObjectMapper JSON =1059 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);10601061 private DecodeJwtPayloadExample() {}10621063 static String decodeUtf8(byte[] bytes) throws IOException {1064 try {1065 return StandardCharsets.UTF_81066 .newDecoder()1067 .onMalformedInput(CodingErrorAction.REPORT)1068 .onUnmappableCharacter(CodingErrorAction.REPORT)1069 .decode(ByteBuffer.wrap(bytes))1070 .toString();1071 } catch (CharacterCodingException exception) {1072 throw new IOException("JWT segment is not valid UTF-8", exception);1073 }1074 }10751076 static String decodeSegment(String segment) throws IOException {1077 if (!isBase64UrlSegment(segment)) {1078 throw new IllegalArgumentException("JWT segment is not valid Base64URL");1079 }1080 byte[] bytes = Base64.getUrlDecoder().decode(segment);1081 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {1082 throw new IllegalArgumentException("JWT segment is not valid Base64URL");1083 }1084 String decoded = decodeUtf8(bytes);1085 JsonNode value = JSON.readTree(decoded);1086 if (value == null || value.isMissingNode() || !value.isObject()) {1087 throw new IOException("JWT segment is not a JSON object");1088 }1089 return decoded;1090 }10911092 static boolean isBase64UrlSegment(String segment) {1093 if (segment.isEmpty() || segment.length() % 4 == 1) {1094 return false;1095 }1096 return segment1097 .chars()1098 .allMatch(1099 character ->1100 character >= 'A' && character <= 'Z'1101 || character >= 'a' && character <= 'z'1102 || character >= '0' && character <= '9'1103 || character == '-'1104 || character == '_');1105 }11061107 static String[] requireCompactJwt(String token) {1108 if (token == null) {1109 throw new IllegalArgumentException("Expected a compact JWT with three segments");1110 }1111 String[] parts = token.split("\\.", -1);1112 if (parts.length != 3) {1113 throw new IllegalArgumentException("Expected a compact JWT with three segments");1114 }1115 return parts;1116 }11171118 public static void main(String[] args) throws IOException {1119 String[] parts = requireCompactJwt(System.getenv("TOKEN"));1120 System.out.println(decodeSegment(parts[1]));1121 }1122}1123```11241125```csharp1126using System.Text;1127using System.Text.Json;11281129static string DecodeSegment(string segment)1130{1131 if (1132 segment.Length % 4 == 1 ||1133 segment.Any(1134 character =>1135 !(1136 character is >= 'A' and <= 'Z' ||1137 character is >= 'a' and <= 'z' ||1138 character is >= '0' and <= '9' ||1139 character is '-' or '_'1140 )1141 )1142 )1143 {1144 throw new FormatException("JWT segment is not valid Base64URL");1145 }11461147 byte[] decoded = Convert.FromBase64String(1148 segment.Replace('-', '+').Replace('_', '/') +1149 new string('=', (4 - segment.Length % 4) % 4)1150 );1151 string canonicalSegment = Convert1152 .ToBase64String(decoded)1153 .TrimEnd('=')1154 .Replace('+', '-')1155 .Replace('/', '_');1156 if (canonicalSegment != segment)1157 {1158 throw new FormatException("JWT segment is not valid Base64URL");1159 }1160 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);1161 using JsonDocument document = JsonDocument.Parse(decodedJson);1162 if (document.RootElement.ValueKind is not JsonValueKind.Object)1163 {1164 throw new FormatException("JWT segment is not a JSON object");1165 }1166 return decodedJson;1167}11681169string? token = Environment.GetEnvironmentVariable("TOKEN");1170if (token is null)1171{1172 throw new InvalidOperationException(1173 "Expected a compact JWT with three segments"1174 );1175}1176string[] parts = token.Split('.');1177if (parts.Length != 3)1178{1179 throw new InvalidOperationException(1180 "Expected a compact JWT with three segments"1181 );1182}11831184Console.WriteLine(DecodeSegment(parts[1]));1185```11861187```ruby1188require "base64"1189require "json"11901191parts = ENV.fetch("TOKEN", "").split(".", -1)1192raise "Expected a compact JWT with three segments" unless parts.length == 311931194unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 11195 raise "JWT payload is not valid Base64URL"1196end11971198begin1199 payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))1200rescue ArgumentError1201 raise "JWT payload is not valid Base64URL"1202end1203unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]1204 raise "JWT payload is not valid Base64URL"1205end12061207payload.force_encoding(Encoding::UTF_8)1208raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?12091210claims = JSON.parse(payload)1211raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)12121213puts(payload)1214```121512161217This command decodes the JWT payload without verifying the token signature. Use a local decoder for production tokens, and avoid pasting production tokens into third-party tools.12181219A decoded AKS projected service account token will look similar to:12201221```json1222{1223 "iss": "https://eastus.oic.prod-aks.azure.com/11111111-2222-3333-4444-555555555555/22222222-3333-4444-5555-666666666666/",1224 "aud": ["https://api.openai.com/v1"],1225 "sub": "system:serviceaccount:default:openai-wif",1226 "iat": 1716235422,1227 "exp": 1716239022,1228 "kubernetes.io": {1229 "namespace": "default",1230 "serviceaccount": {1231 "name": "openai-wif",1232 "uid": "11111111-2222-3333-4444-555555555555"1233 }1234 }1235}1236```12371238Verify the claims you plan to configure in OpenAI:12391240- `iss`: Must match the AKS issuer URL configured in the OpenAI Workload Identity Provider.1241- `aud`: Must match the projected service account token audience and the OpenAI Workload Identity Provider audience.1242- `sub`: Must match the Kubernetes service account subject you configure in the service account mapping.12431244Use the decoded payload to compare the token you received with the issuer, audience, and mapping values configured in OpenAI. Most configuration issues are visible in the `iss`, `aud`, and `sub` claims before you exchange the token.12451246### Setting up workload identity federation12471248Create a Workload Identity Provider in OpenAI for the AKS issuer, then add a service account mapping that matches attributes from the projected token.12491250Configure the Workload Identity Provider first, then create the service account mapping.12511252#### Set up the Workload Identity Provider125312541. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `azure-aks-prod`. Use **Description**, such as `Production AKS cluster`, to help admins identify the cluster.125512562. **Set the issuer and audience.** Set **OIDC Issuer URL** to the issuer returned by `az aks show --query "oidcIssuerProfile.issuerUrl"`. This value must match the `iss` claim in the projected AKS service account token. Set **Audience** to the same audience configured on the projected service account token volume. In this example, that value is `https://api.openai.com/v1`.125712583. **Use AKS OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses the AKS issuer's OIDC discovery metadata and JWKS to verify the projected service account token.125912604. **Add attribute transformations if you need derived mapping attributes.** For example, enter `aks_subject` with expression `assertion.sub` to create `openai.aks_subject`. The dashboard applies the `openai.` prefix automatically. Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.12611262#### Set up the service account mapping126312641. **Create a service account mapping.** Set **Name** to a value that is unique within that Workload Identity Provider, such as `default-openai-wif`. Use **Description**, such as `Default namespace AKS OpenAI API workload`, to explain which workload can use the mapping.126512662. **Match the AKS service account subject.** Set **Key** to `sub` and **Value** to `system:serviceaccount:default:openai-wif`. For AKS service accounts, the subject format is `system:serviceaccount:<namespace>:<service-account-name>`.12671268 The Workload Identity Provider restricts tokens to the configured AKS issuer. The service account mapping further restricts access to the specified Kubernetes service account subject.126912703. **Choose the OpenAI target.** Set **Project** to the OpenAI project that owns the target service account. Set **Service account** to the OpenAI service account the AKS workload can use, such as `azure-aks-prod-openai-wif`.127112724. **Narrow API permissions if needed.** Select appropriate **Permissions** such as `api.model.request` and `api.vector_store.read` to further narrow access tokens minted from this mapping. Leave permissions blank to avoid adding a WIF-specific scope restriction; the token still authorizes as the mapped service account.12731274### Using the token in code12751276Configure your OpenAI SDK client to read the projected AKS service account token and exchange it for an OpenAI-issued access token.12771278Use the mounted token path, such as `/var/run/secrets/tokens/token`, as the subject token source for the SDK workload identity federation provider. The SDK exchanges that AKS token for an OpenAI-issued access token and uses the OpenAI token to authenticate API requests.12791280The following examples initialize an OpenAI client with a custom subject token provider. The provider reads the projected AKS service account token from the mounted file path and uses it as the subject token for workload identity federation.12811282Authenticate from an AKS projected service account token12831284```javascript1285import { readFile } from "node:fs/promises";1286import OpenAI from "openai";12871288const tokenPath = "/var/run/secrets/tokens/token";1289const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;1290const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;12911292if (!identityProviderId || !serviceAccountId) {1293 throw new Error(1294 "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"1295 );1296}12971298function mountedAksServiceAccountTokenProvider(path) {1299 return {1300 tokenType: "jwt",1301 getToken: async () => {1302 const token = (await readFile(path, "utf8")).trim();1303 if (!token) {1304 throw new Error("The mounted AKS service account token file is empty.");1305 }1306 return token;1307 },1308 };1309}13101311const client = new OpenAI({1312 workloadIdentity: {1313 identityProviderId,1314 serviceAccountId,1315 provider: mountedAksServiceAccountTokenProvider(tokenPath),1316 },1317});13181319const response = await client.responses.create({1320 model: "gpt-5.6-terra",1321 input: "Say hello from AKS workload identity federation.",1322});13231324console.log(response.output_text);1325```13261327```python1328import os1329from pathlib import Path13301331from openai import OpenAI1332from openai.auth import SubjectTokenProvider13331334TOKEN_PATH = "/var/run/secrets/tokens/token"133513361337def mounted_aks_service_account_token_provider(token_path: str) -> SubjectTokenProvider:1338 def get_token() -> str:1339 token = Path(token_path).read_text().strip()1340 if not token:1341 raise RuntimeError("The mounted AKS service account token file is empty.")1342 return token13431344 return {"token_type": "jwt", "get_token": get_token}134513461347client = OpenAI(1348 workload_identity={1349 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],1350 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],1351 "provider": mounted_aks_service_account_token_provider(TOKEN_PATH),1352 },1353)13541355response = client.responses.create(1356 model="gpt-5.6-terra",1357 input="Say hello from AKS workload identity federation.",1358)13591360print(response.output_text)1361```13621363```go1364package main13651366import (1367 "context"1368 "fmt"1369 "log"1370 "os"1371 "strings"13721373 "github.com/openai/openai-go/v3"1374 "github.com/openai/openai-go/v3/auth"1375 "github.com/openai/openai-go/v3/option"1376 "github.com/openai/openai-go/v3/responses"1377)13781379const tokenPath = "/var/run/secrets/tokens/token"13801381type mountedAksServiceAccountTokenProvider struct {1382 path string1383}13841385func (p mountedAksServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {1386 return auth.SubjectTokenTypeJWT1387}13881389func (p mountedAksServiceAccountTokenProvider) GetToken(_ context.Context, _ auth.HTTPDoer) (string, error) {1390 data, err := os.ReadFile(p.path)1391 if err != nil {1392 return "", &auth.SubjectTokenProviderError{1393 Provider: "azure-aks",1394 Message: "failed to read mounted AKS service account token",1395 Cause: err,1396 }1397 }13981399 token := strings.TrimSpace(string(data))1400 if token == "" {1401 return "", &auth.SubjectTokenProviderError{1402 Provider: "azure-aks",1403 Message: "mounted AKS service account token is empty",1404 }1405 }14061407 return token, nil1408}14091410func main() {1411 client := openai.NewClient(1412 option.WithWorkloadIdentity(auth.WorkloadIdentity{1413 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),1414 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),1415 Provider: mountedAksServiceAccountTokenProvider{1416 path: tokenPath,1417 },1418 }),1419 )14201421 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{1422 Model: openai.ChatModelGPT4_1Mini,1423 Input: responses.ResponseNewParamsInputUnion{1424 OfString: openai.String("Say hello from AKS workload identity federation."),1425 },1426 })1427 if err != nil {1428 log.Fatal(err)1429 }14301431 fmt.Println(response.OutputText())1432}1433```14341435```java1436import com.fasterxml.jackson.databind.json.JsonMapper;1437import com.openai.auth.SubjectTokenProvider;1438import com.openai.auth.SubjectTokenType;1439import com.openai.auth.WorkloadIdentity;1440import com.openai.client.OpenAIClient;1441import com.openai.client.okhttp.OpenAIOkHttpClient;1442import com.openai.core.http.HttpClient;1443import com.openai.errors.SubjectTokenProviderException;1444import com.openai.models.responses.ResponseCreateParams;1445import java.nio.file.Files;1446import java.nio.file.Path;1447import java.util.concurrent.CompletableFuture;14481449public final class AzureAksWorkloadIdentityExample {1450 private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";14511452 private AzureAksWorkloadIdentityExample() {}14531454 static final class MountedAksServiceAccountTokenProvider implements SubjectTokenProvider {1455 private final Path tokenPath;14561457 MountedAksServiceAccountTokenProvider(String tokenPath) {1458 this.tokenPath = Path.of(tokenPath);1459 }14601461 @Override1462 public SubjectTokenType tokenType() {1463 return SubjectTokenType.JWT;1464 }14651466 @Override1467 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {1468 String token;1469 try {1470 token = Files.readString(tokenPath).trim();1471 } catch (Exception e) {1472 throw new SubjectTokenProviderException(1473 "azure-aks", "failed to read mounted AKS service account token", e);1474 }14751476 if (token.isEmpty()) {1477 throw new SubjectTokenProviderException(1478 "azure-aks", "mounted AKS service account token is empty", null);1479 }14801481 return token;1482 }14831484 @Override1485 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {1486 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));1487 }1488 }14891490 public static void main(String[] args) {1491 WorkloadIdentity workloadIdentity =1492 WorkloadIdentity.builder()1493 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))1494 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))1495 .provider(new MountedAksServiceAccountTokenProvider(TOKEN_PATH))1496 .build();14971498 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();14991500 ResponseCreateParams params =1501 ResponseCreateParams.builder()1502 .model("gpt-5.6-terra")1503 .input("Say hello from AKS workload identity federation.")1504 .build();15051506 client.responses().create(params).output().stream()1507 .flatMap(item -> item.message().stream())1508 .flatMap(message -> message.content().stream())1509 .flatMap(content -> content.outputText().stream())1510 .forEach(outputText -> System.out.println(outputText.text()));1511 }1512}1513```15141515```ruby1516require "openai"15171518TOKEN_PATH = "/var/run/secrets/tokens/token"15191520class MountedAksServiceAccountTokenProvider1521 include OpenAI::Auth::SubjectTokenProvider15221523 def initialize(token_path:)1524 @token_path = token_path1525 end15261527 def token_type1528 OpenAI::Auth::TokenType::JWT1529 end15301531 def get_token1532 token = File.read(@token_path).strip1533 if token.empty?1534 raise OpenAI::Errors::SubjectTokenProviderError.new(1535 message: "Mounted AKS service account token is empty",1536 provider: "azure-aks"1537 )1538 end1539 token1540 rescue SystemCallError => e1541 raise OpenAI::Errors::SubjectTokenProviderError.new(1542 message: "Failed to read mounted AKS service account token: #{e.message}",1543 provider: "azure-aks",1544 cause: e1545 )1546 end1547end15481549provider = MountedAksServiceAccountTokenProvider.new(token_path: TOKEN_PATH)15501551workload_identity = OpenAI::Auth::WorkloadIdentity.new(1552 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),1553 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),1554 provider: provider1555)15561557client = OpenAI::Client.new(workload_identity: workload_identity)15581559response = client.responses.create(1560 model: "gpt-5.6-terra",1561 input: "Say hello from AKS workload identity federation."1562)15631564puts(response.output_text)1565```1566156715681569## Microsoft Azure best practices15701571- Use managed identities whenever possible. Managed identities provide a simpler and more secure authentication model than distributing credentials manually.1572- Use separate managed identities, Microsoft Entra applications, and OpenAI mappings for different applications and environments. Avoid sharing one identity across development, staging, and production workloads.1573- Restrict accepted audiences. Configure only the audiences required for OpenAI workload identity federation.1574- Use dedicated Microsoft Entra ID applications for security boundaries. Separate applications provide clearer ownership, auditing, and access management.1575- Prefer workload-specific mappings. Match on workload-specific claims rather than broad tenant-wide attributes.1576- Review federated credential configurations regularly. Stale federated credentials can unintentionally continue granting access long after workloads are retired.1577- Separate production and non-production identities. Production workloads should authenticate through distinct federated identities and OpenAI service accounts.