SPB Git

Repository Public

google-cloud.md 51.0 KB 1,534 lines markdown

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

Theme