Skip to content

feat(auth): add X.509 workload identity federation - #864

Open
jbeckwith-oai wants to merge 2 commits into
mainfrom
codex/x509-wif-java-sdk
Open

feat(auth): add X.509 workload identity federation#864
jbeckwith-oai wants to merge 2 commits into
mainfrom
codex/x509-wif-java-sdk

Conversation

@jbeckwith-oai

Copy link
Copy Markdown
Contributor

What and why

Implement Phase 1 (HTTP only) of X.509 Workload Identity Federation in OpenAI SDKs — Design Proposal. Applications that already own a mutually authenticated HTTP transport can exchange their X.509 workload identity for short-lived OpenAI access tokens without configuring a subject-token provider or exposing certificate material through SDK options.

Public API and Java idioms

WorkloadIdentity identity = WorkloadIdentity.x509Builder()
    .identityProviderId("idp_...")
    .serviceAccountId("svc_acct_...")
    .refreshBuffer(Duration.ofMinutes(20))
    .build();

OpenAIClient client = OpenAIOkHttpClient.builder()
    .sslSocketFactory(applicationSslContext.getSocketFactory())
    .trustManager(defaultServerTrustManager)
    .workloadIdentity(identity)
    .build();
  • Adds the additive @JvmStatic WorkloadIdentity.x509Builder() fluent builder; the existing builder() binary signature and JWT/ID-token builder remain unchanged.
  • Stores an immutable credential-source discriminator. Only subject-token identities require a provider; the existing public provider accessor remains binary compatible and reports a clear error when called for an X.509 identity.
  • Uses idiomatic Java Duration, existing SDK builders, existing HttpClient, canonical retry/backoff machinery, ReentrantLock, and Java 8-compatible CompletableFuture rather than introducing another transport or concurrency framework.
  • Defaults only X.509 clients without an explicit base URL to https://mtls.api.openai.com/v1; preserves explicit URLs, withOptions() semantics, synchronous/asynchronous parity, Spring wiring, and Kotlin consumers.
  • Adds an executable Java 8-compatible OPENAI_AUTH_MODE=api_key|x509 rollout example and README guidance.

Security, transport, and certificate ownership

  • The exact exchange is POST https://mtls.auth.openai.com/oauth/token; its JSON structurally omits subject_token, and there is no custom public exchange URL.
  • Both the exchange and API calls reuse the effective caller-configured mTLS HttpClient; JSSE/the native transport retain sole ownership of certificates, private keys, passphrases, trust managers, proxies, HSMs, pools, executors, and certificate rotation.
  • X.509 exchange and API requests disable redirects; bearer and cookie headers are redacted, token-bearing debug representations do not expose secrets, and OAuth error descriptions are not leaked.
  • JSSE client key managers stay separate from default server trust; hostname verification is never disabled.
  • Exchanges are lazy, require positive expires_in, cache against monotonic time, clamp refresh buffers to half the token TTL, deduplicate sync/async refreshes, protect shared refreshes from waiter cancellation, and honor bounded retry/backoff including Retry-After.
  • A rejected X.509 bearer token is invalidated and retried at most once, only when the original request body is repeatable; non-repeatable bodies are never replayed.

Validation matrix

  • env JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./scripts/format
  • env JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./scripts/lint
  • env JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./scripts/build — all modules/packages, build-logic tests, Jackson compatibility, generated test classes, and support-policy checks.
  • env JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./scripts/test12,074 tests, 0 failures, 0 errors across the complete generated-mock-backed repository suite, including ProGuard/R8 and existing API-key/JWT/ID-token regressions.
  • ./scripts/detect-breaking-changes fcdcc7ee0414bbd92284a0839c0b7a6997cf8415 — baseline and proposed public API compatibility.
  • :openai-java-runtime-compatibility:runRuntimeCompatibility for every declared runtime: Java 8, 11, 17, 21, 25, and 26; exercises all four published artifacts: core, OkHttp, aggregate SDK, and Bedrock.
  • Compiled the new example directly with a native Java 8 javac and confirmed major bytecode version 52.
  • Focused tests cover exact endpoint/method/body, absence of subject_token, redirect refusal, full client certificate chain on both network legs, trust/transport ownership, bounded transient retry and Retry-After, sync/async single flight, mixed sync/async concurrency, cancellation-safe waiters, monotonic expiry/refresh clamp, proactive refresh, stale-token invalidation, repeatable/non-repeatable request-body replay, one-retry limits, secret redaction, withOptions() rebinding, and existing API-key plus JWT/ID-token behavior.

Compatibility and scope

API-key authentication and existing JWT/ID-token workload federation retain their existing endpoints, token payloads, defaults, and retry conventions. HttpRequest.followRedirects() is additive and defaults to existing behavior. All supported Java runtimes, Java/Kotlin builder compatibility, caller-owned transports, and conventional Spring configuration remain supported.

Realtime/WebSockets are explicitly excluded and deferred to Phase 2.

@jbeckwith-oai
jbeckwith-oai requested a review from a team as a code owner August 15, 2026 08:40
@openai-sdks

openai-sdks Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

OkTest Summary

237/237 SDK tests passed in 16.786s for Java SDK PR #864.

Test results — 42 files
Test Result Time
tests/chat-completions-complex-body.test.ts ✅ Passed 322ms
tests/chat-completions-create.test.ts ✅ Passed 738ms
tests/chat-completions-stream.test.ts ✅ Passed 349ms
tests/files-content-binary.test.ts ✅ Passed 275ms
tests/files-create-multipart.test.ts ✅ Passed 255ms
tests/files-list-pagination.test.ts ✅ Passed 255ms
tests/initialize-config.test.ts ✅ Passed 180ms
tests/instance-isolation.test.ts ✅ Passed 268ms
tests/models-list.test.ts ✅ Passed 179ms
tests/responses-background-lifecycle.test.ts ✅ Passed 376ms
tests/responses-body-method-errors.test.ts ✅ Passed 643ms
tests/responses-cancel-timeout.test.ts ✅ Passed 292ms
tests/responses-cancel.test.ts ✅ Passed 387ms
tests/responses-compact-retries.test.ts ✅ Passed 415ms
tests/responses-compact.test.ts ✅ Passed 366ms
tests/responses-create-advanced-stream.test.ts ✅ Passed 659ms
tests/responses-create-advanced.test.ts ✅ Passed 1.306s
tests/responses-create-disconnect.test.ts ✅ Passed 1.2s
tests/responses-create-errors.test.ts ✅ Passed 370ms
tests/responses-create-malformed-api-responses.test.ts ✅ Passed 283ms
tests/responses-create-retries.test.ts ✅ Passed 693ms
tests/responses-create-stream-failures.test.ts ✅ Passed 238ms
tests/responses-create-stream-timeout.test.ts ✅ Passed 280ms
tests/responses-create-stream-wire.test.ts ✅ Passed 6.242s
tests/responses-create-stream.test.ts ✅ Passed 162ms
tests/responses-create-terminal-states.test.ts ✅ Passed 382ms
tests/responses-create-timeout.test.ts ✅ Passed 315ms
tests/responses-create.test.ts ✅ Passed 239ms
tests/responses-delete.test.ts ✅ Passed 319ms
tests/responses-input-items-errors.test.ts ✅ Passed 282ms
tests/responses-input-items-list.test.ts ✅ Passed 300ms
tests/responses-input-items-options.test.ts ✅ Passed 612ms
tests/responses-input-tokens-count-timeout.test.ts ✅ Passed 246ms
tests/responses-input-tokens-count.test.ts ✅ Passed 263ms
tests/responses-malformed-inputs.test.ts ✅ Passed 5.055s
tests/responses-not-found-errors.test.ts ✅ Passed 435ms
tests/responses-parse.test.ts ✅ Passed 652ms
tests/responses-retrieve-retries.test.ts ✅ Passed 303ms
tests/responses-retrieve.test.ts ✅ Passed 454ms
tests/responses-stored-method-errors.test.ts ✅ Passed 1.071s
tests/retry-behavior.test.ts ✅ Passed 3.581s
tests/sdk-error-shape.test.ts ✅ Passed 422ms

View OkTest run #31907229592

SDK merge (e79270b9484f) · head (1bbbaf763601) · base (fcdcc7ee0414) · OkTest (7e7850017529)

@HAYDEN-OAI HAYDEN-OAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues need attention before this is safe to ship: X.509 clients currently accept plaintext API endpoints, exposing transferable service-account bearer tokens, and the 401 refresh path resets the configured transient retry budget. Details and concrete reproductions are inline.

Comment thread openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt Outdated

@HAYDEN-OAI HAYDEN-OAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous HTTPS enforcement and shared retry-budget findings are fixed. One separate backward-compatibility regression remains in legacy workload-identity refresh timing; details and a concrete timing example are inline.

val newExpiry = Instant.now().plusSeconds(expiresIn.toLong())
val expiresAfterNanos = TimeUnit.SECONDS.toNanos(expiresIn.toLong())
val configuredBufferNanos = max(0L, config.refreshBuffer.toNanos())
val effectiveBufferNanos = min(configuredBufferNanos, expiresAfterNanos / 2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve configured refresh timing for existing JWT/ID-token identities

This half-lifetime clamp runs for every workload identity, although it is documented only for the new X.509 builder. Existing WorkloadIdentity.builder().refreshBufferSeconds(...) clients previously refreshed at exactly expiry - refreshBufferSeconds; for a one-hour JWT/ID-token lease with refreshBufferSeconds(3000), refresh used to begin after 600 seconds but now silently begins after 1800 seconds. That 20-minute delay breaks the existing public configuration contract and can leave long-running requests or downstream token consumers without their deliberately configured expiration margin. Apply the half-TTL clamp only when isX509, preserving the configured refresh threshold for legacy subject-token identities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants