La fédération d’identités sortante AWS permet à un principal AWS de demander un JWT OIDC signé à AWS STS et de le présenter à un service externe. Dans la fédération d’identités de charge de travail OpenAI, le JWT émis par AWS est le jeton de sujet qu’OpenAI valide avant d’émettre un jeton d’accès OpenAI.
Activez la fédération d’identités sortante pour le compte AWS qui émettra les jetons. Pour en savoir plus sur la configuration, consultez le guide AWS pour bien démarrer avec la fédération d’identités sortante.
aws iam enable-outbound-web-identity-federation
Notez l’URL de l’émetteur propre au compte renvoyée par AWS. Vous utiliserez cette valeur pour configurer l’émetteur du fournisseur d’identités de charge de travail dans OpenAI. Elle doit correspondre à la revendication iss des jetons émis par AWS.
L’API GetWebIdentityToken d’AWS STS n’est pas disponible sur le point de terminaison
global STS. Configurez la CLI ou le SDK AWS pour utiliser un point de terminaison régional STS.
Accordez à la charge de travail l’autorisation d’appeler sts:GetWebIdentityToken. Limitez l’audience et la durée de vie maximale des jetons dans IAM afin que le principal AWS ne puisse émettre que des jetons destinés à OpenAI. Cet exemple autorise les jetons pour l’audience https://api.openai.com/v1 avec une durée de vie maximale de 300 secondes :
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
}
}
}
]
}
Demandez un jeton OIDC émis par AWS avec la même audience que celle que vous configurerez sur le fournisseur d’identités de charge de travail dans OpenAI. Utilisez ES384, sauf si votre environnement exige la compatibilité avec 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
Avant de configurer la fédération d’identités de charge de travail, exportez le jeton émis par AWS dans la variable TOKEN, puis exécutez ce script localement pour inspecter ses revendications :
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)
Cette commande décode la charge utile du JWT sans vérifier la signature du jeton. Utilisez un décodeur local pour les jetons de production et évitez de les coller dans des outils tiers.
Un jeton OIDC émis par AWS, une fois décodé, ressemble à ceci :
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"
}
}
}
Les jetons émis par AWS ne contiennent pas tous l’ensemble des revendications propres à AWS. Les revendications sous https://sts.amazonaws.com/ dépendent du principal appelant, du contexte de session et des balises de requête.
Vérifiez les revendications que vous prévoyez de configurer dans OpenAI :
iss : doit correspondre à l’URL de l’émetteur propre au compte AWS configurée dans le fournisseur d’identités de charge de travail dans OpenAI.
aud : doit correspondre à l’audience de GetWebIdentityToken et à celle du fournisseur d’identités de charge de travail dans OpenAI.
sub : identifie l’ARN du principal IAM qui a demandé le jeton. Privilégiez une correspondance exacte avec l’ARN du rôle.
- Revendications propres à AWS : utilisez le jeton décodé comme référence avant de définir des correspondances sur les valeurs de compte, d’organisation, de balise de principal ou de balise de requête.
Utilisez la charge utile décodée pour comparer le jeton reçu aux valeurs d’émetteur, d’audience et de correspondance configurées dans OpenAI. La plupart des problèmes de configuration sont visibles dans les revendications iss, aud et sub avant l’échange du jeton.
Créez un fournisseur d’identités de charge de travail dans OpenAI pour l’émetteur du compte AWS, puis ajoutez une correspondance de compte de service basée sur des revendications stables du jeton émis par AWS.
Configurez d’abord le fournisseur d’identités de charge de travail, puis créez la correspondance de compte de service.
-
Créez le fournisseur d’identités de charge de travail. Renseignez le champ Nom avec une valeur unique, telle que aws-outbound-prod. Renseignez le champ Description, par exemple avec Production AWS outbound identity federation workloads, pour aider les administrateurs à identifier le fournisseur.
-
Définissez l’émetteur et l’audience. Renseignez le champ URL de l’émetteur OIDC avec l’URL de l’émetteur propre au compte AWS renvoyée lors de l’activation de la fédération d’identités sortante. Cette valeur doit correspondre à la revendication iss du jeton. Renseignez le champ Audience avec la même audience que celle transmise à GetWebIdentityToken. Dans cet exemple, cette valeur est https://api.openai.com/v1.
-
Utilisez la découverte OIDC AWS. Laissez l’option Utiliser le JWKS importé pour vérifier les jetons désactivée. OpenAI utilise les métadonnées de découverte OIDC et le JWKS de l’émetteur AWS pour vérifier le jeton émis par AWS.
-
Ajoutez des transformations d’attributs uniquement si vous avez besoin d’attributs dérivés pour les correspondances. La mise en correspondance directe des jetons prend en charge les revendications scalaires de premier niveau, telles que sub, aud et iss. Les revendications propres à AWS, regroupées dans un espace de noms, sont imbriquées sous https://sts.amazonaws.com/. Créez donc des attributs dérivés avec la notation entre crochets de CEL avant de les utiliser dans les correspondances. Par exemple, saisissez aws_environment avec l’expression assertion["https://sts.amazonaws.com/"]["principal_tags"]["environment"] pour créer openai.aws_environment à partir de l’exemple de jeton décodé ci-dessus. Vérifiez le chemin de la revendication imbriquée dans un exemple de jeton avant de l’utiliser ; si une transformation ne peut pas être évaluée, la résolution de la correspondance échoue. Les revendications brutes du jeton dont le nom commence déjà par openai. sont ignorées pour les clés de correspondance openai., sauf si une transformation correspondante est configurée.
-
Créez une correspondance de compte de service. Renseignez le champ Nom avec une valeur unique au sein du fournisseur d’identités de charge de travail, telle que aws-role-openai-wif. Renseignez le champ Description, par exemple avec Production AWS role for OpenAI API workload, pour indiquer quelle charge de travail peut utiliser cette correspondance.
-
Définissez une correspondance pour le principal AWS. Renseignez le champ Clé avec sub et le champ Valeur avec l’ARN du principal IAM présent dans le jeton décodé, tel que arn:aws:iam::123456789012:role/OpenAIWifRole. Une correspondance exacte sur la revendication sub offre l’isolation la plus forte pour la fédération d’identités sortante AWS.
-
Ajoutez des critères de correspondance sur d’autres revendications si nécessaire. Vous pouvez définir une correspondance sur n’importe quelle revendication scalaire ou n’importe quel attribut transformé disponible. Par exemple, utilisez des attributs transformés dérivés des revendications de compte AWS, d’organisation, de balise de principal ou de balise de requête si vous avez besoin de délimiter davantage le périmètre de confiance.
-
Choisissez la cible OpenAI. Dans le champ Projet , sélectionnez le projet OpenAI auquel appartient le compte de service cible. Dans le champ Compte de service , sélectionnez le compte de service OpenAI que la charge de travail AWS peut utiliser, tel que aws-outbound-prod-openai-wif.
-
Restreignez les autorisations de l’API si nécessaire. Sélectionnez les Autorisations appropriées, telles que api.model.request et api.vector_store.read, pour restreindre davantage les droits des jetons d’accès émis à partir de cette correspondance. Laissez ce champ vide pour ne pas ajouter de restriction de portée propre à WIF ; le jeton continue d’autoriser l’accès au nom du compte de service associé.
Configurez votre client du SDK OpenAI pour demander un jeton OIDC émis par AWS à AWS STS et l’échanger contre un jeton d’accès émis par OpenAI.
Définissez OPENAI_WIF_AUDIENCE sur la même audience que celle configurée sur le fournisseur d’identités de charge de travail dans OpenAI. Le fournisseur de jetons de sujet appelle GetWebIdentityToken d’AWS STS avec cette audience et renvoie le JWT émis par AWS comme jeton de sujet. Le SDK OpenAI l’échange ensuite contre un jeton d’accès émis par 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)
Utilisez Amazon EKS comme fournisseur d’identités de charge de travail en échangeant un jeton de compte de service projeté émis par EKS contre un jeton d’accès OpenAI à courte durée de vie.
Utilisez un ServiceAccount Kubernetes pour la charge de travail EKS qui doit appeler l’API OpenAI. Si vous n’en avez pas encore, créez-en un :
kubectl create serviceaccount openai-wif --namespace default
Les jetons de compte de service projetés EKS utilisent une revendication sub au format system:serviceaccount:<namespace>:<service-account-name>. Pour le compte de service ci-dessus, la revendication sub vaut system:serviceaccount:default:openai-wif.
Récupérez l’URL de l’émetteur OIDC associée au cluster EKS :
12345aws eks describe-cluster \
--name <cluster-name> \
--region <region> \
--query "cluster.identity.oidc.issuer" \
--output text
Exemple de sortie :
https://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLED539D4633E53DE1B716D3
L’émetteur que vous configurez dans le fournisseur d’identités de charge de travail dans OpenAI doit correspondre à cette URL d’émetteur et à la revendication iss du jeton de compte de service projeté EKS.
Configurez le jeton de compte de service projeté avec l’audience attendue par OpenAI et une durée de validité adaptée à votre charge de travail. OpenAI valide l’émetteur, la signature, l’audience et l’expiration du jeton. Dans cet exemple, le fichier du jeton est monté à l’emplacement /var/run/secrets/tokens/token, le jeton utilise l’audience https://api.openai.com/v1 et expire au bout de 3600 secondes. Vous pouvez utiliser une autre audience à condition que celle du jeton projeté corresponde à celle du fournisseur d’identités de charge de travail dans 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
Avant de configurer la fédération d’identités de charge de travail, décodez localement un exemple de jeton de compte de service projeté et inspectez ses revendications. Depuis un pod en cours d’exécution dans lequel le jeton projeté est monté, récupérez le jeton et exportez-le dans la variable TOKEN :
TOKEN=$(kubectl exec -n default openai-wif-app -- cat /var/run/secrets/tokens/token)
export TOKEN
Exécutez ensuite ce 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)
Cette commande décode la charge utile du JWT sans vérifier la signature du token. Utilisez un décodeur local pour les tokens de production et évitez de les coller dans des outils tiers.
Une fois décodé, un token de compte de service projeté EKS ressemble à ceci :
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"
}
}
}
Utilisez la charge utile décodée pour comparer le token reçu aux valeurs d’émetteur, d’audience et de mappage configurées dans OpenAI. La plupart des problèmes de configuration sont visibles dans les revendications iss, aud et sub avant l’échange du token.
Créez un fournisseur d’identités de charge de travail dans OpenAI pour l’émetteur EKS, puis ajoutez un mappage de compte de service basé sur les attributs du token projeté.
Configurez d’abord le fournisseur d’identités de charge de travail, puis créez le mappage de compte de service.
-
Créez le fournisseur d’identités de charge de travail. Définissez Nom sur une valeur unique, par exemple aws-eks-prod. Renseignez le champ Description, par exemple avec Production EKS cluster, pour aider les administrateurs à identifier le cluster.
-
Définissez l’émetteur et l’audience. Définissez URL de l’émetteur OIDC sur l’émetteur renvoyé par aws eks describe-cluster --query "cluster.identity.oidc.issuer". Cette valeur doit correspondre à la revendication iss du token de compte de service projeté EKS. Définissez Audience sur la même audience que celle configurée dans le volume du token de compte de service projeté. Dans cet exemple, cette valeur est https://api.openai.com/v1.
-
Utilisez la découverte OIDC d’EKS. Laissez l’option Utiliser le JWKS téléversé pour vérifier les tokens désactivée. OpenAI utilise les métadonnées de découverte OIDC et le JWKS de l’émetteur EKS pour vérifier le token de compte de service projeté.
-
Ajoutez des transformations d’attributs uniquement si vous avez besoin d’attributs dérivés pour les mappages. Les revendications brutes du token, telles que sub, aud et iss, peuvent être utilisées directement dans les assertions de mappage. Par exemple, créez un attribut transformé nommé subject avec l’expression assertion.sub. Dans le tableau de bord, saisissez subject comme nom d’attribut ; OpenAI l’enregistre sous le nom openai.subject, que vous pouvez référencer dans les mappages.
Remarque : Les revendications brutes du token dont le nom commence déjà par openai. sont ignorées pour les clés de mappage commençant par openai., sauf si une transformation correspondante est configurée.
-
Créez un mappage de compte de service. Définissez Nom sur une valeur unique au sein du fournisseur d’identités de charge de travail, par exemple openai-mapping-eks. Renseignez le champ Description, par exemple avec Workload Identity Provider Mapping for EKS Workloads, pour préciser quelle charge de travail peut utiliser ce mappage.
-
Définissez une correspondance avec le sujet du compte de service EKS. Définissez Clé sur sub et Valeur sur system:serviceaccount:default:openai-wif. Vous pouvez définir une correspondance avec n’importe quelle revendication ou n’importe quel attribut transformé disponible. La correspondance sur sub est l’option la plus restrictive, car cette revendication identifie de manière unique un compte de service Kubernetes.
-
Choisissez la cible OpenAI. Définissez Projet sur le projet OpenAI auquel appartient le compte de service cible. Définissez Compte de service sur le compte de service OpenAI que la charge de travail EKS peut utiliser, par exemple aws-eks-prod-openai-wif. Cochez Create a new service account in this project si vous souhaitez créer un compte de service pour ce mappage plutôt que réutiliser un compte existant.
-
Restreignez les autorisations de l’API si nécessaire. Sélectionnez les Autorisations appropriées, telles que api.model.request et api.vector_store.read, pour restreindre davantage la portée des jetons d’accès émis à partir de ce mappage. Laissez les autorisations vides pour ne pas ajouter de restriction de portée propre à la WIF ; le token continue d’accorder l’accès au nom du compte de service associé.
Configurez votre client du SDK OpenAI pour qu’il lise le token de compte de service projeté EKS et l’échange contre un jeton d’accès émis par OpenAI.
Utilisez le chemin du token monté, par exemple /var/run/secrets/tokens/token, comme source du token de sujet pour le fournisseur de fédération d’identités de charge de travail du SDK. Le SDK échange ce token EKS contre un jeton d’accès émis par OpenAI et utilise le token OpenAI pour authentifier les requêtes API.
Les exemples suivants initialisent un client OpenAI avec un fournisseur personnalisé de tokens de sujet. Ce fournisseur lit le token de compte de service projeté EKS à partir du chemin du fichier monté et l’utilise comme token de sujet pour la fédération d’identités de charge de travail.
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)