La federación de identidades saliente de AWS permite que una entidad principal de AWS solicite a AWS STS un JWT de OIDC firmado y lo presente a un servicio externo. En la federación de identidades de cargas de trabajo de OpenAI, el JWT que emite AWS es el token de sujeto que OpenAI valida antes de emitir un token de acceso de OpenAI.
Habilita la federación de identidades saliente en la cuenta de AWS que emitirá los tokens. Para obtener más detalles sobre la configuración, consulta la guía de AWS sobre cómo empezar a usar la federación de identidades saliente.
aws iam enable-outbound-web-identity-federation
Anota la URL del emisor que AWS devuelve para esa cuenta. Configurarás este valor como emisor del proveedor de identidades de cargas de trabajo de OpenAI. Debe coincidir con la declaración iss de los tokens que emite AWS.
La API GetWebIdentityToken de AWS STS no está disponible en el punto de acceso global de STS. Configura la CLI o el SDK de AWS para que use un punto de acceso regional de STS.
Otorga a la carga de trabajo permiso para llamar a sts:GetWebIdentityToken. Restringe la audiencia y la duración máxima del token en IAM para que la entidad principal de AWS solo pueda generar tokens destinados a OpenAI. Este ejemplo permite generar tokens para la audiencia https://api.openai.com/v1 con una duración máxima de 300 segundos:
123456789101112131415161718{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:GetWebIdentityToken",
"Resource": "*",
"Condition": {
"ForAllValues:StringEquals": {
"sts:IdentityTokenAudience": "https://api.openai.com/v1"
},
"NumericLessThanEquals": {
"sts:DurationSeconds": 300
}
}
}
]
}
Solicita a AWS un token de OIDC con la misma audiencia que configurarás en el proveedor de identidades de cargas de trabajo de OpenAI. Usa ES384, a menos que tu entorno requiera compatibilidad con RS256.
123456789TOKEN=$(aws sts get-web-identity-token \
--audience "https://api.openai.com/v1" \
--signing-algorithm ES384 \
--duration-seconds 300 \
--tags Key=environment,Value=production \
Key=workload,Value=batch-ingest \
--query "WebIdentityToken" \
--output text)
export TOKEN
Antes de configurar la federación de identidades de cargas de trabajo, exporta el token emitido por AWS como TOKEN. Luego ejecuta este script localmente para revisar sus declaraciones:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
throw new Error("JWT payload is not valid Base64URL");
}
const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import base64
import json
import os
import re
def reject_non_json_constant(value):
raise ValueError(f"JWT payload contains non-JSON constant: {value}")
parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
raise ValueError("Expected a compact JWT with three segments")
payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
raise ValueError("JWT payload is not a JSON object")
print(decoded_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"unicode/utf8"
)
func decodeSegment(segment string) (json.RawMessage, error) {
if !isBase64URLSegment(segment) {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
decoded, err := base64.RawURLEncoding.DecodeString(segment)
if err != nil {
return nil, err
}
if base64.RawURLEncoding.EncodeToString(decoded) != segment {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
if !utf8.Valid(decoded) {
return nil, fmt.Errorf("JWT segment is not valid UTF-8")
}
var value json.RawMessage
if err := json.Unmarshal(decoded, &value); err != nil {
return nil, err
}
if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
return nil, fmt.Errorf("JWT segment is not a JSON object")
}
return value, nil
}
func isBase64URLSegment(segment string) bool {
if segment == "" || len(segment)%4 == 1 {
return false
}
for _, character := range segment {
if !('A' <= character && character <= 'Z') &&
!('a' <= character && character <= 'z') &&
!('0' <= character && character <= '9') &&
character != '-' &&
character != '_' {
return false
}
}
return true
}
func main() {
parts := strings.Split(os.Getenv("TOKEN"), ".")
if len(parts) != 3 {
panic("Expected a compact JWT with three segments")
}
payload, err := decodeSegment(parts[1])
if err != nil {
panic(err)
}
formatted, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(formatted))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public final class DecodeJwtPayloadExample {
private static final ObjectMapper JSON =
new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
private DecodeJwtPayloadExample() {}
static String decodeUtf8(byte[] bytes) throws IOException {
try {
return StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
} catch (CharacterCodingException exception) {
throw new IOException("JWT segment is not valid UTF-8", exception);
}
}
static String decodeSegment(String segment) throws IOException {
if (!isBase64UrlSegment(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
byte[] bytes = Base64.getUrlDecoder().decode(segment);
if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
String decoded = decodeUtf8(bytes);
JsonNode value = JSON.readTree(decoded);
if (value == null || value.isMissingNode() || !value.isObject()) {
throw new IOException("JWT segment is not a JSON object");
}
return decoded;
}
static boolean isBase64UrlSegment(String segment) {
if (segment.isEmpty() || segment.length() % 4 == 1) {
return false;
}
return segment
.chars()
.allMatch(
character ->
character >= 'A' && character <= 'Z'
|| character >= 'a' && character <= 'z'
|| character >= '0' && character <= '9'
|| character == '-'
|| character == '_');
}
static String[] requireCompactJwt(String token) {
if (token == null) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
String[] parts = token.split("\\.", -1);
if (parts.length != 3) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
return parts;
}
public static void main(String[] args) throws IOException {
String[] parts = requireCompactJwt(System.getenv("TOKEN"));
System.out.println(decodeSegment(parts[1]));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59using System.Text;
using System.Text.Json;
static string DecodeSegment(string segment)
{
if (
segment.Length % 4 == 1 ||
segment.Any(
character =>
!(
character is >= 'A' and <= 'Z' ||
character is >= 'a' and <= 'z' ||
character is >= '0' and <= '9' ||
character is '-' or '_'
)
)
)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
byte[] decoded = Convert.FromBase64String(
segment.Replace('-', '+').Replace('_', '/') +
new string('=', (4 - segment.Length % 4) % 4)
);
string canonicalSegment = Convert
.ToBase64String(decoded)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
if (canonicalSegment != segment)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
using JsonDocument document = JsonDocument.Parse(decodedJson);
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new FormatException("JWT segment is not a JSON object");
}
return decodedJson;
}
string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
Console.WriteLine(DecodeSegment(parts[1]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26require "base64"
require "json"
parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3
unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
raise "JWT payload is not valid Base64URL"
end
begin
payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
raise "JWT payload is not valid Base64URL"
end
payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?
claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)
puts(payload)
Este comando decodifica la carga útil del JWT sin verificar la firma del token. Usa un decodificador local para los tokens de producción y evita pegarlos en herramientas de terceros.
Una vez decodificada, la carga útil de un token de OIDC emitido por AWS tendrá un aspecto similar al de este ejemplo:
1234567891011121314151617181920{
"iss": "https://abc123-def456-ghi789-jkl012.tokens.sts.global.api.aws",
"aud": "https://api.openai.com/v1",
"sub": "arn:aws:iam::123456789012:role/OpenAIWifRole",
"iat": 1716235422,
"exp": 1716235722,
"jti": "jwt-id-example",
"https://sts.amazonaws.com/": {
"aws_account": "123456789012",
"source_region": "us-west-2",
"org_id": "o-exampleorgid",
"principal_tags": {
"environment": "production"
},
"request_tags": {
"environment": "production",
"workload": "batch-ingest"
}
}
}
Los tokens que emite AWS no siempre incluyen todas las declaraciones específicas de AWS. Las declaraciones que aparecen bajo https://sts.amazonaws.com/ dependen de la entidad principal que hace la llamada, del contexto de la sesión y de las etiquetas de la solicitud.
Verifica las declaraciones que vas a configurar en OpenAI:
iss: debe coincidir con la URL del emisor de la cuenta de AWS que configuraste en el proveedor de identidades de cargas de trabajo de OpenAI.
aud: debe coincidir con la audiencia de GetWebIdentityToken y con la del proveedor de identidades de cargas de trabajo de OpenAI.
sub: identifica el ARN de la entidad principal de IAM que solicitó el token. Es preferible exigir una coincidencia exacta con el ARN del rol.
- Declaraciones específicas de AWS: toma el token decodificado como referencia antes de definir coincidencias por cuenta, organización, etiqueta de entidad principal o etiqueta de solicitud.
Usa la carga útil decodificada para comparar el token que recibiste con el emisor, la audiencia y los valores de asignación configurados en OpenAI. Puedes detectar la mayoría de los problemas de configuración en las declaraciones iss, aud y sub antes de intercambiar el token.
Crea en OpenAI un proveedor de identidades de cargas de trabajo para el emisor de la cuenta de AWS. Luego agrega una asignación de cuenta de servicio que coincida con declaraciones estables del token emitido por AWS.
Configura primero el proveedor de identidades de cargas de trabajo y después crea la asignación de cuenta de servicio.
-
Crea el proveedor de identidades de cargas de trabajo. En Nombre, ingresa un valor único, como aws-outbound-prod. En Descripción, escribe algo que ayude a los administradores a identificar el proveedor, como Production AWS outbound identity federation workloads.
-
Configura el emisor y la audiencia. En URL del emisor de OIDC, ingresa la URL del emisor de la cuenta de AWS que obtuviste al habilitar la federación de identidades saliente. Este valor debe coincidir con la declaración iss del token. En Audiencia, ingresa la misma audiencia que pasaste a GetWebIdentityToken. En este ejemplo, el valor es https://api.openai.com/v1.
-
Usa el descubrimiento de OIDC de AWS. Deja desactivada la opción Usar el JWKS cargado para verificar tokens. OpenAI usa los metadatos de descubrimiento de OIDC y el JWKS del emisor de AWS para verificar el token emitido por AWS.
-
Agrega transformaciones de atributos solo si necesitas atributos derivados para las asignaciones. Para definir coincidencias directamente con el token, puedes usar declaraciones escalares de nivel superior, como sub, aud e iss. Las declaraciones del espacio de nombres de AWS están anidadas bajo https://sts.amazonaws.com/. Para usarlas en asignaciones, primero crea atributos derivados con la notación de corchetes de CEL. Por ejemplo, ingresa aws_environment con la expresión assertion["https://sts.amazonaws.com/"]["principal_tags"]["environment"] para crear openai.aws_environment a partir del token decodificado del ejemplo anterior. Verifica la ruta de la declaración anidada en un token de muestra antes de usarla. Si no se puede evaluar una transformación, no se podrá resolver la asignación. Las declaraciones originales del token cuyo nombre ya empieza por openai. se ignoran al evaluar las claves de asignación con el prefijo openai., salvo que se configure la transformación correspondiente.
-
Crea una asignación de cuenta de servicio. En Nombre, ingresa un valor único dentro del proveedor de identidades de cargas de trabajo, como aws-role-openai-wif. En Descripción, indica qué carga de trabajo puede usar la asignación, por ejemplo, Production AWS role for OpenAI API workload.
-
Define la coincidencia con la entidad principal de AWS. En Clave, ingresa sub y, en Valor, el ARN de la entidad principal de IAM que aparece en el token decodificado, como arn:aws:iam::123456789012:role/OpenAIWifRole. Exigir una coincidencia exacta con la declaración sub ofrece el mayor grado de aislamiento para la federación de identidades saliente de AWS.
-
Agrega condiciones de coincidencia para otras declaraciones si es necesario. Puedes definir coincidencias con cualquier declaración escalar o atributo transformado que esté disponible. Por ejemplo, si necesitas límites de confianza adicionales, usa atributos transformados que se deriven de declaraciones de AWS sobre la cuenta, la organización, las etiquetas de la entidad principal o las etiquetas de la solicitud.
-
Elige el destino en OpenAI. En Proyecto, selecciona el proyecto de OpenAI al que pertenece la cuenta de servicio de destino. En Cuenta de servicio, selecciona la cuenta de servicio de OpenAI que puede usar la carga de trabajo de AWS, como aws-outbound-prod-openai-wif.
-
Limita los permisos de la API si es necesario. Selecciona los Permisos adecuados, como api.model.request y api.vector_store.read, para restringir aún más el acceso que otorgan los tokens generados a partir de esta asignación. Deja el campo de permisos en blanco para evitar una restricción de alcance específica de WIF. El token seguirá otorgando acceso con los permisos de la cuenta de servicio asignada.
Configura tu cliente del SDK de OpenAI para que solicite a AWS STS un token de OIDC emitido por AWS y lo intercambie por un token de acceso emitido por OpenAI.
Establece OPENAI_WIF_AUDIENCE en la misma audiencia que configuraste en el proveedor de identidades de cargas de trabajo de OpenAI. El proveedor de tokens de sujeto llama a la API GetWebIdentityToken de AWS STS con esa audiencia y devuelve el JWT emitido por AWS como token de sujeto. El SDK de OpenAI lo intercambia por un token de acceso emitido por OpenAI.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import { GetWebIdentityTokenCommand, STSClient } from "@aws-sdk/client-sts";
import OpenAI from "openai";
const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;
const audience = process.env.OPENAI_WIF_AUDIENCE;
const awsRegion = process.env.AWS_REGION;
if (!identityProviderId || !serviceAccountId || !audience || !awsRegion) {
throw new Error(
"Set OPENAI_IDENTITY_PROVIDER_ID, OPENAI_SERVICE_ACCOUNT_ID, OPENAI_WIF_AUDIENCE, and AWS_REGION"
);
}
const wifAudience = audience;
const sts = new STSClient({ region: awsRegion });
function awsOutboundWebIdentityTokenProvider() {
return {
tokenType: "jwt",
getToken: async () => {
const response = await sts.send(
new GetWebIdentityTokenCommand({
Audience: [wifAudience],
SigningAlgorithm: "ES384",
DurationSeconds: 300,
})
);
if (!response.WebIdentityToken) {
throw new Error("AWS STS did not return a web identity token.");
}
return response.WebIdentityToken;
},
};
}
const client = new OpenAI({
workloadIdentity: {
identityProviderId,
serviceAccountId,
provider: awsOutboundWebIdentityTokenProvider(),
},
});
const response = await client.responses.create({
model: "gpt-5.6-terra",
input: "Say hello from AWS outbound workload identity federation.",
});
console.log(response.output_text);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import os
import boto3
from openai import OpenAI
from openai.auth import SubjectTokenProvider
def aws_outbound_web_identity_token_provider(audience: str) -> SubjectTokenProvider:
sts = boto3.client("sts", region_name=os.environ["AWS_REGION"])
def get_token() -> str:
response = sts.get_web_identity_token(
Audience=[audience],
SigningAlgorithm="ES384",
DurationSeconds=300,
)
token = response.get("WebIdentityToken", "")
if not token:
raise RuntimeError("AWS STS did not return a web identity token.")
return token
return {"token_type": "jwt", "get_token": get_token}
client = OpenAI(
workload_identity={
"identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
"service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
"provider": aws_outbound_web_identity_token_provider(
os.environ["OPENAI_WIF_AUDIENCE"]
),
},
)
response = client.responses.create(
model="gpt-5.6-terra",
input="Say hello from AWS outbound workload identity federation.",
)
print(response.output_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86package main
import (
"context"
"fmt"
"log"
"os"
awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/auth"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
type awsOutboundWebIdentityTokenProvider struct {
client *sts.Client
audience string
}
func (p awsOutboundWebIdentityTokenProvider) TokenType() auth.SubjectTokenType {
return auth.SubjectTokenTypeJWT
}
func (p awsOutboundWebIdentityTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
output, err := p.client.GetWebIdentityToken(ctx, &sts.GetWebIdentityTokenInput{
Audience: []string{p.audience},
DurationSeconds: awssdk.Int32(300),
SigningAlgorithm: awssdk.String("ES384"),
})
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "aws-outbound",
Message: "failed to request AWS web identity token",
Cause: err,
}
}
token := awssdk.ToString(output.WebIdentityToken)
if token == "" {
return "", &auth.SubjectTokenProviderError{
Provider: "aws-outbound",
Message: "AWS STS did not return a web identity token",
}
}
return token, nil
}
func main() {
ctx := context.Background()
audience := os.Getenv("OPENAI_WIF_AUDIENCE")
if audience == "" {
log.Fatal("Set OPENAI_WIF_AUDIENCE")
}
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Fatal(err)
}
client := openai.NewClient(
option.WithWorkloadIdentity(auth.WorkloadIdentity{
IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
Provider: awsOutboundWebIdentityTokenProvider{
client: sts.NewFromConfig(cfg),
audience: audience,
},
}),
)
response, err := client.Responses.New(ctx, responses.ResponseNewParams{
Model: openai.ChatModelGPT4_1Mini,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("Say hello from AWS outbound workload identity federation."),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.OutputText())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.HttpClient;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.models.responses.ResponseCreateParams;
import java.util.concurrent.CompletableFuture;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.model.GetWebIdentityTokenRequest;
public final class AwsOutboundWorkloadIdentityExample {
private AwsOutboundWorkloadIdentityExample() {}
static final class AwsOutboundWebIdentityTokenProvider implements SubjectTokenProvider {
private final StsClient stsClient;
private final String audience;
AwsOutboundWebIdentityTokenProvider(StsClient stsClient, String audience) {
this.stsClient = stsClient;
this.audience = audience;
}
@Override
public SubjectTokenType tokenType() {
return SubjectTokenType.JWT;
}
@Override
public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
try {
String token =
stsClient
.getWebIdentityToken(
GetWebIdentityTokenRequest.builder()
.audience(audience)
.durationSeconds(300)
.signingAlgorithm("ES384")
.build())
.webIdentityToken();
if (token == null || token.isEmpty()) {
throw new SubjectTokenProviderException(
"aws-outbound", "AWS STS did not return a web identity token", null);
}
return token;
} catch (SubjectTokenProviderException e) {
throw e;
} catch (Exception e) {
throw new SubjectTokenProviderException(
"aws-outbound", "failed to request AWS web identity token", e);
}
}
@Override
public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
}
}
public static void main(String[] args) {
String audience = System.getenv("OPENAI_WIF_AUDIENCE");
StsClient stsClient =
StsClient.builder().region(Region.of(System.getenv("AWS_REGION"))).build();
WorkloadIdentity workloadIdentity =
WorkloadIdentity.builder()
.identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
.serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
.provider(new AwsOutboundWebIdentityTokenProvider(stsClient, audience))
.build();
OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-5.6-terra")
.input("Say hello from AWS outbound workload identity federation.")
.build();
client.responses().create(params).output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(outputText -> System.out.println(outputText.text()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57require "aws-sdk-sts"
require "openai"
class AwsOutboundWebIdentityTokenProvider
include OpenAI::Auth::SubjectTokenProvider
def initialize(audience:, sts_client:)
@audience = audience
@sts_client = sts_client
end
def token_type
OpenAI::Auth::TokenType::JWT
end
def get_token
response = @sts_client.get_web_identity_token(
audience: [@audience],
signing_algorithm: "ES384",
duration_seconds: 300
)
token = response.web_identity_token.to_s
if token.empty?
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "AWS STS did not return a web identity token",
provider: "aws-outbound"
)
end
token
rescue Aws::STS::Errors::ServiceError => e
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Failed to request AWS web identity token: #{e.message}",
provider: "aws-outbound",
cause: e
)
end
end
provider = AwsOutboundWebIdentityTokenProvider.new(
audience: ENV.fetch("OPENAI_WIF_AUDIENCE"),
sts_client: Aws::STS::Client.new(region: ENV.fetch("AWS_REGION"))
)
workload_identity = OpenAI::Auth::WorkloadIdentity.new(
identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
provider: provider
)
client = OpenAI::Client.new(workload_identity: workload_identity)
response = client.responses.create(
model: "gpt-5.6-terra",
input: "Say hello from AWS outbound workload identity federation."
)
puts(response.output_text)
Para usar Amazon EKS como proveedor de identidades de cargas de trabajo, intercambia un token proyectado de cuenta de servicio emitido por EKS por un token de acceso de OpenAI de corta duración.
Usa una ServiceAccount de Kubernetes para la carga de trabajo de EKS que necesita llamar a la API de OpenAI. Si aún no tienes una, créala:
kubectl create serviceaccount openai-wif --namespace default
Los tokens proyectados de cuentas de servicio de EKS usan una declaración sub con el formato system:serviceaccount:<namespace>:<service-account-name>. Para la cuenta de servicio del ejemplo anterior, la declaración sub es system:serviceaccount:default:openai-wif.
Obtén la URL del emisor de OIDC asociado al clúster de EKS:
12345aws eks describe-cluster \
--name <cluster-name> \
--region <region> \
--query "cluster.identity.oidc.issuer" \
--output text
Ejemplo de salida:
https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3
El emisor que configures en el proveedor de identidades de cargas de trabajo de OpenAI debe coincidir con esta URL y con la declaración iss del token proyectado de la cuenta de servicio de EKS.
Configura el token proyectado de la cuenta de servicio con la audiencia que espera OpenAI y un plazo de vencimiento adecuado para tu carga de trabajo. OpenAI valida el emisor, la firma, la audiencia y el vencimiento del token. En este ejemplo, el archivo del token se monta en /var/run/secrets/tokens/token. El token usa la audiencia https://api.openai.com/v1 y vence después de 3600 segundos. Puedes usar otra audiencia siempre que coincidan la del token proyectado y la del proveedor de identidades de cargas de trabajo de OpenAI:
12345678910111213141516171819202122apiVersion: v1
kind: Pod
metadata:
name: openai-wif-app
namespace: default
spec:
serviceAccountName: openai-wif
containers:
- name: app
image: my-image
volumeMounts:
- name: eks-sa-token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: eks-sa-token
projected:
sources:
- serviceAccountToken:
path: token
audience: "https://api.openai.com/v1"
expirationSeconds: 3600
Antes de configurar la federación de identidades de cargas de trabajo, decodifica localmente un token proyectado de cuenta de servicio y revisa sus declaraciones. Para obtener una muestra, lee el token de un pod en ejecución que tenga montado el archivo del token proyectado y expórtalo como TOKEN:
TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)
export TOKEN
Luego ejecuta este script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const parts = process.env.TOKEN?.split(".") ?? [];
if (parts.length !== 3) {
throw new Error("Expected a compact JWT with three segments");
}
if (!/^[A-Za-z0-9_-]+$/.test(parts[1]) || parts[1].length % 4 === 1) {
throw new Error("JWT payload is not valid Base64URL");
}
const bytes = Buffer.from(parts[1], "base64url");
if (bytes.toString("base64url") !== parts[1]) {
throw new Error("JWT payload is not valid Base64URL");
}
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
const claims = JSON.parse(decoded);
if (claims === null || Array.isArray(claims) || typeof claims !== "object") {
throw new Error("JWT payload is not a JSON object");
}
console.log(decoded);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import base64
import json
import os
import re
def reject_non_json_constant(value):
raise ValueError(f"JWT payload contains non-JSON constant: {value}")
parts = os.environ.get("TOKEN", "").split(".")
if len(parts) != 3:
raise ValueError("Expected a compact JWT with three segments")
payload = parts[1]
if re.fullmatch(r"[A-Za-z0-9_-]+", payload) is None or len(payload) % 4 == 1:
raise ValueError("JWT payload is not valid Base64URL")
padded_payload = payload + "=" * (-len(payload) % 4)
decoded = base64.b64decode(padded_payload, altchars=b"-_", validate=True)
if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != payload:
raise ValueError("JWT payload is not valid Base64URL")
decoded_text = decoded.decode("utf-8")
claims = json.loads(decoded_text, parse_constant=reject_non_json_constant)
if not isinstance(claims, dict):
raise ValueError("JWT payload is not a JSON object")
print(decoded_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"unicode/utf8"
)
func decodeSegment(segment string) (json.RawMessage, error) {
if !isBase64URLSegment(segment) {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
decoded, err := base64.RawURLEncoding.DecodeString(segment)
if err != nil {
return nil, err
}
if base64.RawURLEncoding.EncodeToString(decoded) != segment {
return nil, fmt.Errorf("JWT segment is not valid Base64URL")
}
if !utf8.Valid(decoded) {
return nil, fmt.Errorf("JWT segment is not valid UTF-8")
}
var value json.RawMessage
if err := json.Unmarshal(decoded, &value); err != nil {
return nil, err
}
if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
return nil, fmt.Errorf("JWT segment is not a JSON object")
}
return value, nil
}
func isBase64URLSegment(segment string) bool {
if segment == "" || len(segment)%4 == 1 {
return false
}
for _, character := range segment {
if !('A' <= character && character <= 'Z') &&
!('a' <= character && character <= 'z') &&
!('0' <= character && character <= '9') &&
character != '-' &&
character != '_' {
return false
}
}
return true
}
func main() {
parts := strings.Split(os.Getenv("TOKEN"), ".")
if len(parts) != 3 {
panic("Expected a compact JWT with three segments")
}
payload, err := decodeSegment(parts[1])
if err != nil {
panic(err)
}
formatted, err := json.MarshalIndent(payload, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(formatted))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public final class DecodeJwtPayloadExample {
private static final ObjectMapper JSON =
new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
private DecodeJwtPayloadExample() {}
static String decodeUtf8(byte[] bytes) throws IOException {
try {
return StandardCharsets.UTF_8
.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString();
} catch (CharacterCodingException exception) {
throw new IOException("JWT segment is not valid UTF-8", exception);
}
}
static String decodeSegment(String segment) throws IOException {
if (!isBase64UrlSegment(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
byte[] bytes = Base64.getUrlDecoder().decode(segment);
if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
throw new IllegalArgumentException("JWT segment is not valid Base64URL");
}
String decoded = decodeUtf8(bytes);
JsonNode value = JSON.readTree(decoded);
if (value == null || value.isMissingNode() || !value.isObject()) {
throw new IOException("JWT segment is not a JSON object");
}
return decoded;
}
static boolean isBase64UrlSegment(String segment) {
if (segment.isEmpty() || segment.length() % 4 == 1) {
return false;
}
return segment
.chars()
.allMatch(
character ->
character >= 'A' && character <= 'Z'
|| character >= 'a' && character <= 'z'
|| character >= '0' && character <= '9'
|| character == '-'
|| character == '_');
}
static String[] requireCompactJwt(String token) {
if (token == null) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
String[] parts = token.split("\\.", -1);
if (parts.length != 3) {
throw new IllegalArgumentException("Expected a compact JWT with three segments");
}
return parts;
}
public static void main(String[] args) throws IOException {
String[] parts = requireCompactJwt(System.getenv("TOKEN"));
System.out.println(decodeSegment(parts[1]));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59using System.Text;
using System.Text.Json;
static string DecodeSegment(string segment)
{
if (
segment.Length % 4 == 1 ||
segment.Any(
character =>
!(
character is >= 'A' and <= 'Z' ||
character is >= 'a' and <= 'z' ||
character is >= '0' and <= '9' ||
character is '-' or '_'
)
)
)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
byte[] decoded = Convert.FromBase64String(
segment.Replace('-', '+').Replace('_', '/') +
new string('=', (4 - segment.Length % 4) % 4)
);
string canonicalSegment = Convert
.ToBase64String(decoded)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
if (canonicalSegment != segment)
{
throw new FormatException("JWT segment is not valid Base64URL");
}
string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
using JsonDocument document = JsonDocument.Parse(decodedJson);
if (document.RootElement.ValueKind is not JsonValueKind.Object)
{
throw new FormatException("JWT segment is not a JSON object");
}
return decodedJson;
}
string? token = Environment.GetEnvironmentVariable("TOKEN");
if (token is null)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
string[] parts = token.Split('.');
if (parts.Length != 3)
{
throw new InvalidOperationException(
"Expected a compact JWT with three segments"
);
}
Console.WriteLine(DecodeSegment(parts[1]));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26require "base64"
require "json"
parts = ENV.fetch("TOKEN", "").split(".", -1)
raise "Expected a compact JWT with three segments" unless parts.length == 3
unless parts[1].match?(/\A[A-Za-z0-9_-]+\z/) && parts[1].length % 4 != 1
raise "JWT payload is not valid Base64URL"
end
begin
payload = Base64.urlsafe_decode64(parts[1].ljust((parts[1].length + 3) & ~3, "="))
rescue ArgumentError
raise "JWT payload is not valid Base64URL"
end
unless Base64.urlsafe_encode64(payload, padding: false) == parts[1]
raise "JWT payload is not valid Base64URL"
end
payload.force_encoding(Encoding::UTF_8)
raise "JWT payload is not valid UTF-8" unless payload.valid_encoding?
claims = JSON.parse(payload)
raise "JWT payload is not a JSON object" unless claims.is_a?(Hash)
puts(payload)
Este comando decodifica la carga útil del JWT sin verificar la firma del token. Usa un decodificador local para los tokens de producción y evita pegarlos en herramientas de terceros.
Una vez decodificada, la carga útil de un token proyectado de cuenta de servicio de EKS tendrá un aspecto similar al de este ejemplo:
1234567891011121314{
"iss": "https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3",
"aud": ["https://api.openai.com/v1"],
"sub": "system:serviceaccount:default:openai-wif",
"iat": 1716235422,
"exp": 1716239022,
"kubernetes.io": {
"namespace": "default",
"serviceaccount": {
"name": "openai-wif",
"uid": "11111111-2222-3333-4444-555555555555"
}
}
}
Usa la carga útil decodificada para comparar el token que recibiste con el emisor, la audiencia y los valores de asignación configurados en OpenAI. Puedes detectar la mayoría de los problemas de configuración en las declaraciones iss, aud y sub antes de intercambiar el token.
Crea en OpenAI un proveedor de identidades de cargas de trabajo para el emisor de EKS. Luego agrega una asignación de cuenta de servicio que coincida con atributos del token proyectado.
Configura primero el proveedor de identidades de cargas de trabajo y después crea la asignación de cuenta de servicio.
-
Crea el proveedor de identidades de cargas de trabajo. En Nombre, ingresa un valor único, como aws-eks-prod. En Descripción, escribe algo que ayude a los administradores a identificar el clúster, como Production EKS cluster.
-
Configura el emisor y la audiencia. En URL del emisor de OIDC, ingresa el emisor que devuelve aws eks describe-cluster --query "cluster.identity.oidc.issuer". Este valor debe coincidir con la declaración iss del token proyectado de la cuenta de servicio de EKS. En Audiencia, ingresa la misma audiencia que configuraste en el volumen del token proyectado de la cuenta de servicio. En este ejemplo, el valor es https://api.openai.com/v1.
-
Usa el descubrimiento de OIDC de EKS. Deja desactivada la opción Usar el JWKS cargado para verificar tokens. OpenAI usa los metadatos de descubrimiento de OIDC y el JWKS del emisor de EKS para verificar el token proyectado de la cuenta de servicio.
-
Agrega transformaciones de atributos solo si necesitas atributos derivados para las asignaciones. Puedes usar declaraciones originales del token, como sub, aud e iss, directamente en las aserciones de asignación. Por ejemplo, crea un atributo transformado llamado subject con la expresión assertion.sub. En el panel, ingresa subject como nombre del atributo. OpenAI lo guarda como openai.subject, y puedes usar ese nombre en las asignaciones.
Nota: las declaraciones originales del token cuyo nombre ya empieza por openai. se ignoran al evaluar las claves de asignación con el prefijo openai., salvo que se configure la transformación correspondiente.
-
Crea una asignación de cuenta de servicio. En Nombre, ingresa un valor único dentro del proveedor de identidades de cargas de trabajo, como openai-mapping-eks. En Descripción, indica qué carga de trabajo puede usar la asignación, por ejemplo, Workload Identity Provider Mapping for EKS Workloads.
-
Define la coincidencia con el sujeto de la cuenta de servicio de EKS. En Clave, ingresa sub y, en Valor, system:serviceaccount:default:openai-wif. Puedes definir coincidencias con cualquier declaración o atributo transformado que esté disponible. Exigir una coincidencia con sub es la opción más restrictiva porque identifica de forma única una cuenta de servicio de Kubernetes.
-
Elige el destino en OpenAI. En Proyecto, selecciona el proyecto de OpenAI al que pertenece la cuenta de servicio de destino. En Cuenta de servicio, selecciona la cuenta de servicio de OpenAI que puede usar la carga de trabajo de EKS, como aws-eks-prod-openai-wif. Marca Create a new service account in this project si quieres crear una cuenta de servicio para esta asignación en lugar de reutilizar una existente.
-
Limita los permisos de la API si es necesario. Selecciona los Permisos adecuados, como api.model.request y api.vector_store.read, para restringir aún más el acceso que otorgan los tokens generados a partir de esta asignación. Deja el campo de permisos en blanco para evitar una restricción de alcance específica de WIF. El token seguirá otorgando acceso con los permisos de la cuenta de servicio asignada.
Configura tu cliente del SDK de OpenAI para que lea el token proyectado de la cuenta de servicio de EKS y lo intercambie por un token de acceso emitido por OpenAI.
Para indicar el origen del token de sujeto al proveedor de federación de identidades de cargas de trabajo del SDK, usa la ruta del archivo montado, como /var/run/secrets/tokens/token. El SDK intercambia ese token de EKS por un token de acceso emitido por OpenAI y usa el token de OpenAI para autenticar las solicitudes a la API.
Los siguientes ejemplos inicializan un cliente de OpenAI con un proveedor personalizado de tokens de sujeto. El proveedor lee el token proyectado de la cuenta de servicio de EKS desde la ruta del archivo montado y lo usa como token de sujeto para la federación de identidades de cargas de trabajo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import { readFile } from "node:fs/promises";
import OpenAI from "openai";
const tokenPath = "/var/run/secrets/tokens/token";
const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;
if (!identityProviderId || !serviceAccountId) {
throw new Error(
"Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"
);
}
function mountedEksServiceAccountTokenProvider(path) {
return {
tokenType: "jwt",
getToken: async () => {
const token = (await readFile(path, "utf8")).trim();
if (!token) {
throw new Error("The mounted EKS service account token file is empty.");
}
return token;
},
};
}
const client = new OpenAI({
workloadIdentity: {
identityProviderId,
serviceAccountId,
provider: mountedEksServiceAccountTokenProvider(tokenPath),
},
});
const response = await client.responses.create({
model: "gpt-5.6-terra",
input: "Say hello from AWS workload identity federation.",
});
console.log(response.output_text);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import os
from pathlib import Path
from openai import OpenAI
from openai.auth import SubjectTokenProvider
TOKEN_PATH = "/var/run/secrets/tokens/token"
def mounted_eks_service_account_token_provider(token_path: str) -> SubjectTokenProvider:
def get_token() -> str:
token = Path(token_path).read_text().strip()
if not token:
raise RuntimeError("The mounted EKS service account token file is empty.")
return token
return {"token_type": "jwt", "get_token": get_token}
client = OpenAI(
workload_identity={
"identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
"service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
"provider": mounted_eks_service_account_token_provider(TOKEN_PATH),
},
)
response = client.responses.create(
model="gpt-5.6-terra",
input="Say hello from AWS workload identity federation.",
)
print(response.output_text)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/auth"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
)
const tokenPath = "/var/run/secrets/tokens/token"
type mountedEksServiceAccountTokenProvider struct {
path string
}
func (p mountedEksServiceAccountTokenProvider) TokenType() auth.SubjectTokenType {
return auth.SubjectTokenTypeJWT
}
func (p mountedEksServiceAccountTokenProvider) GetToken(_ context.Context, _ auth.HTTPDoer) (string, error) {
data, err := os.ReadFile(p.path)
if err != nil {
return "", &auth.SubjectTokenProviderError{
Provider: "aws-eks",
Message: "failed to read mounted EKS service account token",
Cause: err,
}
}
token := strings.TrimSpace(string(data))
if token == "" {
return "", &auth.SubjectTokenProviderError{
Provider: "aws-eks",
Message: "mounted EKS service account token is empty",
}
}
return token, nil
}
func main() {
client := openai.NewClient(
option.WithWorkloadIdentity(auth.WorkloadIdentity{
IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
Provider: mountedEksServiceAccountTokenProvider{
path: tokenPath,
},
}),
)
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: openai.ChatModelGPT4_1Mini,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("Say hello from AWS workload identity federation."),
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(response.OutputText())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77import com.fasterxml.jackson.databind.json.JsonMapper;
import com.openai.auth.SubjectTokenProvider;
import com.openai.auth.SubjectTokenType;
import com.openai.auth.WorkloadIdentity;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.http.HttpClient;
import com.openai.errors.SubjectTokenProviderException;
import com.openai.models.responses.ResponseCreateParams;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
public final class AwsEksWorkloadIdentityExample {
private static final String TOKEN_PATH = "/var/run/secrets/tokens/token";
private AwsEksWorkloadIdentityExample() {}
static final class MountedEksServiceAccountTokenProvider implements SubjectTokenProvider {
private final Path tokenPath;
MountedEksServiceAccountTokenProvider(String tokenPath) {
this.tokenPath = Path.of(tokenPath);
}
@Override
public SubjectTokenType tokenType() {
return SubjectTokenType.JWT;
}
@Override
public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
String token;
try {
token = Files.readString(tokenPath).trim();
} catch (Exception e) {
throw new SubjectTokenProviderException(
"aws-eks", "failed to read mounted EKS service account token", e);
}
if (token.isEmpty()) {
throw new SubjectTokenProviderException(
"aws-eks", "mounted EKS service account token is empty", null);
}
return token;
}
@Override
public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
}
}
public static void main(String[] args) {
WorkloadIdentity workloadIdentity =
WorkloadIdentity.builder()
.identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
.serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
.provider(new MountedEksServiceAccountTokenProvider(TOKEN_PATH))
.build();
OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-5.6-terra")
.input("Say hello from AWS workload identity federation.")
.build();
client.responses().create(params).output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(outputText -> System.out.println(outputText.text()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49require "openai"
TOKEN_PATH = "/var/run/secrets/tokens/token"
class MountedEksServiceAccountTokenProvider
include OpenAI::Auth::SubjectTokenProvider
def initialize(token_path:)
@token_path = token_path
end
def token_type
OpenAI::Auth::TokenType::JWT
end
def get_token
token = File.read(@token_path).strip
if token.empty?
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Mounted EKS service account token is empty",
provider: "aws-eks"
)
end
token
rescue SystemCallError => e
raise OpenAI::Errors::SubjectTokenProviderError.new(
message: "Failed to read mounted EKS service account token: #{e.message}",
provider: "aws-eks",
cause: e
)
end
end
provider = MountedEksServiceAccountTokenProvider.new(token_path: TOKEN_PATH)
workload_identity = OpenAI::Auth::WorkloadIdentity.new(
identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
provider: provider
)
client = OpenAI::Client.new(workload_identity: workload_identity)
response = client.responses.create(
model: "gpt-5.6-terra",
input: "Say hello from AWS workload identity federation."
)
puts(response.output_text)