[Identity] Stop reporting interruptions and JVM shutdown as authentication errors in getTokenSync by prasanna164-code · Pull Request #50475 · Azure/azure-sdk-for-java · GitHub
Skip to content

[Identity] Stop reporting interruptions and JVM shutdown as authentication errors in getTokenSync - #50475

Merged
Alan Zimmer (alzimmermsft) merged 1 commit into
Azure:mainfrom
prasanna164-code:identity-interrupt-logging
Sep 16, 2026
Merged

Alan Zimmer (alzimmermsft) merged 1 commit into
Azure:mainfrom
prasanna164-code:identity-interrupt-logging

Conversation

@prasanna164-code

@prasanna164-code prasanna164-code commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #49239

getTokenSync on every credential waits for the MSAL token request with CompletableFuture.get() on the caller's thread. When that thread is interrupted while waiting, which is exactly what BoundedElasticScheduler.Worker.dispose() does when an upstream Reactor subscription is cancelled (pod scale-down, request timeouts, client disconnects), get() throws InterruptedException. Today IdentitySyncClient wraps it in a RuntimeException, logs it as an error, and the credential logs it as an error a second time:

Azure Identity => ERROR in getToken() call for scopes [https://graph.microsoft.com/.default]: java.lang.InterruptedException

An interrupt is a cooperative cancellation signal from the caller, not an authentication failure. Reporting it at error level floods dashboards during normal shutdown, and nothing downstream can suppress it because it is logged inside the SDK before the caller sees the exception. Two further problems sat on the same path:

  • Future.get() clears the thread's interrupt status when it throws, and the SDK never restored it, so the cancellation was invisible to the rest of the caller's code.
  • The cache-lookup paths called e.getMessage().contains("Token not found in the cache") on the caught exception. InterruptedException.getMessage() is null, so an interrupt there raised a NullPointerException, which the credentials' silent cache catch then swallowed.

Change

  • IdentitySyncClient now waits for every MSAL future through one helper, awaitTokenResult. On InterruptedException it restores the interrupt status, logs at verbose level, and rethrows a RuntimeException whose cause is the InterruptedException. The flows that previously wrapped any exception as ClientAuthenticationException (username/password, device code, interactive browser, on-behalf-of) or CredentialUnavailableException (workload identity) rethrow an interruption unchanged instead, so a cancellation is never reported as "authentication failed" or "credential unavailable". The broker silent-acquisition path no longer swallows an interruption before falling through to the interactive dialog.

  • IdentityUtil.isInterruption(Throwable) walks the cause chain for an InterruptedException. The chain is followed to its end, as RetryPolicy does when it inspects a chain for retriable causes; two cursors advancing at different speeds make a cyclic chain (which Throwable.initCause permits) terminate instead of spinning.

  • LoggingUtil.logTokenError, shared by all 16 credentials' getTokenSync, logs an interruption at verbose level with a distinct message instead of the configured error level. Genuine failures are logged exactly as before.

  • The cache-miss checks are null-safe.

  • The credentials no longer treat a shutdown signal raised during their token-cache lookup as a cache miss. Their getTokenSync methods wrap that lookup in a catch (Exception) { } so that a miss falls through to a full acquisition; a cancellation was swallowed there too. That made the cancelled request run a second acquisition, and for InteractiveBrowserCredential and DeviceCodeCredential it could prompt the user for a request they had already cancelled, or report AuthenticationRequiredException at error level. Ten such sites across eight credentials now call a shared IdentityUtil.rethrowIfShutdownSignal(Exception).

  • ClientAssertionCredential and AzurePipelinesCredential additionally wrapped every failure with LOGGER.logExceptionAsError(new RuntimeException(e)), which logs at error level regardless of what LoggingUtil.logTokenError decided. They now rethrow a shutdown signal before that wrap, so the guarantee holds for them as well.

  • ChainedTokenCredential.getTokenSync (and therefore DefaultAzureCredential) surfaces an interruption unchanged instead of wrapping it as "<credential> authentication failed", and does not go on to try the remaining credentials: they would be interrupted too. Credentials that use azure-core's default getTokenSync (which blocks on the reactive getToken, for example ManagedIdentityCredential) already behave this way, because Reactor's block() restores the interrupt status and throws a RuntimeException caused by the InterruptedException; this change makes the MSAL-backed synchronous credentials consistent with them.

  • A token request that fails because MSAL's own worker was interrupted (an ExecutionException caused by InterruptedException, for example during executor shutdown) is surfaced the same way, without touching the calling thread's interrupt status.

  • The second failure mode from the issue is covered by the same rule: a token request that runs into the JVM shutting down surfaces as IllegalStateException("Shutdown in progress") (the JVM's message when azure-core's shared executor tries to register its shutdown hook after shutdown has begun), either as the MSAL future's failure or thrown synchronously by acquireToken. IdentityUtil.isShutdownSignal recognises both signals; the log-level decision, the sync client and the chain use it, so the async doOnError path is covered as well since it logs through the same LoggingUtil.logTokenError. Only a real interruption of the calling thread restores the interrupt status.

The root cause of that second mode, azure-core's ImplUtils.addShutdownHookSafely not tolerating Runtime.addShutdownHook failing during shutdown and SharedExecutorService.ensureNotShutdown recreating the executor at that point, lives in azure-core and is worth a separate change there; this PR makes identity stop reporting the symptom as an authentication error, as the issue asks.

No public API changes. The exception a caller receives on interruption is still a RuntimeException, now with a clear message and the interrupt status set, which is the convention azure-core follows elsewhere (Thread.currentThread().interrupt() before rethrowing).

Testing

  • LoggingUtilTest: the level decision is a function, LoggingUtil.tokenErrorLogLevel, so the tests assert it directly: an authentication failure keeps the configured error level, an interruption resolves to verbose, an interruption nested inside a ClientAuthenticationException (as ChainedTokenCredential produces) is detected, IllegalStateException("Shutdown in progress") resolves to verbose while any other IllegalStateException stays at error, a 64-deep chain is followed to its end, and a cyclic chain terminates (both under a timeout).
  • IdentitySyncClientInterruptionTests (new): builds a real IdentitySyncClient with a mocked MSAL application whose futures never complete, runs the acquisition on the test thread and interrupts it from a helper thread, for the client-credential, cache-lookup, on-behalf-of and workload-identity flows. Asserts the call returns promptly, the exception is caused by the InterruptedException, it is not a ClientAuthenticationException, CredentialUnavailableException or NullPointerException, and the interrupt status is restored. A future failed by MSAL's own worker being interrupted, or by the JVM shutting down, is surfaced as a cancellation without setting the caller's interrupt status; any other failure of the future is still an authentication error. A completed future is unaffected.
  • ChainedTokenCredentialTest: an interrupted credential's exception is surfaced unchanged by the chain and the next credential is not tried.
  • CredentialShutdownSignalTests (new): drives the behaviour through the public getTokenSync surface. A cancellation raised by the cache lookup is surfaced unchanged and no full acquisition follows; an ordinary cache failure still falls through and returns a token; ClientAssertionCredential propagates the cancellation instead of re-wrapping it; and InteractiveBrowserCredential does not reach browser interaction after a cancelled cache lookup. Against the previous commit three of these four fail.
  • The reporter's reproducer (ClientSecretCredential against a slow local authority with the calling thread interrupted every 20 ms) run locally against this branch produces no error-level identity log, only the verbose interruption line, and leaves the interrupt status set.

With the behaviour neutralised (old code paths restored), the new tests fail, including the reporter's reproducer. Existing azure-identity unit tests pass; Checkstyle and SpotBugs are clean.

All SDK Contribution checklist:

  • The pull request does not introduce [breaking changes]
  • CHANGELOG is updated for new features, bug fixes or other significant changes.
  • I have read the contribution guidelines.

General Guidelines and Best Practices

  • Title of the pull request is clear and informative.
  • There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, see this page.

Testing Guidelines

  • Pull request includes test coverage for the included changes.

Copilot AI lite review requested due to automatic review settings September 15, 2026 22:48
@github-actions github-actions Bot added Azure.Identity Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization. labels Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
31 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI 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.

🟡 Changes recommended

Cache interruptions can still be swallowed, and two credential paths can still log interruptions at error level.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR updates Azure Identity synchronous token acquisition to handle interruptions and JVM shutdown as cancellation signals instead of authentication failures.

Changes:

  • Preserves interrupt status and adjusts cancellation logging.
  • Propagates cancellation through credential chains.
  • Adds regression tests and changelog documentation.
File summaries
File Summary
sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/util/LoggingUtilTest.java Tests cancellation detection and log levels.
sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentitySyncClientInterruptionTests.java Tests interrupted and shutdown token requests.
sdk/identity/azure-identity/src/test/java/com/azure/identity/ChainedTokenCredentialTest.java Tests cancellation propagation through credential chains.
sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/util/LoggingUtil.java Adjusts token error logging for cancellation signals.
sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/util/IdentityUtil.java Detects interruption and shutdown signals.
sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/IdentitySyncClient.java Handles interrupted and shutdown MSAL requests.
sdk/identity/azure-identity/src/main/java/com/azure/identity/ChainedTokenCredential.java Propagates cancellation without trying remaining credentials.
sdk/identity/azure-identity/CHANGELOG.md Documents the behavior change.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Copilot AI 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.

🔵 Needs a closer look

Fix the workload-identity test setup and make interruption logging null-safe.

Review details

Suppressed comments (2)

sdk/identity/azure-identity/src/main/java/com/azure/identity/implementation/util/LoggingUtil.java:45

  • For a direct InterruptedException (for example, an async MSAL future failure), getMessage() is typically null, so this new verbose message is emitted as interrupted: null. Keep the shutdown/cancellation message useful by applying the same null-safe handling as the existing error path.
                    error.getMessage()),

