Skip to content

fix: clear WorkloadIdentityAuth refresh state after synchronous provider failures #852

Description

@abhinavkr26104

Description

WorkloadIdentityAuth.getTokenAsync() can permanently poison its token-refresh state when a custom SubjectTokenProvider.getTokenAsync() throws synchronously instead of returning an exceptionally completed future.

The method stores a new CompletableFuture in refreshInFlight before calling refreshTokenAsync(). If the provider throws synchronously, the exception escapes before finishRefresh() is registered, so refreshInFlight is never cleared or completed.

Reproduction

Implement a provider whose asynchronous method throws directly:

val provider = object : SubjectTokenProvider {
    override fun tokenType() = SubjectTokenType.JWT

    override fun getToken(httpClient: HttpClient, jsonMapper: JsonMapper): String =
        "subject-token"

    override fun getTokenAsync(
        httpClient: HttpClient,
        jsonMapper: JsonMapper,
    ): CompletableFuture<String> {
        throw IllegalStateException("provider failed")
    }
}

Use it in a WorkloadIdentityAuth and call:

assertThrows<IllegalStateException> { auth.getTokenAsync() }
val second = auth.getTokenAsync()
assertTimeoutPreemptively(Duration.ofSeconds(1)) { second.join() }

Code reference

  • openai-java-core/src/main/kotlin/com/openai/auth/WorkloadIdentityAuth.kt:134-152 stores refreshInFlight before starting a refresh.
  • WorkloadIdentityAuth.kt:157-174 calls performRefreshAndComplete() or refreshTokenAsync() without catching synchronous exceptions.
  • WorkloadIdentityAuth.kt:178-182 only clears the state from the asynchronous completion callback.
  • SubjectTokenProvider.kt:31-38 allows custom implementations of the async provider method.

Expected behavior

A synchronous provider exception should be converted into a failed future, and refreshInFlight should be cleared so later calls can retry or fail normally.

Actual behavior

The first call throws synchronously. Subsequent callers observe the abandoned refreshInFlight future and wait forever.

The same issue can occur on the background refresh path when refreshTokenAsync() throws before registering its completion callback.

Why it matters

A transient or poorly behaved custom credential provider can permanently hang all future asynchronous authentication attempts in a long-lived client. This is especially problematic because the failure is not limited to the request that encountered the provider exception.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions