1# Configuring workload identity federation for Idira
2
3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.
4
5Use Idira Secure Workload Access (SWA) to call the OpenAI API without storing an OpenAI API key. SWA assigns your workload a SPIFFE ID and issues a JWT-SVID, a signed token that represents that identity. The OpenAI SDK exchanges the token for a short-lived OpenAI access token with the permissions of a mapped service account.
6
7This guide covers the OpenAI API setup. For Codex, use the token delivery steps here, then follow [Use workload identity with Codex](https://developers.openai.com/api/docs/guides/workload-identity-federation#use-workload-identity-with-codex).
8
9## Before you begin
10
11You need:
12
13- An SWA deployment with a trust domain and an attestation policy that assigns the expected SPIFFE ID to your workload.
14- Access to the SWA Agent's Workload API socket from the workload.
15- Permission to manage Workload Identity Providers, service account mappings, projects, and service accounts in your OpenAI organization.
16- An OpenAI SDK that supports workload identity credentials.
17
18Complete the SWA infrastructure setup using the prerequisites in [Idira's OpenAI integration guide](https://docs.cyberark.com/secrets-manager-saas/latest/en/content/conjurcloud/ccl-swa-openai.htm).
19
20## Get the issuer and signing keys
21
22Retrieve your trust domain's `jwt.discovery_endpoints.oidc_discovery_url` and `jwt.discovery_endpoints.jwks_uri`. Follow [Idira's OIDC configuration instructions](https://docs.cyberark.com/secrets-manager-saas/latest/en/content/conjurcloud/ccl-swa-oidc.htm) to find these values.
23
24The discovery document and JWKS must be reachable by OpenAI over HTTPS without credentials. The issuer is the HTTPS URL in the token's `iss` claim. The SPIFFE ID in `sub` identifies your workload.
25
26Choose a dedicated audience for OpenAI access, such as `https://api.openai.com/v1`. Use the same value when requesting the JWT-SVID and configuring the OpenAI provider. The audience identifies the token's intended recipient; it doesn't set the token exchange endpoint.
27
28SWA defaults to `RS512` signing with a 4096-bit RSA key. OpenAI's JWT verifier accepts this combination. For other SWA signing options, see [Idira's JWT requirements](https://docs.cyberark.com/secrets-manager-saas/latest/en/content/conjurcloud/ccl-swa-jwt.htm).
29
30## Deliver a fresh JWT-SVID to the workload
31
32Install [`spiffe-helper`](https://github.com/spiffe/spiffe-helper) in the workload environment to request and refresh a JWT-SVID from the SWA Agent. Save the following configuration as `helper.conf`. The helper writes `/var/run/spiffe/openai.jwt`, the path used by the shared SPIFFE SDK examples:
33
34```hcl
35agent_address = "/tmp/swa-agent/public/api.sock"
36cert_dir = "/var/run/spiffe"
37daemon_mode = true
38
39jwt_svids = [{
40 jwt_audience = "https://api.openai.com/v1"
41 jwt_svid_file_name = "openai.jwt"
42}]
43```
44
45In Kubernetes, run the helper as a sidecar in the workload's pod. Share a memory-backed `emptyDir` volume at `/var/run/spiffe`, and mount the SWA socket's parent directory, `/tmp/swa-agent/public`, into the helper. On VMs, use `/run/swa-agent/api.sock` for `agent_address` and keep the token directory on memory-backed storage. Follow Idira's deployment instructions for socket access and attestation.
46
47Create the token directory before starting the helper, and give the helper write access. Start the helper with this configuration in the sidecar or VM service:
48
49```bash
50spiffe-helper -config helper.conf
51```
52
53Restrict access to the token file and agent socket to the processes that need them. Keep the helper running so it replaces the token before it expires.
54
55If your language has a SPIFFE Workload API client, you can instead fetch a fresh JWT-SVID inside the subject token provider. See the direct Workload API option in [Idira's integration guide](https://docs.cyberark.com/secrets-manager-saas/latest/en/content/conjurcloud/ccl-swa-openai.htm).
56
57## Inspect the token
58
59From inside the workload, read the token into the local decoder's environment:
60
61```bash
62export TOKEN="$(cat /var/run/spiffe/openai.jwt)"
63```
64
65```javascript
66const parts = process.env.TOKEN?.split(".") ?? [];
67if (parts.length !== 3) {
68 throw new Error("Expected a compact JWT with three segments");
69}
70
71const decode = (segment) => {
72 if (!/^[A-Za-z0-9_-]+$/.test(segment) || segment.length % 4 === 1) {
73 throw new Error("JWT segment is not valid Base64URL");
74 }
75 const bytes = Buffer.from(segment, "base64url");
76 if (bytes.toString("base64url") !== segment) {
77 throw new Error("JWT segment is not valid Base64URL");
78 }
79 const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
80 const value = JSON.parse(decoded);
81 if (value === null || Array.isArray(value) || typeof value !== "object") {
82 throw new Error("JWT segment is not a JSON object");
83 }
84 return decoded;
85};
86
87console.log("Header:");
88console.log(decode(parts[0]));
89console.log("\nPayload:");
90console.log(decode(parts[1]));
91```
92
93```python
94import base64
95import json
96import os
97import re
98
99
100def reject_non_json_constant(value):
101 raise ValueError(f"JWT segment contains non-JSON constant: {value}")
102
103
104parts = os.environ.get("TOKEN", "").split(".")
105if len(parts) != 3:
106 raise ValueError("Expected a compact JWT with three segments")
107
108
109def decode(segment):
110 if re.fullmatch(r"[A-Za-z0-9_-]+", segment) is None or len(segment) % 4 == 1:
111 raise ValueError("JWT segment is not valid Base64URL")
112 padded_segment = segment + "=" * (-len(segment) % 4)
113 decoded = base64.b64decode(padded_segment, altchars=b"-_", validate=True)
114 if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != segment:
115 raise ValueError("JWT segment is not valid Base64URL")
116 decoded_text = decoded.decode("utf-8")
117 value = json.loads(decoded_text, parse_constant=reject_non_json_constant)
118 if not isinstance(value, dict):
119 raise ValueError("JWT segment is not a JSON object")
120 return decoded_text
121
122
123print("Header:")
124print(decode(parts[0]))
125print("\nPayload:")
126print(decode(parts[1]))
127```
128
129```go
130package main
131
132import (
133 "bytes"
134 "encoding/base64"
135 "encoding/json"
136 "fmt"
137 "os"
138 "strings"
139 "unicode/utf8"
140)
141
142func decodeSegment(segment string) (json.RawMessage, error) {
143 if !isBase64URLSegment(segment) {
144 return nil, fmt.Errorf("JWT segment is not valid Base64URL")
145 }
146 decoded, err := base64.RawURLEncoding.DecodeString(segment)
147 if err != nil {
148 return nil, err
149 }
150 if base64.RawURLEncoding.EncodeToString(decoded) != segment {
151 return nil, fmt.Errorf("JWT segment is not valid Base64URL")
152 }
153 if !utf8.Valid(decoded) {
154 return nil, fmt.Errorf("JWT segment is not valid UTF-8")
155 }
156
157 var value json.RawMessage
158 if err := json.Unmarshal(decoded, &value); err != nil {
159 return nil, err
160 }
161 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {
162 return nil, fmt.Errorf("JWT segment is not a JSON object")
163 }
164 return value, nil
165}
166
167func isBase64URLSegment(segment string) bool {
168 if segment == "" || len(segment)%4 == 1 {
169 return false
170 }
171 for _, character := range segment {
172 if !('A' <= character && character <= 'Z') &&
173 !('a' <= character && character <= 'z') &&
174 !('0' <= character && character <= '9') &&
175 character != '-' &&
176 character != '_' {
177 return false
178 }
179 }
180 return true
181}
182
183func printJSON(label string, value json.RawMessage) error {
184 formatted, err := json.MarshalIndent(value, "", " ")
185 if err != nil {
186 return err
187 }
188 fmt.Printf("%s:\n%s\n", label, formatted)
189 return nil
190}
191
192func main() {
193 parts := strings.Split(os.Getenv("TOKEN"), ".")
194 if len(parts) != 3 {
195 panic("Expected a compact JWT with three segments")
196 }
197
198 header, err := decodeSegment(parts[0])
199 if err != nil {
200 panic(err)
201 }
202 payload, err := decodeSegment(parts[1])
203 if err != nil {
204 panic(err)
205 }
206 if err := printJSON("Header", header); err != nil {
207 panic(err)
208 }
209 fmt.Println()
210 if err := printJSON("Payload", payload); err != nil {
211 panic(err)
212 }
213}
214```
215
216```java
217// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.
218import com.fasterxml.jackson.databind.DeserializationFeature;
219import com.fasterxml.jackson.databind.JsonNode;
220import com.fasterxml.jackson.databind.ObjectMapper;
221import java.io.IOException;
222import java.nio.ByteBuffer;
223import java.nio.charset.CharacterCodingException;
224import java.nio.charset.CodingErrorAction;
225import java.nio.charset.StandardCharsets;
226import java.util.Base64;
227
228public final class DecodeJwtExample {
229 private static final ObjectMapper JSON =
230 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
231
232 private DecodeJwtExample() {}
233
234 static String decodeUtf8(byte[] bytes) throws IOException {
235 try {
236 return StandardCharsets.UTF_8
237 .newDecoder()
238 .onMalformedInput(CodingErrorAction.REPORT)
239 .onUnmappableCharacter(CodingErrorAction.REPORT)
240 .decode(ByteBuffer.wrap(bytes))
241 .toString();
242 } catch (CharacterCodingException exception) {
243 throw new IOException("JWT segment is not valid UTF-8", exception);
244 }
245 }
246
247 static String decodeSegment(String segment) throws IOException {
248 if (!isBase64UrlSegment(segment)) {
249 throw new IllegalArgumentException("JWT segment is not valid Base64URL");
250 }
251 byte[] bytes = Base64.getUrlDecoder().decode(segment);
252 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {
253 throw new IllegalArgumentException("JWT segment is not valid Base64URL");
254 }
255 String decoded = decodeUtf8(bytes);
256 JsonNode value = JSON.readTree(decoded);
257 if (value == null || value.isMissingNode() || !value.isObject()) {
258 throw new IOException("JWT segment is not a JSON object");
259 }
260 return decoded;
261 }
262
263 static boolean isBase64UrlSegment(String segment) {
264 if (segment.isEmpty() || segment.length() % 4 == 1) {
265 return false;
266 }
267 return segment
268 .chars()
269 .allMatch(
270 character ->
271 character >= 'A' && character <= 'Z'
272 || character >= 'a' && character <= 'z'
273 || character >= '0' && character <= '9'
274 || character == '-'
275 || character == '_');
276 }
277
278 static String[] requireCompactJwt(String token) {
279 if (token == null) {
280 throw new IllegalArgumentException("Expected a compact JWT with three segments");
281 }
282 String[] parts = token.split("\\.", -1);
283 if (parts.length != 3) {
284 throw new IllegalArgumentException("Expected a compact JWT with three segments");
285 }
286 return parts;
287 }
288
289 public static void main(String[] args) throws IOException {
290 String[] parts = requireCompactJwt(System.getenv("TOKEN"));
291 System.out.println("Header:");
292 System.out.println(decodeSegment(parts[0]));
293 System.out.println("\nPayload:");
294 System.out.println(decodeSegment(parts[1]));
295 }
296}
297```
298
299```csharp
300using System.Text;
301using System.Text.Json;
302
303static string DecodeSegment(string segment)
304{
305 if (
306 segment.Length % 4 == 1 ||
307 segment.Any(
308 character =>
309 !(
310 character is >= 'A' and <= 'Z' ||
311 character is >= 'a' and <= 'z' ||
312 character is >= '0' and <= '9' ||
313 character is '-' or '_'
314 )
315 )
316 )
317 {
318 throw new FormatException("JWT segment is not valid Base64URL");
319 }
320
321 byte[] decoded = Convert.FromBase64String(
322 segment.Replace('-', '+').Replace('_', '/') +
323 new string('=', (4 - segment.Length % 4) % 4)
324 );
325 string canonicalSegment = Convert
326 .ToBase64String(decoded)
327 .TrimEnd('=')
328 .Replace('+', '-')
329 .Replace('/', '_');
330 if (canonicalSegment != segment)
331 {
332 throw new FormatException("JWT segment is not valid Base64URL");
333 }
334 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);
335 using JsonDocument document = JsonDocument.Parse(decodedJson);
336 if (document.RootElement.ValueKind is not JsonValueKind.Object)
337 {
338 throw new FormatException("JWT segment is not a JSON object");
339 }
340 return decodedJson;
341}
342
343string? token = Environment.GetEnvironmentVariable("TOKEN");
344if (token is null)
345{
346 throw new InvalidOperationException(
347 "Expected a compact JWT with three segments"
348 );
349}
350string[] parts = token.Split('.');
351if (parts.Length != 3)
352{
353 throw new InvalidOperationException(
354 "Expected a compact JWT with three segments"
355 );
356}
357
358Console.WriteLine("Header:");
359Console.WriteLine(DecodeSegment(parts[0]));
360Console.WriteLine("\nPayload:");
361Console.WriteLine(DecodeSegment(parts[1]));
362```
363
364```ruby
365require "base64"
366require "json"
367
368parts = ENV.fetch("TOKEN", "").split(".", -1)
369raise "Expected a compact JWT with three segments" unless parts.length == 3
370
371decode = lambda do |segment|
372 unless segment.match?(/\A[A-Za-z0-9_-]+\z/) && segment.length % 4 != 1
373 raise "JWT segment is not valid Base64URL"
374 end
375
376 padded = segment.ljust((segment.length + 3) & ~3, "=")
377 begin
378 decoded = Base64.urlsafe_decode64(padded)
379 rescue ArgumentError
380 raise "JWT segment is not valid Base64URL"
381 end
382 unless Base64.urlsafe_encode64(decoded, padding: false) == segment
383 raise "JWT segment is not valid Base64URL"
384 end
385
386 decoded.force_encoding(Encoding::UTF_8)
387 raise "JWT segment is not valid UTF-8" unless decoded.valid_encoding?
388
389 value = JSON.parse(decoded)
390 raise "JWT segment is not a JSON object" unless value.is_a?(Hash)
391
392 decoded
393end
394
395puts("Header:")
396puts(decode.call(parts[0]))
397puts("\nPayload:")
398puts(decode.call(parts[1]))
399```
400
401
402Check these fields before creating the OpenAI provider:
403
404| Field | Expected value |
405| --------------- | -------------------------------------------------------------------- |
406| Header `alg` | The configured signing algorithm, such as `RS512`. |
407| Header `kid` | The ID of a signing key in the trust domain's public JWKS. |
408| `iss` | The trust domain's HTTPS issuer URL. |
409| `sub` | The SPIFFE ID assigned to this workload. |
410| `aud` | Contains the audience you chose for OpenAI access. |
411| `iat` and `exp` | The token's issue and expiry times. Use a token that hasn't expired. |
412
413The decoder reads the claims without verifying the signature. Don't log the token or paste it into a web-based decoder. Unset `TOKEN` after inspection.
414
415## Configure OpenAI workload identity federation
416
417### Create the Workload Identity Provider
418
419Go to [Organization Settings > Security > Workload Identity Provider](https://platform.openai.com/settings/organization/security/workload-identity-provider) and create a provider with these settings:
420
421| Setting | Value |
422| ---------------------------------------- | -------------------------------------------------------------------------- |
423| Name | A unique name, such as `idira-prod`. |
424| Description | The application and environment that use this provider. |
425| OIDC Issuer URL | The JWT-SVID's `iss` value. |
426| Audience | The audience requested by the helper, such as `https://api.openai.com/v1`. |
427| Use uploaded JWKS for token verification | Keep this option off to use OIDC discovery. |
428| Attribute transformations | Leave empty when mapping directly from `sub`. |
429
430For OpenAI API token exchange, issuer comparison ignores trailing slashes. For
431 example, `https://issuer.example.com` and `https://issuer.example.com/` match.
432 All other characters must match, including the scheme and path. Audience
433 values must match exactly. Copy the issuer as issued to keep your
434 configuration consistent.
435
436If OpenAI can't reach the discovery document or JWKS, follow the [shared provider configuration guidance](https://developers.openai.com/api/docs/guides/workload-identity-federation#configure-an-oidc-workload-identity-provider) for uploaded public JWKS. Update uploaded keys when SWA rotates its signing keys.
437
438### Map the SPIFFE ID to a service account
439
4401. Create a service account mapping in the provider and give it a name, such as `idira-production-workload`.
4412. Set **Key** to `sub` and **Value** to the exact SPIFFE ID from the workload's token, such as `spiffe://prod.example.com/k8s-nodegroup/ns/my-namespace/sa/my-service-account`.
4423. Select the **Project** and **Service account** that the workload can use.
4434. Select the **Permissions** the workload needs, such as `api.model.request` for model requests. These permissions can narrow the service account's access; they cannot grant more access. Leaving permissions blank adds no WIF-specific restriction.
444
445Use a separate service account for each application and environment. Prefer an exact SPIFFE ID. Use a trailing wildcard only when every identity under that prefix should receive the same access. Avoid overlapping mappings. See [SPIFFE service account mappings](https://developers.openai.com/api/docs/guides/workload-identity-federation/spiffe#set-up-the-service-account-mapping) for details.
446
447## Use the token in code
448
449Set the IDs from your OpenAI provider and selected service account in the workload's environment:
450
451```bash
452export OPENAI_IDENTITY_PROVIDER_ID="<workload identity provider ID>"
453export OPENAI_SERVICE_ACCOUNT_ID="<service account ID>"
454```
455
456The following shared SPIFFE examples read `/var/run/spiffe/openai.jwt`. The subject token provider returns the current token whenever the SDK needs to exchange or refresh an OpenAI access token. SWA and the helper refresh the external JWT-SVID; the OpenAI SDK manages the OpenAI access token.
457
458Authenticate with an Idira-issued SPIFFE JWT-SVID
459
460```javascript
461import { readFile } from "node:fs/promises";
462import OpenAI from "openai";
463
464const tokenPath = "/var/run/spiffe/openai.jwt";
465const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;
466const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;
467
468if (!identityProviderId || !serviceAccountId) {
469 throw new Error(
470 "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"
471 );
472}
473
474function spiffeJwtSvidProvider(path) {
475 return {
476 tokenType: "jwt",
477 getToken: async () => {
478 const token = (await readFile(path, "utf8")).trim();
479 if (!token) {
480 throw new Error("The SPIFFE JWT-SVID file is empty.");
481 }
482 return token;
483 },
484 };
485}
486
487const client = new OpenAI({
488 workloadIdentity: {
489 identityProviderId,
490 serviceAccountId,
491 provider: spiffeJwtSvidProvider(tokenPath),
492 },
493});
494
495const response = await client.responses.create({
496 model: "gpt-5.6-terra",
497 input: "Say hello from SPIFFE workload identity federation.",
498});
499
500console.log(response.output_text);
501```
502
503```python
504import os
505from pathlib import Path
506
507from openai import OpenAI
508from openai.auth import SubjectTokenProvider
509
510TOKEN_PATH = "/var/run/spiffe/openai.jwt"
511
512
513def spiffe_jwt_svid_provider(token_path: str) -> SubjectTokenProvider:
514 def get_token() -> str:
515 token = Path(token_path).read_text().strip()
516 if not token:
517 raise RuntimeError("The SPIFFE JWT-SVID file is empty.")
518 return token
519
520 return {"token_type": "jwt", "get_token": get_token}
521
522
523client = OpenAI(
524 workload_identity={
525 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],
526 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],
527 "provider": spiffe_jwt_svid_provider(TOKEN_PATH),
528 },
529)
530
531response = client.responses.create(
532 model="gpt-5.6-terra",
533 input="Say hello from SPIFFE workload identity federation.",
534)
535
536print(response.output_text)
537```
538
539```go
540package main
541
542import (
543 "context"
544 "fmt"
545 "log"
546 "os"
547 "strings"
548
549 "github.com/openai/openai-go/v3"
550 "github.com/openai/openai-go/v3/auth"
551 "github.com/openai/openai-go/v3/option"
552 "github.com/openai/openai-go/v3/responses"
553)
554
555const tokenPath = "/var/run/spiffe/openai.jwt"
556
557type spiffeJWTSVIDProvider struct {
558 path string
559}
560
561func (p spiffeJWTSVIDProvider) TokenType() auth.SubjectTokenType {
562 return auth.SubjectTokenTypeJWT
563}
564
565func (p spiffeJWTSVIDProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
566 data, err := os.ReadFile(p.path)
567 if err != nil {
568 return "", &auth.SubjectTokenProviderError{
569 Provider: "spiffe",
570 Message: "failed to read SPIFFE JWT-SVID",
571 Cause: err,
572 }
573 }
574
575 token := strings.TrimSpace(string(data))
576 if token == "" {
577 return "", &auth.SubjectTokenProviderError{
578 Provider: "spiffe",
579 Message: "SPIFFE JWT-SVID file is empty",
580 }
581 }
582
583 return token, nil
584}
585
586func main() {
587 client := openai.NewClient(
588 option.WithWorkloadIdentity(auth.WorkloadIdentity{
589 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
590 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
591 Provider: spiffeJWTSVIDProvider{
592 path: tokenPath,
593 },
594 }),
595 )
596
597 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
598 Model: openai.ChatModelGPT4_1Mini,
599 Input: responses.ResponseNewParamsInputUnion{
600 OfString: openai.String("Say hello from SPIFFE workload identity federation."),
601 },
602 })
603 if err != nil {
604 log.Fatal(err)
605 }
606
607 fmt.Println(response.OutputText())
608}
609```
610
611```java
612import com.fasterxml.jackson.databind.json.JsonMapper;
613import com.openai.auth.SubjectTokenProvider;
614import com.openai.auth.SubjectTokenType;
615import com.openai.auth.WorkloadIdentity;
616import com.openai.client.OpenAIClient;
617import com.openai.client.okhttp.OpenAIOkHttpClient;
618import com.openai.core.http.HttpClient;
619import com.openai.errors.SubjectTokenProviderException;
620import com.openai.models.responses.ResponseCreateParams;
621import java.nio.file.Files;
622import java.nio.file.Path;
623import java.util.concurrent.CompletableFuture;
624
625public final class SpiffeWorkloadIdentityExample {
626 private static final String TOKEN_PATH = "/var/run/spiffe/openai.jwt";
627
628 private SpiffeWorkloadIdentityExample() {}
629
630 static final class SpiffeJwtSvidProvider implements SubjectTokenProvider {
631 private final Path tokenPath;
632
633 SpiffeJwtSvidProvider(String tokenPath) {
634 this.tokenPath = Path.of(tokenPath);
635 }
636
637 @Override
638 public SubjectTokenType tokenType() {
639 return SubjectTokenType.JWT;
640 }
641
642 @Override
643 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {
644 String token;
645 try {
646 token = Files.readString(tokenPath).trim();
647 } catch (Exception e) {
648 throw new SubjectTokenProviderException("spiffe", "failed to read SPIFFE JWT-SVID", e);
649 }
650
651 if (token.isEmpty()) {
652 throw new SubjectTokenProviderException("spiffe", "SPIFFE JWT-SVID file is empty", null);
653 }
654
655 return token;
656 }
657
658 @Override
659 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {
660 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));
661 }
662 }
663
664 public static void main(String[] args) {
665 WorkloadIdentity workloadIdentity =
666 WorkloadIdentity.builder()
667 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))
668 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))
669 .provider(new SpiffeJwtSvidProvider(TOKEN_PATH))
670 .build();
671
672 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();
673
674 ResponseCreateParams params =
675 ResponseCreateParams.builder()
676 .model("gpt-5.6-terra")
677 .input("Say hello from SPIFFE workload identity federation.")
678 .build();
679
680 client.responses().create(params).output().stream()
681 .flatMap(item -> item.message().stream())
682 .flatMap(message -> message.content().stream())
683 .flatMap(content -> content.outputText().stream())
684 .forEach(outputText -> System.out.println(outputText.text()));
685 }
686}
687```
688
689```ruby
690require "openai"
691
692TOKEN_PATH = "/var/run/spiffe/openai.jwt"
693
694class SpiffeJWTSVIDProvider
695 include OpenAI::Auth::SubjectTokenProvider
696
697 def initialize(token_path:)
698 @token_path = token_path
699 end
700
701 def token_type
702 OpenAI::Auth::TokenType::JWT
703 end
704
705 def get_token
706 token = File.read(@token_path).strip
707 if token.empty?
708 raise OpenAI::Errors::SubjectTokenProviderError.new(
709 message: "SPIFFE JWT-SVID file is empty",
710 provider: "spiffe"
711 )
712 end
713 token
714 rescue SystemCallError => e
715 raise OpenAI::Errors::SubjectTokenProviderError.new(
716 message: "Failed to read SPIFFE JWT-SVID: #{e.message}",
717 provider: "spiffe",
718 cause: e
719 )
720 end
721end
722
723provider = SpiffeJWTSVIDProvider.new(token_path: TOKEN_PATH)
724
725workload_identity = OpenAI::Auth::WorkloadIdentity.new(
726 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),
727 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),
728 provider: provider
729)
730
731client = OpenAI::Client.new(workload_identity: workload_identity)
732
733response = client.responses.create(
734 model: "gpt-5.6-terra",
735 input: "Say hello from SPIFFE workload identity federation."
736)
737
738puts(response.output_text)
739```
740
741
742## Verify and troubleshoot
743
744Run the example inside the attested workload with `OPENAI_API_KEY` unset. A successful request prints the model's reply. Confirm that requests continue to succeed after the helper replaces the JWT-SVID and the SDK refreshes its OpenAI access token.
745
746| Symptom | What to check |
747| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
748| The helper receives no identity | Check that the workload matches the SWA attestation policy and can access the agent socket. Run checks in the workload's context. |
749| The token file is missing or empty, or its token has expired | Check that the helper is running, the shared volume and file path match, and the workload can read the file. Keep host clocks synchronized. |
750| Token verification fails | Check `iss`, `aud`, `kid`, token expiry, and discovery/JWKS reachability. Apply the issuer comparison rule described earlier. |
751| No service account mapping matches | Check the provider ID, service account ID, enabled mapping, and exact `sub` value. |
752| More than one mapping matches | Narrow or disable overlapping mappings so exactly one enabled mapping matches. |
753| Exchange succeeds but the API rejects the request | Check the mapped service account's project, permissions, and applicable API access policies. |
754
755For SWA deployment diagnostics, see [Idira's integration troubleshooting](https://docs.cyberark.com/secrets-manager-saas/latest/en/content/conjurcloud/ccl-swa-openai.htm).