Repository Public
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.