SPB Git

Repository Public

aws.md 50.6 KB 1,509 lines markdown

1# Configuring workload identity federation for AWS23> 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 AWS as a Workload Identity Provider in either of these scenarios:67- **AWS outbound identity federation:** Exchange an AWS STS-issued OIDC JWT from `GetWebIdentityToken` for a short-lived OpenAI access token.8- **Amazon EKS:** Exchange a projected Amazon EKS service account token for a short-lived OpenAI access token.910OpenAI supports AWS-issued OIDC JWTs from outbound identity federation and11  Kubernetes projected service account tokens issued by Amazon EKS. OpenAI does12  not support SigV4-signed requests or AWS STS temporary access key credentials13  as workload identity federation subject tokens.14151617## AWS outbound identity federation1819AWS outbound identity federation lets an AWS principal request a signed OIDC JWT from AWS STS and present that token to an external service. In OpenAI workload identity federation, the AWS-issued JWT is the subject token that OpenAI validates before issuing an OpenAI access token.2021### Setting up AWS outbound identity federation2223Enable outbound identity federation for the AWS account that will issue tokens. For setup details, see the AWS guide to [getting started with outbound identity federation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_outbound_getting_started.html).2425```bash26aws iam enable-outbound-web-identity-federation27```2829Record the account-specific issuer URL returned by AWS. You will configure this value as the OpenAI Workload Identity Provider issuer, and it must match the `iss` claim in AWS-issued tokens.3031The AWS STS `GetWebIdentityToken` API is not available on the STS global32  endpoint. Configure the AWS CLI or SDK to use a regional STS endpoint.3334Grant the workload permission to call `sts:GetWebIdentityToken`. Restrict the audience and maximum token lifetime in IAM so the AWS principal can mint only tokens intended for OpenAI. This example allows tokens for the audience `https://api.openai.com/v1` with a maximum lifetime of 300 seconds:3536```json37{38  "Version": "2012-10-17",39  "Statement": [40    {41      "Effect": "Allow",42      "Action": "sts:GetWebIdentityToken",43      "Resource": "*",44      "Condition": {45        "ForAllValues:StringEquals": {46          "sts:IdentityTokenAudience": "https://api.openai.com/v1"47        },48        "NumericLessThanEquals": {49          "sts:DurationSeconds": 30050        }51      }52    }53  ]54}55```5657Request an AWS-issued OIDC token with the same audience you will configure on the OpenAI Workload Identity Provider. Use `ES384` unless your environment requires `RS256` compatibility.5859```bash60TOKEN=$(aws sts get-web-identity-token \61  --audience "https://api.openai.com/v1" \62  --signing-algorithm ES384 \63  --duration-seconds 300 \64  --tags Key=environment,Value=production \65         Key=workload,Value=batch-ingest \66  --query "WebIdentityToken" \67  --output text)68export TOKEN69```7071### Verify the AWS-issued token7273Before configuring workload identity federation, export the AWS-issued token as `TOKEN`, then run this script locally to inspect its claims:7475```javascript76const parts = process.env.TOKEN?.split(".") ?? [];77if (parts.length !== 3) {78  throw new Error("Expected a compact JWT with three segments");79}80if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {81  throw new Error("JWT payload is not valid Base64URL");82}8384const bytes = Buffer.from(parts[1], "base64url");85if (bytes.toString("base64url") !== parts[1]) {86  throw new Error("JWT payload is not valid Base64URL");87}88const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);89const claims = JSON.parse(decoded);90if (claims === null || Array.isArray(claims) || typeof claims !== "object") {91  throw new Error("JWT payload is not a JSON object");92}93console.log(decoded);94```9596```python97import base6498import json99import os100import re101102103def reject_non_json_constant(value):104    raise ValueError(f"JWT payload contains non-JSON constant: {value}")105106107parts = os.environ.get("TOKEN", "").split(".")108if len(parts) != 3:109    raise ValueError("Expected a compact JWT with three segments")110111payload = parts[1]112if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:113    raise ValueError("JWT payload is not valid Base64URL")114padded_payload = payload + "=" * (-len(payload) % 4)115decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)116if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:117    raise ValueError("JWT payload is not valid Base64URL")118decoded_text = decoded.decode("utf-8")119claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)120if not isinstance(claims, dict):121    raise ValueError("JWT payload is not a JSON object")122print(decoded_text)123```124125```go126package main127128import (129	"bytes"130	"encoding/base64"131	"encoding/json"132	"fmt"133	"os"134	"strings"135	"unicode/utf8"136)137138func decodeSegment(segment string) (json.RawMessage, error) {139	if !isBase64URLSegment(segment) {140		return nil, fmt.Errorf("JWT segment is not valid Base64URL")141	}142	decoded, err := base64.RawURLEncoding.DecodeString(segment)143	if err != nil {144		return nil, err145	}146	if base64.RawURLEncoding.EncodeToString(decoded) != segment {147		return nil, fmt.Errorf("JWT segment is not valid Base64URL")148	}149	if !utf8.Valid(decoded) {150		return nil, fmt.Errorf("JWT segment is not valid UTF-8")151	}152153	var value json.RawMessage154	if err := json.Unmarshal(decoded, &value); err != nil {155		return nil, err156	}157	if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {158		return nil, fmt.Errorf("JWT segment is not a JSON object")159	}160	return value, nil161}162163func isBase64URLSegment(segment string) bool {164	if segment == "" || len(segment)%4 == 1 {165		return false166	}167	for _, character := range segment {168		if !('A' <= character && character <= 'Z') &&169			!('a' <= character && character <= 'z') &&170			!('0' <= character && character <= '9') &&171			character != '-' &&172			character != '_' {173			return false174		}175	}176	return true177}178179func main() {180	parts := strings.Split(os.Getenv("TOKEN"), ".")181	if len(parts) != 3 {182		panic("Expected a compact JWT with three segments")183	}184185	payload, err := decodeSegment(parts[1])186	if err != nil {187		panic(err)188	}189	formatted, err := json.MarshalIndent(payload, "", "  ")190	if err != nil {191		panic(err)192	}193	fmt.Println(string(formatted))194}195```196197```java198// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.199import com.fasterxml.jackson.databind.DeserializationFeature;200import com.fasterxml.jackson.databind.JsonNode;201import com.fasterxml.jackson.databind.ObjectMapper;202import java.io.IOException;203import java.nio.ByteBuffer;204import java.nio.charset.CharacterCodingException;205import java.nio.charset.CodingErrorAction;206import java.nio.charset.StandardCharsets;207import java.util.Base64;208209public final class DecodeJwtPayloadExample {210  private static final ObjectMapper JSON =211      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);212213  private DecodeJwtPayloadExample() {}214215  static String decodeUtf8(byte[] bytes) throws IOException {216    try {217      return StandardCharsets.UTF_8218          .newDecoder()219          .onMalformedInput(CodingErrorAction.REPORT)220          .onUnmappableCharacter(CodingErrorAction.REPORT)221          .decode(ByteBuffer.wrap(bytes))222          .toString();223    } catch (CharacterCodingException exception) {224      throw new IOException("JWT segment is not valid UTF-8", exception);225    }226  }227228  static String decodeSegment(String segment) throws IOException {229    if (!isBase64UrlSegment(segment)) {230      throw new IllegalArgumentException("JWT segment is not valid Base64URL");231    }232    byte[] bytes = Base64.getUrlDecoder().decode(segment);233    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {234      throw new IllegalArgumentException("JWT segment is not valid Base64URL");235    }236    String decoded = decodeUtf8(bytes);237    JsonNode value = JSON.readTree(decoded);238    if (value == null || value.isMissingNode() || !value.isObject()) {239      throw new IOException("JWT segment is not a JSON object");240    }241    return decoded;242  }243244  static boolean isBase64UrlSegment(String segment) {245    if (segment.isEmpty() || segment.length() % 4 == 1) {246      return false;247    }248    return segment249        .chars()250        .allMatch(251            character ->252                character >= 'A' && character <= 'Z'253                    || character >= 'a' && character <= 'z'254                    || character >= '0' && character <= '9'255                    || character == '-'256                    || character == '_');257  }258259  static String[] requireCompactJwt(String token) {260    if (token == null) {261      throw new IllegalArgumentException("Expected a compact JWT with three segments");262    }263    String[] parts = token.split("\\.", -1);264    if (parts.length != 3) {265      throw new IllegalArgumentException("Expected a compact JWT with three segments");266    }267    return parts;268  }269270  public static void main(String[] args) throws IOException {271    String[] parts = requireCompactJwt(System.getenv("TOKEN"));272    System.out.println(decodeSegment(parts[1]));273  }274}275```276277```csharp278using System.Text;279using System.Text.Json;280281static string DecodeSegment(string segment)282{283    if (284        segment.Length % 4 == 1 ||285        segment.Any(286            character =>287                !(288                    character is >= 'A' and <= 'Z' ||289                    character is >= 'a' and <= 'z' ||290                    character is >= '0' and <= '9' ||291                    character is '-' or '_'292                )293        )294    )295    {296        throw new FormatException("JWT segment is not valid Base64URL");297    }298299    byte[] decoded = Convert.FromBase64String(300        segment.Replace('-', '+').Replace('_', '/') +301        new string('=', (4 - segment.Length % 4) % 4)302    );303    string canonicalSegment = Convert304        .ToBase64String(decoded)305        .TrimEnd('=')306        .Replace('+', '-')307        .Replace('/', '_');308    if (canonicalSegment != segment)309    {310        throw new FormatException("JWT segment is not valid Base64URL");311    }312    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);313    using JsonDocument document = JsonDocument.Parse(decodedJson);314    if (document.RootElement.ValueKind is not JsonValueKind.Object)315    {316        throw new FormatException("JWT segment is not a JSON object");317    }318    return decodedJson;319}320321string? token = Environment.GetEnvironmentVariable("TOKEN");322if (token is null)323{324    throw new InvalidOperationException(325        "Expected a compact JWT with three segments"326    );327}328string[] parts = token.Split('.');329if (parts.Length != 3)330{331    throw new InvalidOperationException(332        "Expected a compact JWT with three segments"333    );334}335336Console.WriteLine(DecodeSegment(parts[1]));337```338339```ruby340require "base64"341require "json"342343parts = ENV.fetch("TOKEN", "").split(".", -1)344raise "Expected a compact JWT with three segments" unless parts.length == 3345346unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1347  raise "JWT payload is not valid Base64URL"348end349350begin351  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))352rescue ArgumentError353  raise "JWT payload is not valid Base64URL"354end355unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]356  raise "JWT payload is not valid Base64URL"357end358359payload.force_encoding(Encoding::UTF_8)360raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?361362claims = JSON.parse(payload)363raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)364365puts(payload)366```367368369This 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.370371A decoded AWS-issued OIDC token will look similar to:372373```json374{375  "iss": "https://abc123-def456-ghi789-jkl012.tokens.sts.global.api.aws",376  "aud": "https://api.openai.com/v1",377  "sub": "arn:aws:iam::123456789012:role/OpenAIWifRole",378  "iat": 1716235422,379  "exp": 1716235722,380  "jti": "jwt-id-example",381  "https://sts.amazonaws.com/": {382    "aws_account": "123456789012",383    "source_region": "us-west-2",384    "org_id": "o-exampleorgid",385    "principal_tags": {386      "environment": "production"387    },388    "request_tags": {389      "environment": "production",390      "workload": "batch-ingest"391    }392  }393}394```395396Not every AWS-issued token contains every AWS-specific claim. The claims under `https://sts.amazonaws.com/` depend on the calling principal, session context, and request tags.397398Verify the claims you plan to configure in OpenAI:399400- `iss`: Must match the AWS account-specific issuer URL configured in the OpenAI Workload Identity Provider.401- `aud`: Must match the `GetWebIdentityToken` audience and the OpenAI Workload Identity Provider audience.402- `sub`: Identifies the IAM principal ARN that requested the token. Prefer matching the exact role ARN.403- AWS-specific claims: Use the decoded token as the source of truth before matching account, organization, principal tag, or request tag values.404405Use 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.406407### Setting up workload identity federation408409Create a Workload Identity Provider in OpenAI for the AWS account issuer, then add a service account mapping that matches stable claims from the AWS-issued token.410411Configure the Workload Identity Provider first, then create the service account mapping.412413#### Set up the Workload Identity Provider4144151. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `aws-outbound-prod`. Use **Description**, such as `Production AWS outbound identity federation workloads`, to help admins identify the provider.4164172. **Set the issuer and audience.** Set **OIDC Issuer URL** to the AWS account-specific issuer URL returned when outbound identity federation was enabled. This value must match the token's `iss` claim. Set **Audience** to the same audience passed to `GetWebIdentityToken`. In this example, that value is `https://api.openai.com/v1`.4184193. **Use AWS OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses the AWS issuer's OIDC discovery metadata and JWKS to verify the AWS-issued token.4204214. **Add attribute transformations only if you need derived mapping attributes.** Raw token matching supports top-level scalar claims such as `sub`, `aud`, and `iss`. AWS-specific namespaced claims are nested under `https://sts.amazonaws.com/`, so create derived attributes with CEL bracket notation before using them in mappings. For example, enter `aws_environment` with expression `assertion["https://sts.amazonaws.com/"]["principal_tags"]["environment"]` to create `openai.aws_environment` from the decoded token example above. Verify the nested claim path in a sample token before using it; if a transformation cannot be evaluated, mapping resolution fails. Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.422423#### Set up the service account mapping4244251. **Create a service account mapping.** Set **Name** to a value that is unique within the Workload Identity Provider, such as `aws-role-openai-wif`. Use **Description**, such as `Production AWS role for OpenAI API workload`, to explain which workload can use the mapping.4264272. **Match the AWS principal.** Set **Key** to `sub` and **Value** to the IAM principal ARN from the decoded token, such as `arn:aws:iam::123456789012:role/OpenAIWifRole`. Matching on the exact `sub` claim provides the strongest isolation for AWS outbound identity federation.4284293. **Add additional claim matches if needed.** You can match on any available scalar claim or transformed attribute. For example, use transformed attributes derived from AWS account, organization, principal tag, or request tag claims if you need additional trust boundaries.4304314. **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 AWS workload can use, such as `aws-outbound-prod-openai-wif`.4324335. **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.434435### Using the token in code436437Configure your OpenAI SDK client to request an AWS-issued OIDC token from AWS STS and exchange it for an OpenAI-issued access token.438439Set `OPENAI_WIF_AUDIENCE` to the same audience configured on the OpenAI Workload Identity Provider. The subject token provider calls AWS STS `GetWebIdentityToken` with that audience, returns the AWS-issued JWT as the subject token, and the OpenAI SDK exchanges it for an OpenAI-issued access token.440441Authenticate from an AWS-issued OIDC token442443```javascript444import { GetWebIdentityTokenCommand, STSClient } from "@aws-sdk/client-sts";445import OpenAI from "openai";446447const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;448const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;449const audience = process.env.OPENAI_WIF_AUDIENCE;450const awsRegion = process.env.AWS_REGION;451452if (!identityProviderId || !serviceAccountId || !audience || !awsRegion) {453  throw new Error(454    "Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, OPENAI_WIF_AUDIENCE, and AWS_REGION"455  );456}457const wifAudience = audience;458459const sts = new STSClient({ region: awsRegion });460461function awsOutboundWebIdentityTokenProvider() {462  return {463    tokenType: "jwt",464    getToken: async () => {465      const response = await sts.send(466        new GetWebIdentityTokenCommand({467          Audience: [wifAudience],468          SigningAlgorithm: "ES384",469          DurationSeconds: 300,470        })471      );472473      if (!response.WebIdentityToken) {474        throw new Error("AWS STS did not return a web identity token.");475      }476477      return response.WebIdentityToken;478    },479  };480}481482const client = new OpenAI({483  workloadIdentity: {484    identityProviderId,485    serviceAccountId,486    provider: awsOutboundWebIdentityTokenProvider(),487  },488});489490const response = await client.responses.create({491  model: "gpt-5.6-terra",492  input: "Say hello from AWS outbound workload identity federation.",493});494495console.log(response.output_text);496```497498```python499import os500501import boto3502from openai import OpenAI503from openai.auth import SubjectTokenProvider504505506def aws_outbound_web_identity_token_provider(audience: str) -> SubjectTokenProvider:507    sts = boto3.client("sts", region_name=os.environ["AWS_REGION"])508509    def get_token() -> str:510        response = sts.get_web_identity_token(511            Audience=[audience],512            SigningAlgorithm="ES384",513            DurationSeconds=300,514        )515        token = response.get("WebIdentityToken", "")516        if not token:517            raise RuntimeError("AWS STS did not return a web identity token.")518        return token519520    return {"token_type": "jwt", "get_token": get_token}521522523client = OpenAI(524    workload_identity={525        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],526        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],527        "provider": aws_outbound_web_identity_token_provider(528            os.environ["OPENAI_WIF_AUDIENCE"]529        ),530    },531)532533response = client.responses.create(534    model="gpt-5.6-terra",535    input="Say hello from AWS outbound workload identity federation.",536)537538print(response.output_text)539```540541```go542package main543544import (545	"context"546	"fmt"547	"log"548	"os"549550	awssdk "github.com/aws/aws-sdk-go-v2/aws"551	"github.com/aws/aws-sdk-go-v2/config"552	"github.com/aws/aws-sdk-go-v2/service/sts"553	"github.com/openai/openai-go/v3"554	"github.com/openai/openai-go/v3/auth"555	"github.com/openai/openai-go/v3/option"556	"github.com/openai/openai-go/v3/responses"557)558559type awsOutboundWebIdentityTokenProvider struct {560	client   *sts.Client561	audience string562}563564func (p awsOutboundWebIdentityTokenProvider) TokenType() auth.SubjectTokenType {565	return auth.SubjectTokenTypeJWT566}567568func (p awsOutboundWebIdentityTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {569	output, err := p.client.GetWebIdentityToken(ctx, &sts.GetWebIdentityTokenInput{570		Audience:         []string{p.audience},571		DurationSeconds:  awssdk.Int32(300),572		SigningAlgorithm: awssdk.String("ES384"),573	})574	if err != nil {575		return "", &auth.SubjectTokenProviderError{576			Provider: "aws-outbound",577			Message:  "failed to request AWS web identity token",578			Cause:    err,579		}580	}581582	token := awssdk.ToString(output.WebIdentityToken)583	if token == "" {584		return "", &auth.SubjectTokenProviderError{585			Provider: "aws-outbound",586			Message:  "AWS STS did not return a web identity token",587		}588	}589590	return token, nil591}592593func main() {594	ctx := context.Background()595	audience := os.Getenv("OPENAI_WIF_AUDIENCE")596	if audience == "" {597		log.Fatal("Set OPENAI_WIF_AUDIENCE")598	}599600	cfg, err := config.LoadDefaultConfig(ctx)601	if err != nil {602		log.Fatal(err)603	}604605	client := openai.NewClient(606		option.WithWorkloadIdentity(auth.WorkloadIdentity{607			IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),608			ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),609			Provider: awsOutboundWebIdentityTokenProvider{610				client:   sts.NewFromConfig(cfg),611				audience: audience,612			},613		}),614	)615616	response, err := client.Responses.New(ctx, responses.ResponseNewParams{617		Model: openai.ChatModelGPT4_1Mini,618		Input: responses.ResponseNewParamsInputUnion{619			OfString: openai.String("Say hello from AWS outbound workload identity federation."),620		},621	})622	if err != nil {623		log.Fatal(err)624	}625626	fmt.Println(response.OutputText())627}628```629630```java631import com.fasterxml.jackson.databind.json.JsonMapper;632import com.openai.auth.SubjectTokenProvider;633import com.openai.auth.SubjectTokenType;634import com.openai.auth.WorkloadIdentity;635import com.openai.client.OpenAIClient;636import com.openai.client.okhttp.OpenAIOkHttpClient;637import com.openai.core.http.HttpClient;638import com.openai.errors.SubjectTokenProviderException;639import com.openai.models.responses.ResponseCreateParams;640import java.util.concurrent.CompletableFuture;641import software.amazon.awssdk.regions.Region;642import software.amazon.awssdk.services.sts.StsClient;643import software.amazon.awssdk.services.sts.model.GetWebIdentityTokenRequest;644645public final class AwsOutboundWorkloadIdentityExample {646  private AwsOutboundWorkloadIdentityExample() {}647648  static final class AwsOutboundWebIdentityTokenProvider implements SubjectTokenProvider {649    private final StsClient stsClient;650    private final String audience;651652    AwsOutboundWebIdentityTokenProvider(StsClient stsClient, String audience) {653      this.stsClient = stsClient;654      this.audience = audience;655    }656657    @Override658    public SubjectTokenType tokenType() {659      return SubjectTokenType.JWT;660    }661662    @Override663    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {664      try {665        String token =666            stsClient667                .getWebIdentityToken(668                    GetWebIdentityTokenRequest.builder()669                        .audience(audience)670                        .durationSeconds(300)671                        .signingAlgorithm("ES384")672                        .build())673                .webIdentityToken();674675        if (token == null || token.isEmpty()) {676          throw new SubjectTokenProviderException(677              "aws-outbound", "AWS STS did not return a web identity token", null);678        }679680        return token;681      } catch (SubjectTokenProviderException e) {682        throw e;683      } catch (Exception e) {684        throw new SubjectTokenProviderException(685            "aws-outbound", "failed to request AWS web identity token", e);686      }687    }688689    @Override690    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {691      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));692    }693  }694695  public static void main(String[] args) {696    String audience = System.getenv("OPENAI_WIF_AUDIENCE");697    StsClient stsClient =698        StsClient.builder().region(Region.of(System.getenv("AWS_REGION"))).build();699700    WorkloadIdentity workloadIdentity =701        WorkloadIdentity.builder()702            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))703            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))704            .provider(new AwsOutboundWebIdentityTokenProvider(stsClient, audience))705            .build();706707    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();708709    ResponseCreateParams params =710        ResponseCreateParams.builder()711            .model("gpt-5.6-terra")712            .input("Say hello from AWS outbound workload identity federation.")713            .build();714715    client.responses().create(params).output().stream()716        .flatMap(item -> item.message().stream())717        .flatMap(message -> message.content().stream())718        .flatMap(content -> content.outputText().stream())719        .forEach(outputText -> System.out.println(outputText.text()));720  }721}722```723724```ruby725require "aws-sdk-sts"726require "openai"727728class AwsOutboundWebIdentityTokenProvider729  include OpenAI::Auth::SubjectTokenProvider730731  def initialize(audience:, sts_client:)732    @audience = audience733    @sts_client = sts_client734  end735736  def token_type737    OpenAI::Auth::TokenType::JWT738  end739740  def get_token741    response = @sts_client.get_web_identity_token(742      audience: [@audience],743      signing_algorithm: "ES384",744      duration_seconds: 300745    )746    token = response.web_identity_token.to_s747    if token.empty?748      raise OpenAI::Errors::SubjectTokenProviderError.new(749        message: "AWS STS did not return a web identity token",750        provider: "aws-outbound"751      )752    end753    token754  rescue Aws::STS::Errors::ServiceError => e755    raise OpenAI::Errors::SubjectTokenProviderError.new(756      message: "Failed to request AWS web identity token: #{e.message}",757      provider: "aws-outbound",758      cause: e759    )760  end761end762763provider = AwsOutboundWebIdentityTokenProvider.new(764  audience: ENV.fetch("OPENAI_WIF_AUDIENCE"),765  sts_client: Aws::STS::Client.new(region: ENV.fetch("AWS_REGION"))766)767768workload_identity = OpenAI::Auth::WorkloadIdentity.new(769  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),770  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),771  provider: provider772)773774client = OpenAI::Client.new(workload_identity: workload_identity)775776response = client.responses.create(777  model: "gpt-5.6-terra",778  input: "Say hello from AWS outbound workload identity federation."779)780781puts(response.output_text)782```783784785  786787788  789790791## Amazon EKS projected service account tokens792793Use Amazon EKS as a Workload Identity Provider by exchanging an EKS-issued projected service account token for a short-lived OpenAI access token.794795### Setting up EKS796797Use a Kubernetes `ServiceAccount` for the EKS workload that needs to call the OpenAI API. If you do not already have one, create it:798799```bash800kubectl create serviceaccount openai-wif --namespace default801```802803EKS projected service account tokens use a `sub` claim in the format `system:serviceaccount:<namespace>:<service-account-name>`. For the service account above, the `sub` claim is `system:serviceaccount:default:openai-wif`.804805Retrieve the OIDC issuer URL associated with the EKS cluster:806807```bash808aws eks describe-cluster \809  --name <cluster-name> \810  --region <region> \811  --query "cluster.identity.oidc.issuer" \812  --output text813```814815Example output:816817```text818https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3819```820821The issuer you configure in the OpenAI Workload Identity Provider must match this issuer URL and the `iss` claim in the projected EKS service account token.822823Configure 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:824825```yaml826apiVersion: v1827kind: Pod828metadata:829  name: openai-wif-app830  namespace: default831spec:832  serviceAccountName: openai-wif833  containers:834    - name: app835      image: my-image836      volumeMounts:837        - name: eks-sa-token838          mountPath: /var/run/secrets/tokens839          readOnly: true840  volumes:841    - name: eks-sa-token842      projected:843        sources:844          - serviceAccountToken:845              path: token846              audience: "https://api.openai.com/v1"847              expirationSeconds: 3600848```849850### Verify the EKS token851852Before 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`:853854```bash855TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)856export TOKEN857```858859Then run this script:860861```javascript862const parts = process.env.TOKEN?.split(".") ?? [];863if (parts.length !== 3) {864  throw new Error("Expected a compact JWT with three segments");865}866if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {867  throw new Error("JWT payload is not valid Base64URL");868}869870const bytes = Buffer.from(parts[1], "base64url");871if (bytes.toString("base64url") !== parts[1]) {872  throw new Error("JWT payload is not valid Base64URL");873}874const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);875const claims = JSON.parse(decoded);876if (claims === null || Array.isArray(claims) || typeof claims !== "object") {877  throw new Error("JWT payload is not a JSON object");878}879console.log(decoded);880```881882```python883import base64884import json885import os886import re887888889def reject_non_json_constant(value):890    raise ValueError(f"JWT payload contains non-JSON constant: {value}")891892893parts = os.environ.get("TOKEN", "").split(".")894if len(parts) != 3:895    raise ValueError("Expected a compact JWT with three segments")896897payload = parts[1]898if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:899    raise ValueError("JWT payload is not valid Base64URL")900padded_payload = payload + "=" * (-len(payload) % 4)901decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)902if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:903    raise ValueError("JWT payload is not valid Base64URL")904decoded_text = decoded.decode("utf-8")905claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)906if not isinstance(claims, dict):907    raise ValueError("JWT payload is not a JSON object")908print(decoded_text)909```910911```go912package main913914import (915	"bytes"916	"encoding/base64"917	"encoding/json"918	"fmt"919	"os"920	"strings"921	"unicode/utf8"922)923924func decodeSegment(segment string) (json.RawMessage, error) {925	if !isBase64URLSegment(segment) {926		return nil, fmt.Errorf("JWT segment is not valid Base64URL")927	}928	decoded, err := base64.RawURLEncoding.DecodeString(segment)929	if err != nil {930		return nil, err931	}932	if base64.RawURLEncoding.EncodeToString(decoded) != segment {933		return nil, fmt.Errorf("JWT segment is not valid Base64URL")934	}935	if !utf8.Valid(decoded) {936		return nil, fmt.Errorf("JWT segment is not valid UTF-8")937	}938939	var value json.RawMessage940	if err := json.Unmarshal(decoded, &value); err != nil {941		return nil, err942	}943	if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {944		return nil, fmt.Errorf("JWT segment is not a JSON object")945	}946	return value, nil947}948949func isBase64URLSegment(segment string) bool {950	if segment == "" || len(segment)%4 == 1 {951		return false952	}953	for _, character := range segment {954		if !('A' <= character && character <= 'Z') &&955			!('a' <= character && character <= 'z') &&956			!('0' <= character && character <= '9') &&957			character != '-' &&958			character != '_' {959			return false960		}961	}962	return true963}964965func main() {966	parts := strings.Split(os.Getenv("TOKEN"), ".")967	if len(parts) != 3 {968		panic("Expected a compact JWT with three segments")969	}970971	payload, err := decodeSegment(parts[1])972	if err != nil {973		panic(err)974	}975	formatted, err := json.MarshalIndent(payload, "", "  ")976	if err != nil {977		panic(err)978	}979	fmt.Println(string(formatted))980}981```982983```java984// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.985import com.fasterxml.jackson.databind.DeserializationFeature;986import com.fasterxml.jackson.databind.JsonNode;987import com.fasterxml.jackson.databind.ObjectMapper;988import java.io.IOException;989import java.nio.ByteBuffer;990import java.nio.charset.CharacterCodingException;991import java.nio.charset.CodingErrorAction;992import java.nio.charset.StandardCharsets;993import java.util.Base64;994995public final class DecodeJwtPayloadExample {996  private static final ObjectMapper JSON =997      new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);998999  private DecodeJwtPayloadExample() {}10001001  static String decodeUtf8(byte[] bytes) throws IOException {1002    try {1003      return StandardCharsets.UTF_81004          .newDecoder()1005          .onMalformedInput(CodingErrorAction.REPORT)1006          .onUnmappableCharacter(CodingErrorAction.REPORT)1007          .decode(ByteBuffer.wrap(bytes))1008          .toString();1009    } catch (CharacterCodingException exception) {1010      throw new IOException("JWT segment is not valid UTF-8", exception);1011    }1012  }10131014  static String decodeSegment(String segment) throws IOException {1015    if (!isBase64UrlSegment(segment)) {1016      throw new IllegalArgumentException("JWT segment is not valid Base64URL");1017    }1018    byte[] bytes = Base64.getUrlDecoder().decode(segment);1019    if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {1020      throw new IllegalArgumentException("JWT segment is not valid Base64URL");1021    }1022    String decoded = decodeUtf8(bytes);1023    JsonNode value = JSON.readTree(decoded);1024    if (value == null || value.isMissingNode() || !value.isObject()) {1025      throw new IOException("JWT segment is not a JSON object");1026    }1027    return decoded;1028  }10291030  static boolean isBase64UrlSegment(String segment) {1031    if (segment.isEmpty() || segment.length() % 4 == 1) {1032      return false;1033    }1034    return segment1035        .chars()1036        .allMatch(1037            character ->1038                character >= 'A' && character <= 'Z'1039                    || character >= 'a' && character <= 'z'1040                    || character >= '0' && character <= '9'1041                    || character == '-'1042                    || character == '_');1043  }10441045  static String[] requireCompactJwt(String token) {1046    if (token == null) {1047      throw new IllegalArgumentException("Expected a compact JWT with three segments");1048    }1049    String[] parts = token.split("\\.", -1);1050    if (parts.length != 3) {1051      throw new IllegalArgumentException("Expected a compact JWT with three segments");1052    }1053    return parts;1054  }10551056  public static void main(String[] args) throws IOException {1057    String[] parts = requireCompactJwt(System.getenv("TOKEN"));1058    System.out.println(decodeSegment(parts[1]));1059  }1060}1061```10621063```csharp1064using System.Text;1065using System.Text.Json;10661067static string DecodeSegment(string segment)1068{1069    if (1070        segment.Length % 4 == 1 ||1071        segment.Any(1072            character =>1073                !(1074                    character is >= 'A' and <= 'Z' ||1075                    character is >= 'a' and <= 'z' ||1076                    character is >= '0' and <= '9' ||1077                    character is '-' or '_'1078                )1079        )1080    )1081    {1082        throw new FormatException("JWT segment is not valid Base64URL");1083    }10841085    byte[] decoded = Convert.FromBase64String(1086        segment.Replace('-', '+').Replace('_', '/') +1087        new string('=', (4 - segment.Length % 4) % 4)1088    );1089    string canonicalSegment = Convert1090        .ToBase64String(decoded)1091        .TrimEnd('=')1092        .Replace('+', '-')1093        .Replace('/', '_');1094    if (canonicalSegment != segment)1095    {1096        throw new FormatException("JWT segment is not valid Base64URL");1097    }1098    string decodedJson = new UTF8Encoding(false, true).GetString(decoded);1099    using JsonDocument document = JsonDocument.Parse(decodedJson);1100    if (document.RootElement.ValueKind is not JsonValueKind.Object)1101    {1102        throw new FormatException("JWT segment is not a JSON object");1103    }1104    return decodedJson;1105}11061107string? token = Environment.GetEnvironmentVariable("TOKEN");1108if (token is null)1109{1110    throw new InvalidOperationException(1111        "Expected a compact JWT with three segments"1112    );1113}1114string[] parts = token.Split('.');1115if (parts.Length != 3)1116{1117    throw new InvalidOperationException(1118        "Expected a compact JWT with three segments"1119    );1120}11211122Console.WriteLine(DecodeSegment(parts[1]));1123```11241125```ruby1126require "base64"1127require "json"11281129parts = ENV.fetch("TOKEN", "").split(".", -1)1130raise "Expected a compact JWT with three segments" unless parts.length == 311311132unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 11133  raise "JWT payload is not valid Base64URL"1134end11351136begin1137  payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))1138rescue ArgumentError1139  raise "JWT payload is not valid Base64URL"1140end1141unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]1142  raise "JWT payload is not valid Base64URL"1143end11441145payload.force_encoding(Encoding::UTF_8)1146raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?11471148claims = JSON.parse(payload)1149raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)11501151puts(payload)1152```115311541155This 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.11561157A decoded EKS projected service account token will look similar to:11581159```json1160{1161  "iss": "https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3",1162  "aud": ["https://api.openai.com/v1"],1163  "sub": "system:serviceaccount:default:openai-wif",1164  "iat": 1716235422,1165  "exp": 1716239022,1166  "kubernetes.io": {1167    "namespace": "default",1168    "serviceaccount": {1169      "name": "openai-wif",1170      "uid": "11111111-2222-3333-4444-555555555555"1171    }1172  }1173}1174```11751176Use 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.11771178### Setting up workload identity federation11791180Create a Workload Identity Provider in OpenAI for the EKS issuer, then add a service account mapping that matches attributes from the projected token.11811182Configure the Workload Identity Provider first, then create the service account mapping.11831184#### Set up the Workload Identity Provider118511861. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `aws-eks-prod`. Use **Description**, such as `Production EKS cluster`, to help admins identify the cluster.118711882. **Set the issuer and audience.** Set **OIDC Issuer URL** to the issuer returned by `aws eks describe-cluster --query "cluster.identity.oidc.issuer"`. This value must match the `iss` claim in the projected EKS 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`.118911903. **Use EKS OIDC discovery.** Leave **Use uploaded JWKS for token verification** disabled. OpenAI uses the EKS issuer's OIDC discovery metadata and JWKS to verify the projected service account token.119111924. **Add attribute transformations only if you need derived mapping attributes.** Raw token claims such as `sub`, `aud`, and `iss` can be used directly in mapping assertions. For example, create a transformed attribute named `subject` with expression `assertion.sub`. In the dashboard, enter `subject` as the attribute name; OpenAI stores it as `openai.subject`, which you can reference in mappings.11931194   > **Note:** Raw token claims that already start with `openai.` are ignored for `openai.` mapping keys unless a matching transformation is configured.11951196#### Set up the service account mapping119711981. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `openai-mapping-eks`. Use **Description**, such as `Workload Identity Provider Mapping for EKS Workloads`, to explain which workload can use the mapping.119912002. **Match the EKS service account subject.** Set **Key** to `sub` and **Value** to `system:serviceaccount:default:openai-wif`. You can match on any available claim or transformed attribute. Matching on `sub` is the most restrictive option because it uniquely identifies a Kubernetes service account.120112023. **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 EKS workload can use, such as `aws-eks-prod-openai-wif`. Check `Create a new service account in this project` if you wish to create a new service account for this mapping rather than reuse an existing one.120312044. **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.12051206### Using the token in code12071208Configure your OpenAI SDK client to read the projected EKS service account token and exchange it for an OpenAI-issued access token.12091210Use 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 EKS token for an OpenAI-issued access token and uses the OpenAI token to authenticate API requests.12111212The following examples initialize an OpenAI client with a custom subject token provider. The provider reads the projected EKS service account token from the mounted file path and uses it as the subject token for workload identity federation.12131214Authenticate from an EKS projected service account token12151216```javascript1217import { readFile } from "node:fs/promises";1218import OpenAI from "openai";12191220const tokenPath = "/var/run/secrets/tokens/token";1221const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;1222const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;12231224if (!identityProviderId || !serviceAccountId) {1225  throw new Error(1226    "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"1227  );1228}12291230function mountedEksServiceAccountTokenProvider(path) {1231  return {1232    tokenType: "jwt",1233    getToken: async () => {1234      const token = (await readFile(path, "utf8")).trim();1235      if (!token) {1236        throw new Error("The mounted EKS service account token file is empty.");1237      }1238      return token;1239    },1240  };1241}12421243const client = new OpenAI({1244  workloadIdentity: {1245    identityProviderId,1246    serviceAccountId,1247    provider: mountedEksServiceAccountTokenProvider(tokenPath),1248  },1249});12501251const response = await client.responses.create({1252  model: "gpt-5.6-terra",1253  input: "Say hello from AWS workload identity federation.",1254});12551256console.log(response.output_text);1257```12581259```python1260import os1261from pathlib import Path12621263from openai import OpenAI1264from openai.auth import SubjectTokenProvider12651266TOKEN_PATH = "/var/run/secrets/tokens/token"126712681269def mounted_eks_service_account_token_provider(token_path: str) -> SubjectTokenProvider:1270    def get_token() -> str:1271        token = Path(token_path).read_text().strip()1272        if not token:1273            raise RuntimeError("The mounted EKS service account token file is empty.")1274        return token12751276    return {"token_type": "jwt", "get_token": get_token}127712781279client = OpenAI(1280    workload_identity={1281        "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],1282        "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],1283        "provider": mounted_eks_service_account_token_provider(TOKEN_PATH),1284    },1285)12861287response = client.responses.create(1288    model="gpt-5.6-terra",1289    input="Say hello from AWS workload identity federation.",1290)12911292print(response.output_text)1293```12941295```go1296package main12971298import (1299	"context"1300	"fmt"1301	"log"1302	"os"1303	"strings"13041305	"github.com/openai/openai-go/v3"1306	"github.com/openai/openai-go/v3/auth"1307	"github.com/openai/openai-go/v3/option"1308	"github.com/openai/openai-go/v3/responses"1309)13101311const tokenPath = "/var/run/secrets/tokens/token"13121313type mountedEksServiceAccountTokenProvider struct {1314	path string1315}13161317func (p mountedEksServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {1318	return auth.SubjectTokenTypeJWT1319}13201321func (p mountedEksServiceAccountTokenProvider) GetToken(_ context.Context, _ auth.HTTPDoer) (string, error) {1322	data, err := os.ReadFile(p.path)1323	if err != nil {1324		return "", &auth.SubjectTokenProviderError{1325			Provider: "aws-eks",1326			Message:  "failed to read mounted EKS service account token",1327			Cause:    err,1328		}1329	}13301331	token := strings.TrimSpace(string(data))1332	if token == "" {1333		return "", &auth.SubjectTokenProviderError{1334			Provider: "aws-eks",1335			Message:  "mounted EKS service account token is empty",1336		}1337	}13381339	return token, nil1340}13411342func main() {1343	client := openai.NewClient(1344		option.WithWorkloadIdentity(auth.WorkloadIdentity{1345			IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),1346			ServiceAccountID:   os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),1347			Provider: mountedEksServiceAccountTokenProvider{1348				path: tokenPath,1349			},1350		}),1351	)13521353	response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{1354		Model: openai.ChatModelGPT4_1Mini,1355		Input: responses.ResponseNewParamsInputUnion{1356			OfString: openai.String("Say hello from AWS workload identity federation."),1357		},1358	})1359	if err != nil {1360		log.Fatal(err)1361	}13621363	fmt.Println(response.OutputText())1364}1365```13661367```java1368import com.fasterxml.jackson.databind.json.JsonMapper;1369import com.openai.auth.SubjectTokenProvider;1370import com.openai.auth.SubjectTokenType;1371import com.openai.auth.WorkloadIdentity;1372import com.openai.client.OpenAIClient;1373import com.openai.client.okhttp.OpenAIOkHttpClient;1374import com.openai.core.http.HttpClient;1375import com.openai.errors.SubjectTokenProviderException;1376import com.openai.models.responses.ResponseCreateParams;1377import java.nio.file.Files;1378import java.nio.file.Path;1379import java.util.concurrent.CompletableFuture;13801381public final class AwsEksWorkloadIdentityExample {1382  private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";13831384  private AwsEksWorkloadIdentityExample() {}13851386  static final class MountedEksServiceAccountTokenProvider implements SubjectTokenProvider {1387    private final Path tokenPath;13881389    MountedEksServiceAccountTokenProvider(String tokenPath) {1390      this.tokenPath = Path.of(tokenPath);1391    }13921393    @Override1394    public SubjectTokenType tokenType() {1395      return SubjectTokenType.JWT;1396    }13971398    @Override1399    public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {1400      String token;1401      try {1402        token = Files.readString(tokenPath).trim();1403      } catch (Exception e) {1404        throw new SubjectTokenProviderException(1405            "aws-eks", "failed to read mounted EKS service account token", e);1406      }14071408      if (token.isEmpty()) {1409        throw new SubjectTokenProviderException(1410            "aws-eks", "mounted EKS service account token is empty", null);1411      }14121413      return token;1414    }14151416    @Override1417    public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {1418      return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));1419    }1420  }14211422  public static void main(String[] args) {1423    WorkloadIdentity workloadIdentity =1424        WorkloadIdentity.builder()1425            .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))1426            .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))1427            .provider(new MountedEksServiceAccountTokenProvider(TOKEN_PATH))1428            .build();14291430    OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();14311432    ResponseCreateParams params =1433        ResponseCreateParams.builder()1434            .model("gpt-5.6-terra")1435            .input("Say hello from AWS workload identity federation.")1436            .build();14371438    client.responses().create(params).output().stream()1439        .flatMap(item -> item.message().stream())1440        .flatMap(message -> message.content().stream())1441        .flatMap(content -> content.outputText().stream())1442        .forEach(outputText -> System.out.println(outputText.text()));1443  }1444}1445```14461447```ruby1448require "openai"14491450TOKEN_PATH = "/var/run/secrets/tokens/token"14511452class MountedEksServiceAccountTokenProvider1453  include OpenAI::Auth::SubjectTokenProvider14541455  def initialize(token_path:)1456    @token_path = token_path1457  end14581459  def token_type1460    OpenAI::Auth::TokenType::JWT1461  end14621463  def get_token1464    token = File.read(@token_path).strip1465    if token.empty?1466      raise OpenAI::Errors::SubjectTokenProviderError.new(1467        message: "Mounted EKS service account token is empty",1468        provider: "aws-eks"1469      )1470    end1471    token1472  rescue SystemCallError => e1473    raise OpenAI::Errors::SubjectTokenProviderError.new(1474      message: "Failed to read mounted EKS service account token: #{e.message}",1475      provider: "aws-eks",1476      cause: e1477    )1478  end1479end14801481provider = MountedEksServiceAccountTokenProvider.new(token_path: TOKEN_PATH)14821483workload_identity = OpenAI::Auth::WorkloadIdentity.new(1484  identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),1485  service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),1486  provider: provider1487)14881489client = OpenAI::Client.new(workload_identity: workload_identity)14901491response = client.responses.create(1492  model: "gpt-5.6-terra",1493  input: "Say hello from AWS workload identity federation."1494)14951496puts(response.output_text)1497```1498149915001501## AWS best practices15021503- Use a dedicated AWS identity per workload. Use separate IAM roles for AWS outbound identity federation and separate Kubernetes service accounts for EKS workloads.1504- Configure a dedicated audience for OpenAI access. Use the same audience value in the AWS-issued or EKS projected token and in the OpenAI Workload Identity Provider configuration.1505- Keep token lifetimes reasonably short. For AWS outbound identity federation, use IAM conditions such as `sts:DurationSeconds`; for EKS, set an appropriate projected token expiration.1506- Prefer exact subject matching. Match on the full IAM principal ARN for AWS outbound tokens or the full Kubernetes service account subject for EKS tokens.1507- Scope mappings to stable boundaries. Use account, organization, namespace, or transformed attributes when they reduce access without creating broad trust rules.1508- Reload tokens when exchanging them. Request AWS outbound tokens when needed, and read EKS projected tokens from the mounted file path so rotated tokens are picked up automatically.1509- Grant only the permissions required by the workload. Use mapping-level permissions to further narrow access granted by the target OpenAI service account.
Files main
Menu

Theme