Repository Public
1# Configuring workload identity federation for Google Cloud23> 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 Google Cloud as a Workload Identity Provider in either of these scenarios:67- **Google workload identity:** Exchange a Google-signed OIDC token issued to an attached Google service account for a short-lived OpenAI access token.8- **Google Kubernetes Engine:** Exchange a projected GKE service account token for a short-lived OpenAI access token.9101112## Google workload identity1314Google Cloud workloads can request signed OIDC identity tokens from the Google metadata server without storing long-lived service account keys. In OpenAI workload identity federation, the Google identity token is the subject token that OpenAI validates before issuing an OpenAI access token. This flow works on Compute Engine, Cloud Run, GKE workloads using attached Google service accounts, and other Google-managed runtimes that expose the metadata server identity endpoint.1516### Setting up Google workload identity1718Create a Google service account for the workload that needs to call the OpenAI API. For the full setup flow, see Google's guide to [create service accounts](https://docs.cloud.google.com/iam/docs/service-accounts-create).1920For example, create a service account with the Google Cloud CLI:2122```bash23gcloud iam service-accounts create openai-wif \24 --description="Service account for OpenAI workload identity federation" \25 --display-name="OpenAI workload identity federation"26```2728Create the Compute Engine VM with the service account attached, or attach the service account to the Google Cloud resource running your application. The resource must be able to call the Google metadata server at runtime. For VM setup details, see Google's guide to [create a VM that uses a user-managed service account](https://docs.cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).2930Do not create or download service account keys for this flow. The workload uses the attached service account and the metadata server to request a short-lived OIDC token.3132### Getting a Google identity token3334From the Google Cloud resource with the service account attached, request an OIDC identity token from the metadata server with the configured audience. This token is the subject token that OpenAI exchanges for an OpenAI-issued access token.3536```bash37AUDIENCE="https://api.openai.com/v1"3839TOKEN=$(curl -sS -G -H "Metadata-Flavor: Google" \40 "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity" \41 --data-urlencode "audience=${AUDIENCE}")42export TOKEN43```4445The metadata server returns a Google-signed JWT. For more information about the metadata server identity endpoint, see Google's guide to [verify VM identity](https://docs.cloud.google.com/compute/docs/instances/verifying-instance-identity).4647### Verify the token4849Before configuring workload identity federation, export the Google identity token as `TOKEN`, then run this script locally to inspect its claims:5051```javascript52const parts = process.env.TOKEN?.split(".") ?? [];53if (parts.length !== 3) {54 throw new Error("Expected a compact JWT with three segments");55}56if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {57 throw new Error("JWT payload is not valid Base64URL");58}5960const bytes = Buffer.from(parts[1], "base64url");61if (bytes.toString("base64url") !== parts[1]) {62 throw new Error("JWT payload is not valid Base64URL");63}64const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);65const claims = JSON.parse(decoded);66if (claims === null || Array.isArray(claims) || typeof claims !== "object") {67 throw new Error("JWT payload is not a JSON object");68}69console.log(decoded);70```7172```python73import base6474import json75import os76import re777879def reject_non_json_constant(value):80 raise ValueError(f"JWT payload contains non-JSON constant: {value}")818283parts = os.environ.get("TOKEN", "").split(".")84if len(parts) != 3:85 raise ValueError("Expected a compact JWT with three segments")8687payload = parts[1]88if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:89 raise ValueError("JWT payload is not valid Base64URL")90padded_payload = payload + "=" * (-len(payload) % 4)91decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)92if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:93 raise ValueError("JWT payload is not valid Base64URL")94decoded_text = decoded.decode("utf-8")95claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)96if not isinstance(claims, dict):97 raise ValueError("JWT payload is not a JSON object")98print(decoded_text)99```100101```go102package main103104import (105 "bytes"106 "encoding/base64"107 "encoding/json"108 "fmt"109 "os"110 "strings"111 "unicode/utf8"112)113114func decodeSegment(segment string) (json.RawMessage, error) {115 if !isBase64URLSegment(segment) {116 return nil, fmt.Errorf("JWT segment is not valid Base64URL")117 }118 decoded, err := base64.RawURLEncoding.DecodeString(segment)119 if err != nil {120 return nil, err121 }122 if base64.RawURLEncoding.EncodeToString(decoded) != segment {123 return nil, fmt.Errorf("JWT segment is not valid Base64URL")124 }125 if !utf8.Valid(decoded) {126 return nil, fmt.Errorf("JWT segment is not valid UTF-8")127 }128129 var value json.RawMessage130 if err := json.Unmarshal(decoded, &value); err != nil {131 return nil, err132 }133 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {134 return nil, fmt.Errorf("JWT segment is not a JSON object")135 }136 return value, nil137}138139func isBase64URLSegment(segment string) bool {140 if segment == "" || len(segment)%4 == 1 {141 return false142 }143 for _, character := range segment {144 if !('A' <= character && character <= 'Z') &&145 !('a' <= character && character <= 'z') &&146 !('0' <= character && character <= '9') &&147 character != '-' &&148 character != '_' {149 return false150 }151 }152 return true153}154155func main() {156 parts := strings.Split(os.Getenv("TOKEN"), ".")157 if len(parts) != 3 {158 panic("Expected a compact JWT with three segments")159 }160161 payload, err := decodeSegment(parts[1])162 if err != nil {163 panic(err)164 }165 formatted, err := json.MarshalIndent(payload, "", " ")166 if err != nil {167 panic(err)168 }169 fmt.Println(string(formatted))170}171```172173```java174// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.175import com.fasterxml.jackson.databind.DeserializationFeature;176import com.fasterxml.jackson.databind.JsonNode;177import com.fasterxml.jackson.databind.ObjectMapper;178import java.io.IOException;179import java.nio.ByteBuffer;180import java.nio.charset.CharacterCodingException;181import java.nio.charset.CodingErrorAction;182import java.nio.charset.StandardCharsets;183import java.util.Base64;184185public final class DecodeJwtPayloadExample {186 private static final ObjectMapper JSON =187 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);188189 private DecodeJwtPayloadExample() {}190191 static String decodeUtf8(byte[] bytes) throws IOException {192 try {193 return StandardCharsets.UTF_8194 .newDecoder()195 .onMalformedInput(CodingErrorAction.REPORT)196 .onUnmappableCharacter(CodingErrorAction.REPORT)197 .decode(ByteBuffer.wrap(bytes))198 .toString();199 } catch (CharacterCodingException exception) {200 throw new IOException("JWT segment is not valid UTF-8", exception);201 }202 }203204 static String decodeSegment(String segment) throws IOException {205 if (!isBase64UrlSegment(segment)) {206 throw new IllegalArgumentException("JWT segment is not valid Base64URL");207 }208 byte[] bytes = Base64.getUrlDecoder().decode(segment);209 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {210 throw new IllegalArgumentException("JWT segment is not valid Base64URL");211 }212 String decoded = decodeUtf8(bytes);213 JsonNode value = JSON.readTree(decoded);214 if (value == null || value.isMissingNode() || !value.isObject()) {215 throw new IOException("JWT segment is not a JSON object");216 }217 return decoded;218 }219220 static boolean isBase64UrlSegment(String segment) {221 if (segment.isEmpty() || segment.length() % 4 == 1) {222 return false;223 }224 return segment225 .chars()226 .allMatch(227 character ->228 character >= 'A' && character <= 'Z'229 || character >= 'a' && character <= 'z'230 || character >= '0' && character <= '9'231 || character == '-'232 || character == '_');233 }234235 static String[] requireCompactJwt(String token) {236 if (token == null) {237 throw new IllegalArgumentException("Expected a compact JWT with three segments");238 }239 String[] parts = token.split("\\.", -1);240 if (parts.length != 3) {241 throw new IllegalArgumentException("Expected a compact JWT with three segments");242 }243 return parts;244 }245246 public static void main(String[] args) throws IOException {247 String[] parts = requireCompactJwt(System.getenv("TOKEN"));248 System.out.println(decodeSegment(parts[1]));249 }250}251```252253```csharp254using System.Text;255using System.Text.Json;256257static string DecodeSegment(string segment)258{259 if (260 segment.Length % 4 == 1 ||261 segment.Any(262 character =>263 !(264 character is >= 'A' and <= 'Z' ||265 character is >= 'a' and <= 'z' ||266 character is >= '0' and <= '9' ||267 character is '-' or '_'268 )269 )270 )271 {272 throw new FormatException("JWT segment is not valid Base64URL");273 }274275 byte[] decoded = Convert.FromBase64String(276 segment.Replace('-', '+').Replace('_', '/') +277 new string('=', (4 - segment.Length % 4) % 4)278 );279 string canonicalSegment = Convert280 .ToBase64String(decoded)281 .TrimEnd('=')282 .Replace('+', '-')283 .Replace('/', '_');284 if (canonicalSegment != segment)285 {286 throw new FormatException("JWT segment is not valid Base64URL");287 }288 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);289 using JsonDocument document = JsonDocument.Parse(decodedJson);290 if (document.RootElement.ValueKind is not JsonValueKind.Object)291 {292 throw new FormatException("JWT segment is not a JSON object");293 }294 return decodedJson;295}296297string? token = Environment.GetEnvironmentVariable("TOKEN");298if (token is null)299{300 throw new InvalidOperationException(301 "Expected a compact JWT with three segments"302 );303}304string[] parts = token.Split('.');305if (parts.Length != 3)306{307 throw new InvalidOperationException(308 "Expected a compact JWT with three segments"309 );310}311312Console.WriteLine(DecodeSegment(parts[1]));313```314315```ruby316require "base64"317require "json"318319parts = ENV.fetch("TOKEN", "").split(".", -1)320raise "Expected a compact JWT with three segments" unless parts.length == 3321322unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1323 raise "JWT payload is not valid Base64URL"324end325326begin327 payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))328rescue ArgumentError329 raise "JWT payload is not valid Base64URL"330end331unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]332 raise "JWT payload is not valid Base64URL"333end334335payload.force_encoding(Encoding::UTF_8)336raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?337338claims = JSON.parse(payload)339raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)340341puts(payload)342```343344345This 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.346347A decoded Google metadata server identity token will look similar to:348349```json350{351 "iss": "https://accounts.google.com",352 "aud": "https://api.openai.com/v1",353 "azp": "110123456789012345678",354 "sub": "110123456789012345678",355 "email": "openai-wif@my-project.iam.gserviceaccount.com",356 "email_verified": true,357 "iat": 1716235422,358 "exp": 1716239022359}360```361362Use 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`, `email`, and `sub` claims before you exchange the token.363364### Setting up workload identity federation365366Create a Workload Identity Provider in OpenAI for Google-issued identity tokens, then add a service account mapping that matches stable claims from the token.367368Configure the Workload Identity Provider first, then create the service account mapping.369370#### Set up the Workload Identity Provider3713721. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `google-workload-identity-prod`. Use **Description**, such as `Production Google Cloud workloads`, to help admins identify the provider.3733742. **Set the issuer and audience.** Set **OIDC Issuer URL** to `https://accounts.google.com`. Set **Audience** to the custom audience your workload requests from the Google metadata server, such as `https://api.openai.com/v1`. This value must match the token's `aud` claim.3753763. **Use Google OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses Google's OIDC discovery metadata and JWKS to verify the Google-signed identity token.3773784. **Add attribute transformations if you need derived mapping attributes.** For example, enter `subject` with expression `assertion.sub` to create `openai.subject` from the subject 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.379380#### Set up the service account mapping3813821. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `compute-openai-wif`. Use **Description**, such as `Production Compute Engine OpenAI API workload`, to explain which workload can use the mapping.3833842. **Match stable Google service account claims.** Add one **Key** and **Value** row for each claim that must match. Use `sub` as the primary identity binding because it is stable and unique. You may additionally match `email` for readability.3853863. **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 Google Cloud workload can use, such as `google-workload-identity-prod-openai-wif`.3873884. **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.389390### Using the token in code391392Configure your OpenAI SDK client to request a Google identity token from the metadata server and exchange it for an OpenAI-issued access token.393394Set `OPENAI_WIF_AUDIENCE` to the custom audience configured as the Workload Identity Provider audience. The SDK requests a Google identity token for that audience, exchanges it for an OpenAI-issued access token, and uses the OpenAI token to authenticate API requests.395396Authenticate from a Google metadata server identity token397398```javascript399import OpenAI from "openai";400401const metadataEndpoint =402 "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity";403404const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;405const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;406const audience = process.env.OPENAI_WIF_AUDIENCE;407408if (!identityProviderId || !serviceAccountId || !audience) {409 throw new Error(410 "Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, and OPENAI_WIF_AUDIENCE"411 );412}413414function googleMetadataIdentityTokenProvider(audience) {415 return {416 tokenType: "jwt",417 getToken: async () => {418 const url = new URL(metadataEndpoint);419 url.searchParams.set("audience", audience);420 url.searchParams.set("format", "full");421422 const response = await fetch(url, {423 headers: { "Metadata-Flavor": "Google" },424 });425426 if (!response.ok) {427 throw new Error(428 `Google metadata token request failed with status ${response.status}.`429 );430 }431432 const token = (await response.text()).trim();433 if (!token) {434 throw new Error(435 "Google metadata server did not return an identity token."436 );437 }438439 return token;440 },441 };442}443444const client = new OpenAI({445 workloadIdentity: {446 identityProviderId,447 serviceAccountId,448 provider: googleMetadataIdentityTokenProvider(audience),449 },450});451452const response = await client.responses.create({453 model: "gpt-5.6-terra",454 input: "Say hello from Google Cloud workload identity federation.",455});456457console.log(response.output_text);458```459460```python461import os462from urllib.parse import urlencode463from urllib.request import Request, urlopen464465from openai import OpenAI466from openai.auth import SubjectTokenProvider467468METADATA_ENDPOINT = (469 "http://metadata.google.internal/computeMetadata/v1/instance/"470 "service-accounts/default/identity"471)472473474def google_metadata_identity_token_provider(audience: str) -> SubjectTokenProvider:475 def get_token() -> str:476 request = Request(477 f"{METADATA_ENDPOINT}?{urlencode({'audience': audience, 'format': 'full'})}",478 headers={"Metadata-Flavor": "Google"},479 )480481 with urlopen(request, timeout=10) as response:482 token = response.read().decode("utf-8").strip()483484 if not token:485 raise RuntimeError(486 "Google metadata server did not return an identity token."487 )488 return token489490 return {"token_type": "jwt", "get_token": get_token}491492493client = OpenAI(494 workload_identity={495 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],496 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],497 "provider": google_metadata_identity_token_provider(498 audience=os.environ["OPENAI_WIF_AUDIENCE"]499 ),500 },501)502503response = client.responses.create(504 model="gpt-5.6-terra",505 input="Say hello from Google Cloud workload identity federation.",506)507508print(response.output_text)509```510511```go512package main513514import (515 "context"516 "fmt"517 "io"518 "log"519 "net/http"520 "net/url"521 "os"522 "strings"523524 "github.com/openai/openai-go/v3"525 "github.com/openai/openai-go/v3/auth"526 "github.com/openai/openai-go/v3/option"527 "github.com/openai/openai-go/v3/responses"528)529530const googleMetadataEndpoint = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity"531532type googleMetadataIdentityTokenProvider struct {533 audience string534}535536func (p googleMetadataIdentityTokenProvider) TokenType() auth.SubjectTokenType {537 return auth.SubjectTokenTypeJWT538}539540func (p googleMetadataIdentityTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) {541 values := url.Values{}542 values.Set("audience", p.audience)543 values.Set("format", "full")544545 req, err := http.NewRequestWithContext(ctx, http.MethodGet, googleMetadataEndpoint+"?"+values.Encode(), nil)546 if err != nil {547 return "", &auth.SubjectTokenProviderError{548 Provider: "google-metadata",549 Message: "failed to build Google metadata token request",550 Cause: err,551 }552 }553 req.Header.Set("Metadata-Flavor", "Google")554555 resp, err := httpClient.Do(req)556 if err != nil {557 return "", &auth.SubjectTokenProviderError{558 Provider: "google-metadata",559 Message: "failed to request Google identity token",560 Cause: err,561 }562 }563 defer resp.Body.Close()564565 if resp.StatusCode < 200 || resp.StatusCode >= 300 {566 return "", &auth.SubjectTokenProviderError{567 Provider: "google-metadata",568 Message: fmt.Sprintf("Google metadata token request failed with status %d", resp.StatusCode),569 }570 }571572 data, err := io.ReadAll(resp.Body)573 if err != nil {574 return "", &auth.SubjectTokenProviderError{575 Provider: "google-metadata",576 Message: "failed to read Google metadata token response",577 Cause: err,578 }579 }580581 token := strings.TrimSpace(string(data))582 if token == "" {583 return "", &auth.SubjectTokenProviderError{584 Provider: "google-metadata",585 Message: "Google metadata server did not return an identity token",586 }587 }588589 return token, nil590}591592func main() {593 audience := os.Getenv("OPENAI_WIF_AUDIENCE")594 if audience == "" {595 log.Fatal("Set OPENAI_WIF_AUDIENCE")596 }597598 client := openai.NewClient(599 option.WithWorkloadIdentity(auth.WorkloadIdentity{600 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),601 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),602 Provider: googleMetadataIdentityTokenProvider{603 audience: audience,604 },605 }),606 )607608 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{609 Model: openai.ChatModelGPT4_1Mini,610 Input: responses.ResponseNewParamsInputUnion{611 OfString: openai.String("Say hello from Google Cloud workload identity federation."),612 },613 })614 if err != nil {615 log.Fatal(err)616 }617618 fmt.Println(response.OutputText())619}620```621622```java623import com.fasterxml.jackson.databind.json.JsonMapper;624import com.openai.auth.SubjectTokenProvider;625import com.openai.auth.SubjectTokenType;626import com.openai.auth.WorkloadIdentity;627import com.openai.client.OpenAIClient;628import com.openai.client.okhttp.OpenAIOkHttpClient;629import com.openai.core.http.HttpClient;630import com.openai.errors.SubjectTokenProviderException;631import com.openai.models.responses.ResponseCreateParams;632import java.net.URI;633import java.net.URLEncoder;634import java.net.http.HttpRequest;635import java.net.http.HttpResponse;636import java.nio.charset.StandardCharsets;637import java.util.concurrent.CompletableFuture;638639public final class GoogleWorkloadIdentityExample {640 private static final String METADATA_ENDPOINT =641 "http://metadata.google.internal/computeMetadata/v1/instance/"642 + "service-accounts/default/identity";643644 private GoogleWorkloadIdentityExample() {}645646 static final class GoogleMetadataIdentityTokenProvider implements SubjectTokenProvider {647 private final String audience;648649 GoogleMetadataIdentityTokenProvider(String audience) {650 this.audience = audience;651 }652653 @Override654 public SubjectTokenType tokenType() {655 return SubjectTokenType.JWT;656 }657658 @Override659 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {660 try {661 String query =662 "audience=" + URLEncoder.encode(audience, StandardCharsets.UTF_8) + "&format=full";663 HttpRequest request =664 HttpRequest.newBuilder()665 .uri(URI.create(METADATA_ENDPOINT + "?" + query))666 .header("Metadata-Flavor", "Google")667 .GET()668 .build();669670 HttpResponse<String> response =671 java.net.http.HttpClient.newHttpClient()672 .send(request, HttpResponse.BodyHandlers.ofString());673 if (response.statusCode() < 200 || response.statusCode() >= 300) {674 throw new SubjectTokenProviderException(675 "google-metadata",676 "Google metadata token request failed with status " + response.statusCode(),677 null);678 }679680 String token = response.body().trim();681 if (token.isEmpty()) {682 throw new SubjectTokenProviderException(683 "google-metadata", "Google metadata server did not return an identity token", null);684 }685686 return token;687 } catch (SubjectTokenProviderException e) {688 throw e;689 } catch (Exception e) {690 throw new SubjectTokenProviderException(691 "google-metadata", "failed to request Google identity token", e);692 }693 }694695 @Override696 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {697 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));698 }699 }700701 public static void main(String[] args) {702 WorkloadIdentity workloadIdentity =703 WorkloadIdentity.builder()704 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))705 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))706 .provider(new GoogleMetadataIdentityTokenProvider(System.getenv("OPENAI_WIF_AUDIENCE")))707 .build();708709 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();710711 ResponseCreateParams params =712 ResponseCreateParams.builder()713 .model("gpt-5.6-terra")714 .input("Say hello from Google Cloud workload identity federation.")715 .build();716717 client.responses().create(params).output().stream()718 .flatMap(item -> item.message().stream())719 .flatMap(message -> message.content().stream())720 .flatMap(content -> content.outputText().stream())721 .forEach(outputText -> System.out.println(outputText.text()));722 }723}724```725726```ruby727require "net/http"728require "openai"729require "uri"730731class GoogleMetadataIdentityTokenProvider732 include OpenAI::Auth::SubjectTokenProvider733734 METADATA_ENDPOINT =735 "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity"736737 def initialize(audience:)738 @audience = audience739 end740741 def token_type742 OpenAI::Auth::TokenType::ID743 end744745 def get_token746 uri = URI(METADATA_ENDPOINT)747 uri.query = URI.encode_www_form(748 audience: @audience,749 format: "full"750 )751752 request = Net::HTTP::Get.new(uri)753 request["Metadata-Flavor"] = "Google"754755 response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: 10) do |http|756 http.request(request)757 end758759 unless response.is_a?(Net::HTTPSuccess)760 raise OpenAI::Errors::SubjectTokenProviderError.new(761 message: "Google metadata token request failed with status #{response.code}",762 provider: "google-metadata"763 )764 end765766 token = response.body.strip767 if token.empty?768 raise OpenAI::Errors::SubjectTokenProviderError.new(769 message: "Google metadata server did not return an identity token",770 provider: "google-metadata"771 )772 end773 token774 rescue SystemCallError => e775 raise OpenAI::Errors::SubjectTokenProviderError.new(776 message: "Failed to request Google identity token: #{e.message}",777 provider: "google-metadata",778 cause: e779 )780 end781end782783provider = GoogleMetadataIdentityTokenProvider.new(784 audience: ENV.fetch("OPENAI_WIF_AUDIENCE")785)786787workload_identity = OpenAI::Auth::WorkloadIdentity.new(788 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),789 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),790 provider: provider791)792793client = OpenAI::Client.new(workload_identity: workload_identity)794795response = client.responses.create(796 model: "gpt-5.6-terra",797 input: "Say hello from Google Cloud workload identity federation."798)799800puts(response.output_text)801```802803804 805806807 808809810## Google Kubernetes Engine811812Use Google Kubernetes Engine as a Workload Identity Provider by exchanging a GKE-issued projected service account token for a short-lived OpenAI access token.813814GKE workloads can authenticate using either:815816- A projected Kubernetes service account token issued by the cluster OIDC issuer.817- A Google service account identity token obtained through GKE Workload Identity, where a Kubernetes service account is bound to a Google service account.818819Use projected Kubernetes service account tokens when you want OpenAI to trust the cluster's OIDC issuer directly. Use GKE Workload Identity when your workload already relies on a Google service account identity and you want OpenAI to trust Google-issued identity tokens instead.820821If your GKE workload is configured with GKE Workload Identity and can request822 Google identity tokens from the metadata server, follow the [Google workload823 identity](#google-workload-identity) instructions above instead of the GKE824 projected token flow.825826### Setting up GKE827828These instructions assume a managed GKE cluster. For a self-managed Kubernetes cluster, use the [Kubernetes guide](https://developers.openai.com/api/docs/guides/workload-identity-federation/kubernetes).829830Use a Kubernetes `ServiceAccount` for the GKE workload that needs to call the OpenAI API. If you do not already have one, create it:831832```bash833kubectl create serviceaccount openai-wif --namespace default834```835836Retrieve the issuer URL associated with the GKE cluster:837838```bash839kubectl get --raw /.well-known/openid-configuration | jq -r .issuer840```841842Example output:843844```text845https://container.googleapis.com/v1/projects/my-project/locations/us-central1/clusters/openai-wif846```847848The issuer you configure in the OpenAI Workload Identity Provider must match this issuer URL and the `iss` claim in the projected GKE service account token.849850Configure 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:851852```yaml853apiVersion: v1854kind: Pod855metadata:856 name: openai-wif-app857 namespace: default858spec:859 serviceAccountName: openai-wif860 containers:861 - name: app862 image: my-image863 volumeMounts:864 - name: gke-sa-token865 mountPath: /var/run/secrets/tokens866 readOnly: true867 volumes:868 - name: gke-sa-token869 projected:870 sources:871 - serviceAccountToken:872 path: token873 audience: "https://api.openai.com/v1"874 expirationSeconds: 3600875```876877### Verify the token878879Before 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`:880881```bash882TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)883export TOKEN884```885886Then run this script:887888```javascript889const parts = process.env.TOKEN?.split(".") ?? [];890if (parts.length !== 3) {891 throw new Error("Expected a compact JWT with three segments");892}893if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {894 throw new Error("JWT payload is not valid Base64URL");895}896897const bytes = Buffer.from(parts[1], "base64url");898if (bytes.toString("base64url") !== parts[1]) {899 throw new Error("JWT payload is not valid Base64URL");900}901const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);902const claims = JSON.parse(decoded);903if (claims === null || Array.isArray(claims) || typeof claims !== "object") {904 throw new Error("JWT payload is not a JSON object");905}906console.log(decoded);907```908909```python910import base64911import json912import os913import re914915916def reject_non_json_constant(value):917 raise ValueError(f"JWT payload contains non-JSON constant: {value}")918919920parts = os.environ.get("TOKEN", "").split(".")921if len(parts) != 3:922 raise ValueError("Expected a compact JWT with three segments")923924payload = parts[1]925if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:926 raise ValueError("JWT payload is not valid Base64URL")927padded_payload = payload + "=" * (-len(payload) % 4)928decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)929if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:930 raise ValueError("JWT payload is not valid Base64URL")931decoded_text = decoded.decode("utf-8")932claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)933if not isinstance(claims, dict):934 raise ValueError("JWT payload is not a JSON object")935print(decoded_text)936```937938```go939package main940941import (942 "bytes"943 "encoding/base64"944 "encoding/json"945 "fmt"946 "os"947 "strings"948 "unicode/utf8"949)950951func decodeSegment(segment string) (json.RawMessage, error) {952 if !isBase64URLSegment(segment) {953 return nil, fmt.Errorf("JWT segment is not valid Base64URL")954 }955 decoded, err := base64.RawURLEncoding.DecodeString(segment)956 if err != nil {957 return nil, err958 }959 if base64.RawURLEncoding.EncodeToString(decoded) != segment {960 return nil, fmt.Errorf("JWT segment is not valid Base64URL")961 }962 if !utf8.Valid(decoded) {963 return nil, fmt.Errorf("JWT segment is not valid UTF-8")964 }965966 var value json.RawMessage967 if err := json.Unmarshal(decoded, &value); err != nil {968 return nil, err969 }970 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {971 return nil, fmt.Errorf("JWT segment is not a JSON object")972 }973 return value, nil974}975976func isBase64URLSegment(segment string) bool {977 if segment == "" || len(segment)%4 == 1 {978 return false979 }980 for _, character := range segment {981 if !('A' <= character && character <= 'Z') &&982 !('a' <= character && character <= 'z') &&983 !('0' <= character && character <= '9') &&984 character != '-' &&985 character != '_' {986 return false987 }988 }989 return true990}991992func main() {993 parts := strings.Split(os.Getenv("TOKEN"), ".")994 if len(parts) != 3 {995 panic("Expected a compact JWT with three segments")996 }997998 payload, err := decodeSegment(parts[1])999 if err != nil {1000 panic(err)1001 }1002 formatted, err := json.MarshalIndent(payload, "", " ")1003 if err != nil {1004 panic(err)1005 }1006 fmt.Println(string(formatted))1007}1008```10091010```java1011// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.1012import com.fasterxml.jackson.databind.DeserializationFeature;1013import com.fasterxml.jackson.databind.JsonNode;1014import com.fasterxml.jackson.databind.ObjectMapper;1015import java.io.IOException;1016import java.nio.ByteBuffer;1017import java.nio.charset.CharacterCodingException;1018import java.nio.charset.CodingErrorAction;1019import java.nio.charset.StandardCharsets;1020import java.util.Base64;10211022public final class DecodeJwtPayloadExample {1023 private static final ObjectMapper JSON =1024 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);10251026 private DecodeJwtPayloadExample() {}10271028 static String decodeUtf8(byte[] bytes) throws IOException {1029 try {1030 return StandardCharsets.UTF_81031 .newDecoder()1032 .onMalformedInput(CodingErrorAction.REPORT)1033 .onUnmappableCharacter(CodingErrorAction.REPORT)1034 .decode(ByteBuffer.wrap(bytes))1035 .toString();1036 } catch (CharacterCodingException exception) {1037 throw new IOException("JWT segment is not valid UTF-8", exception);1038 }1039 }10401041 static String decodeSegment(String segment) throws IOException {1042 if (!isBase64UrlSegment(segment)) {1043 throw new IllegalArgumentException("JWT segment is not valid Base64URL");1044 }1045 byte[] bytes = Base64.getUrlDecoder().decode(segment);1046 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {1047 throw new IllegalArgumentException("JWT segment is not valid Base64URL");1048 }1049 String decoded = decodeUtf8(bytes);1050 JsonNode value = JSON.readTree(decoded);1051 if (value == null || value.isMissingNode() || !value.isObject()) {1052 throw new IOException("JWT segment is not a JSON object");1053 }1054 return decoded;1055 }10561057 static boolean isBase64UrlSegment(String segment) {1058 if (segment.isEmpty() || segment.length() % 4 == 1) {1059 return false;1060 }1061 return segment1062 .chars()1063 .allMatch(1064 character ->1065 character >= 'A' && character <= 'Z'1066 || character >= 'a' && character <= 'z'1067 || character >= '0' && character <= '9'1068 || character == '-'1069 || character == '_');1070 }10711072 static String[] requireCompactJwt(String token) {1073 if (token == null) {1074 throw new IllegalArgumentException("Expected a compact JWT with three segments");1075 }1076 String[] parts = token.split("\\.", -1);1077 if (parts.length != 3) {1078 throw new IllegalArgumentException("Expected a compact JWT with three segments");1079 }1080 return parts;1081 }10821083 public static void main(String[] args) throws IOException {1084 String[] parts = requireCompactJwt(System.getenv("TOKEN"));1085 System.out.println(decodeSegment(parts[1]));1086 }1087}1088```10891090```csharp1091using System.Text;1092using System.Text.Json;10931094static string DecodeSegment(string segment)1095{1096 if (1097 segment.Length % 4 == 1 ||1098 segment.Any(1099 character =>1100 !(1101 character is >= 'A' and <= 'Z' ||1102 character is >= 'a' and <= 'z' ||1103 character is >= '0' and <= '9' ||1104 character is '-' or '_'1105 )1106 )1107 )1108 {1109 throw new FormatException("JWT segment is not valid Base64URL");1110 }11111112 byte[] decoded = Convert.FromBase64String(1113 segment.Replace('-', '+').Replace('_', '/') +1114 new string('=', (4 - segment.Length % 4) % 4)1115 );1116 string canonicalSegment = Convert1117 .ToBase64String(decoded)1118 .TrimEnd('=')1119 .Replace('+', '-')1120 .Replace('/', '_');1121 if (canonicalSegment != segment)1122 {1123 throw new FormatException("JWT segment is not valid Base64URL");1124 }1125 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);1126 using JsonDocument document = JsonDocument.Parse(decodedJson);1127 if (document.RootElement.ValueKind is not JsonValueKind.Object)1128 {1129 throw new FormatException("JWT segment is not a JSON object");1130 }1131 return decodedJson;1132}11331134string? token = Environment.GetEnvironmentVariable("TOKEN");1135if (token is null)1136{1137 throw new InvalidOperationException(1138 "Expected a compact JWT with three segments"1139 );1140}1141string[] parts = token.Split('.');1142if (parts.Length != 3)1143{1144 throw new InvalidOperationException(1145 "Expected a compact JWT with three segments"1146 );1147}11481149Console.WriteLine(DecodeSegment(parts[1]));1150```11511152```ruby1153require "base64"1154require "json"11551156parts = ENV.fetch("TOKEN", "").split(".", -1)1157raise "Expected a compact JWT with three segments" unless parts.length == 311581159unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 11160 raise "JWT payload is not valid Base64URL"1161end11621163begin1164 payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))1165rescue ArgumentError1166 raise "JWT payload is not valid Base64URL"1167end1168unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]1169 raise "JWT payload is not valid Base64URL"1170end11711172payload.force_encoding(Encoding::UTF_8)1173raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?11741175claims = JSON.parse(payload)1176raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)11771178puts(payload)1179```118011811182This 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.11831184A decoded GKE projected service account token will look similar to:11851186```json1187{1188 "iss": "https://container.googleapis.com/v1/projects/my-project/locations/us-central1/clusters/openai-wif",1189 "aud": ["https://api.openai.com/v1"],1190 "sub": "system:serviceaccount:default:openai-wif",1191 "iat": 1716235422,1192 "exp": 1716239022,1193 "kubernetes.io": {1194 "namespace": "default",1195 "serviceaccount": {1196 "name": "openai-wif",1197 "uid": "11111111-2222-3333-4444-555555555555"1198 }1199 }1200}1201```12021203Use 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.12041205### Setting up workload identity federation12061207Create a Workload Identity Provider in OpenAI for the GKE issuer, then add a service account mapping that matches attributes from the projected token.12081209Configure the Workload Identity Provider first, then create the service account mapping.12101211#### Set up the Workload Identity Provider121212131. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `google-gke-prod`. Use **Description**, such as `Production GKE cluster`, to help admins identify the cluster.121412152. **Set the issuer and audience.** Set **OIDC Issuer URL** to the issuer returned by `kubectl get --raw /.well-known/openid-configuration | jq -r .issuer`. This value must match the `iss` claim in the projected GKE 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`.121612173. **Use GKE OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses the GKE issuer's OIDC discovery metadata and JWKS to verify the projected service account token.121812194. **Add attribute transformations if you need derived mapping attributes.** For example, enter `gke_subject` with expression `assertion.sub` to create `openai.gke_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.12201221#### Set up the service account mapping122212231. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `default-openai-wif`. Use **Description**, such as `Default namespace GKE OpenAI API workload`, to explain which workload can use the mapping.122412252. **Match the GKE service account subject.** Set **Key** to `sub` and **Value** to `system:serviceaccount:default:openai-wif`. For GKE service accounts, the subject format is `system:serviceaccount:<namespace>:<service-account-name>`.122612273. **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 GKE workload can use, such as `google-gke-prod-openai-wif`.122812294. **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.12301231### Using the token in code12321233Configure your OpenAI SDK client to read the projected GKE service account token and exchange it for an OpenAI-issued access token.12341235Use 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 GKE token for an OpenAI-issued access token and uses the OpenAI token to authenticate API requests.12361237The following examples initialize an OpenAI client with a custom subject token provider. The provider reads the projected GKE service account token from the mounted file path and uses it as the subject token for workload identity federation.12381239Authenticate from a GKE projected service account token12401241```javascript1242import { readFile } from "node:fs/promises";1243import OpenAI from "openai";12441245const tokenPath = "/var/run/secrets/tokens/token";1246const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;1247const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;12481249if (!identityProviderId || !serviceAccountId) {1250 throw new Error(1251 "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"1252 );1253}12541255function mountedGkeServiceAccountTokenProvider(path) {1256 return {1257 tokenType: "jwt",1258 getToken: async () => {1259 const token = (await readFile(path, "utf8")).trim();1260 if (!token) {1261 throw new Error("The mounted GKE service account token file is empty.");1262 }1263 return token;1264 },1265 };1266}12671268const client = new OpenAI({1269 workloadIdentity: {1270 identityProviderId,1271 serviceAccountId,1272 provider: mountedGkeServiceAccountTokenProvider(tokenPath),1273 },1274});12751276const response = await client.responses.create({1277 model: "gpt-5.6-terra",1278 input: "Say hello from Google GKE workload identity federation.",1279});12801281console.log(response.output_text);1282```12831284```python1285import os1286from pathlib import Path12871288from openai import OpenAI1289from openai.auth import SubjectTokenProvider12901291TOKEN_PATH = "/var/run/secrets/tokens/token"129212931294def mounted_gke_service_account_token_provider(token_path: str) -> SubjectTokenProvider:1295 def get_token() -> str:1296 token = Path(token_path).read_text().strip()1297 if not token:1298 raise RuntimeError("The mounted GKE service account token file is empty.")1299 return token13001301 return {"token_type": "jwt", "get_token": get_token}130213031304client = OpenAI(1305 workload_identity={1306 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],1307 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],1308 "provider": mounted_gke_service_account_token_provider(TOKEN_PATH),1309 },1310)13111312response = client.responses.create(1313 model="gpt-5.6-terra",1314 input="Say hello from Google GKE workload identity federation.",1315)13161317print(response.output_text)1318```13191320```go1321package main13221323import (1324 "context"1325 "fmt"1326 "log"1327 "os"1328 "strings"13291330 "github.com/openai/openai-go/v3"1331 "github.com/openai/openai-go/v3/auth"1332 "github.com/openai/openai-go/v3/option"1333 "github.com/openai/openai-go/v3/responses"1334)13351336const tokenPath = "/var/run/secrets/tokens/token"13371338type mountedGkeServiceAccountTokenProvider struct {1339 path string1340}13411342func (p mountedGkeServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {1343 return auth.SubjectTokenTypeJWT1344}13451346func (p mountedGkeServiceAccountTokenProvider) GetToken(_ context.Context, _ auth.HTTPDoer) (string, error) {1347 data, err := os.ReadFile(p.path)1348 if err != nil {1349 return "", &auth.SubjectTokenProviderError{1350 Provider: "google-gke",1351 Message: "failed to read mounted GKE service account token",1352 Cause: err,1353 }1354 }13551356 token := strings.TrimSpace(string(data))1357 if token == "" {1358 return "", &auth.SubjectTokenProviderError{1359 Provider: "google-gke",1360 Message: "mounted GKE service account token is empty",1361 }1362 }13631364 return token, nil1365}13661367func main() {1368 client := openai.NewClient(1369 option.WithWorkloadIdentity(auth.WorkloadIdentity{1370 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),1371 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),1372 Provider: mountedGkeServiceAccountTokenProvider{1373 path: tokenPath,1374 },1375 }),1376 )13771378 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{1379 Model: openai.ChatModelGPT4_1Mini,1380 Input: responses.ResponseNewParamsInputUnion{1381 OfString: openai.String("Say hello from Google GKE workload identity federation."),1382 },1383 })1384 if err != nil {1385 log.Fatal(err)1386 }13871388 fmt.Println(response.OutputText())1389}1390```13911392```java1393import com.fasterxml.jackson.databind.json.JsonMapper;1394import com.openai.auth.SubjectTokenProvider;1395import com.openai.auth.SubjectTokenType;1396import com.openai.auth.WorkloadIdentity;1397import com.openai.client.OpenAIClient;1398import com.openai.client.okhttp.OpenAIOkHttpClient;1399import com.openai.core.http.HttpClient;1400import com.openai.errors.SubjectTokenProviderException;1401import com.openai.models.responses.ResponseCreateParams;1402import java.nio.file.Files;1403import java.nio.file.Path;1404import java.util.concurrent.CompletableFuture;14051406public final class GoogleGkeWorkloadIdentityExample {1407 private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";14081409 private GoogleGkeWorkloadIdentityExample() {}14101411 static final class MountedGkeServiceAccountTokenProvider implements SubjectTokenProvider {1412 private final Path tokenPath;14131414 MountedGkeServiceAccountTokenProvider(String tokenPath) {1415 this.tokenPath = Path.of(tokenPath);1416 }14171418 @Override1419 public SubjectTokenType tokenType() {1420 return SubjectTokenType.JWT;1421 }14221423 @Override1424 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {1425 String token;1426 try {1427 token = Files.readString(tokenPath).trim();1428 } catch (Exception e) {1429 throw new SubjectTokenProviderException(1430 "google-gke", "failed to read mounted GKE service account token", e);1431 }14321433 if (token.isEmpty()) {1434 throw new SubjectTokenProviderException(1435 "google-gke", "mounted GKE service account token is empty", null);1436 }14371438 return token;1439 }14401441 @Override1442 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {1443 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));1444 }1445 }14461447 public static void main(String[] args) {1448 WorkloadIdentity workloadIdentity =1449 WorkloadIdentity.builder()1450 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))1451 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))1452 .provider(new MountedGkeServiceAccountTokenProvider(TOKEN_PATH))1453 .build();14541455 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();14561457 ResponseCreateParams params =1458 ResponseCreateParams.builder()1459 .model("gpt-5.6-terra")1460 .input("Say hello from Google GKE workload identity federation.")1461 .build();14621463 client.responses().create(params).output().stream()1464 .flatMap(item -> item.message().stream())1465 .flatMap(message -> message.content().stream())1466 .flatMap(content -> content.outputText().stream())1467 .forEach(outputText -> System.out.println(outputText.text()));1468 }1469}1470```14711472```ruby1473require "openai"14741475TOKEN_PATH = "/var/run/secrets/tokens/token"14761477class MountedGkeServiceAccountTokenProvider1478 include OpenAI::Auth::SubjectTokenProvider14791480 def initialize(token_path:)1481 @token_path = token_path1482 end14831484 def token_type1485 OpenAI::Auth::TokenType::JWT1486 end14871488 def get_token1489 token = File.read(@token_path).strip1490 if token.empty?1491 raise OpenAI::Errors::SubjectTokenProviderError.new(1492 message: "Mounted GKE service account token is empty",1493 provider: "google-gke"1494 )1495 end1496 token1497 rescue SystemCallError => e1498 raise OpenAI::Errors::SubjectTokenProviderError.new(1499 message: "Failed to read mounted GKE service account token: #{e.message}",1500 provider: "google-gke",1501 cause: e1502 )1503 end1504end15051506provider = MountedGkeServiceAccountTokenProvider.new(token_path: TOKEN_PATH)15071508workload_identity = OpenAI::Auth::WorkloadIdentity.new(1509 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),1510 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),1511 provider: provider1512)15131514client = OpenAI::Client.new(workload_identity: workload_identity)15151516response = client.responses.create(1517 model: "gpt-5.6-terra",1518 input: "Say hello from Google GKE workload identity federation."1519)15201521puts(response.output_text)1522```1523152415251526## Google Cloud best practices15271528- Use dedicated Google service accounts for each workload. Avoid sharing service accounts across unrelated services or environments.1529- Use workload identity flows instead of long-lived service account keys. Avoid distributing and rotating JSON key files for workloads that can use metadata-server identity tokens or GKE Workload Identity.1530- Scope identities to the smallest practical workload boundary. Separate service accounts for individual applications provide clearer auditing and least-privilege access.1531- Use attribute-based mappings carefully. Prefer stable identifiers such as service account subject claims over mutable metadata where possible.1532- Separate production and non-production projects. Distinct projects reduce the risk of accidental privilege sharing and simplify auditing.1533- Grant only required IAM permissions. Restrict the Google identity to only the permissions required for the workload.1534- Monitor service account usage. Unexpected token exchanges may indicate configuration drift or compromised workloads.