sdk/identity/azure-identity/src/test/java/com/azure/identity/implementation/IdentitySyncClientInterruptionTests.java:185

  • The new workload-identity test uses the same mocked ConfidentialClientApplication.Builder, but IdentityClientBase.getWorkloadIdentityConfidentialClient() assigns the result of appTokenProvider(...) before build(). That fluent method is not stubbed here, so Mockito returns null and the subsequent builder call throws a NullPointerException; workloadIdentitySurfacesInterruptionAsCancellation never reaches the interruption path. Stub this method to return the builder.
                    when(builder.logPii(anyBoolean())).thenReturn(builder);
  • Files reviewed: 17/17 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…rrors

getTokenSync waits for the MSAL token request with Future.get() on the
caller's thread. When that thread is interrupted while waiting, which is
what a Reactor scheduler does when it disposes a worker on cancellation,
the InterruptedException was wrapped, logged as an error by the sync
client and logged as an error again by the credential, the thread's
interrupt status was lost, and on the cache-lookup paths the null message
of the InterruptedException caused a NullPointerException. A token request
that ran into the JVM shutting down (IllegalStateException "Shutdown in
progress" from the shared executor) was logged as an error the same way.

Wait for every MSAL future through one helper that restores the interrupt
status, logs at verbose level and rethrows the interruption as a
RuntimeException caused by the InterruptedException. Treat the JVM
shutdown signal the same way, without touching the interrupt status.
LoggingUtil.logTokenError, shared by every credential on both the sync and
async paths, logs shutdown signals at verbose level instead of the
configured error level, and ChainedTokenCredential surfaces them unchanged
instead of reporting the credential as failed.

The credentials also no longer treat a shutdown signal raised during their
token-cache lookup as a cache miss. That catch-all let a cancelled request
fall through to a full acquisition and, for the interactive credentials,
prompt the user for a request they had already cancelled. ClientAssertion
and AzurePipelines additionally re-wrapped every failure through
logExceptionAsError, which logged a cancellation at error level regardless.
Both now go through IdentityUtil.rethrowIfShutdownSignal. Cache-miss checks
are null-safe.

Fixes Azure#49239
@prasanna164-code
prasanna164-code force-pushed the identity-interrupt-logging branch from 178e259 to a9d1dcd Compare September 16, 2026 15:07
@alzimmermsft
Alan Zimmer (alzimmermsft) merged commit 081db35 into Azure:main Sep 16, 2026
26 checks passed
browndav-msft pushed a commit to browndav-msft/azure-sdk-for-java that referenced this pull request Oct 14, 2026
…rrors (Azure#50475)

getTokenSync waits for the MSAL token request with Future.get() on the
caller's thread. When that thread is interrupted while waiting, which is
what a Reactor scheduler does when it disposes a worker on cancellation,
the InterruptedException was wrapped, logged as an error by the sync
client and logged as an error again by the credential, the thread's
interrupt status was lost, and on the cache-lookup paths the null message
of the InterruptedException caused a NullPointerException. A token request
that ran into the JVM shutting down (IllegalStateException "Shutdown in
progress" from the shared executor) was logged as an error the same way.

Wait for every MSAL future through one helper that restores the interrupt
status, logs at verbose level and rethrows the interruption as a
RuntimeException caused by the InterruptedException. Treat the JVM
shutdown signal the same way, without touching the interrupt status.
LoggingUtil.logTokenError, shared by every credential on both the sync and
async paths, logs shutdown signals at verbose level instead of the
configured error level, and ChainedTokenCredential surfaces them unchanged
instead of reporting the credential as failed.

The credentials also no longer treat a shutdown signal raised during their
token-cache lookup as a cache miss. That catch-all let a cancelled request
fall through to a full acquisition and, for the interactive credentials,
prompt the user for a request they had already cancelled. ClientAssertion
and AzurePipelines additionally re-wrapped every failure through
logExceptionAsError, which logged a cancellation at error level regardless.
Both now go through IdentityUtil.rethrowIfShutdownSignal. Cache-miss checks
are null-safe.

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

Labels

Azure.Identity Community Contribution Community members are working on the issue customer-reported Issues that are reported by GitHub users external to the Azure organization.

Projects

Development

Successfully merging this pull request may close these issues.

[BUG] [azure-identity] ClientSecretCredential.getTokenSync logs InterruptedException at ERROR — surfaces reactive cancellation as log spam

3 participants