SPB Git

Repository Public

microsoft-azure.md 54.6 KB 1,577 lines markdown

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.
Files main
Menu

Theme