From 694037f6e13c75af0dc2612d5e1da2571126f6fc Mon Sep 17 00:00:00 2001 From: fadidurah Date: Wed, 13 May 2026 18:45:40 -0400 Subject: [PATCH 01/14] Wire ClientDataInfo through AcquireTokenResult and BaseException Propagate ClientDataInfo (parsed from the x-ms-clientdata token response header and the clientdata authorize redirect query parameter) through AcquireTokenResult on success paths and through BaseException on failure paths. Includes broker IPC serialization via BrokerResult so the payload reaches the MSAL caller process for both success and error responses. Adds a raw field to ClientDataInfo preserving the original pipe-delimited string for partner teams that want the unparsed payload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- changelog.txt | 1 + .../common/internal/broker/BrokerResult.java | 20 +++++ .../controllers/LocalMSALController.java | 28 +++++++ .../result/MsalBrokerResultAdapter.java | 61 +++++++++++++-- .../request/MsalBrokerResultAdapterTests.kt | 78 +++++++++++++++++++ .../java/controllers/BaseController.java | 10 +++ .../java/controllers/ExceptionAdapter.java | 12 ++- .../common/java/exception/BaseException.java | 18 +++++ ...tractMicrosoftStsTokenResponseHandler.java | 1 + .../MicrosoftStsAuthorizationResult.java | 24 ++++++ ...icrosoftStsAuthorizationResultFactory.java | 5 +- .../java/providers/oauth2/TokenResult.java | 24 ++++++ .../java/result/AcquireTokenResult.java | 24 ++++++ .../result/LocalAuthenticationResult.java | 22 ++++++ .../common/java/telemetry/ClientDataInfo.java | 14 +++- .../controllers/ExceptionAdapterTests.java | 39 ++++++++++ .../java/telemetry/ClientDataInfoTest.java | 34 ++++---- 17 files changed, 390 insertions(+), 25 deletions(-) diff --git a/changelog.txt b/changelog.txt index ddfbd02e14..a4d132651e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) - [PATCH] Extend filter-then-clone optimization to load() and getIdTokensForAccountRecord() in MsalOAuth2TokenCache: when ENABLE_FILTER_THEN_CLONE_IN_MEMORY_CACHE flight is enabled, skip clone-all preload and call direct flight-gated overloads that clone only matching credentials; add new getCredentialsFilteredBy overload with kid support (#3100) - [PATCH] Move Multiple Listening apps check to the authorization layer (#3070) - [PATCH] Edge TB: Fix lookup mode (#3108) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java index 4c0cc5f3c2..7784c08aee 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/broker/BrokerResult.java @@ -97,6 +97,7 @@ private static class SerializedNames { static final String SPE_RING = "spe_ring"; static final String CLI_TELEM_ERRORCODE = "cli_telem_error_code"; static final String CLI_TELEM_SUB_ERROR_CODE = "cli_telem_suberror_code"; + static final String CLIENT_DATA_INFO = "client_data_info"; } private static final long serialVersionUID = 8606631820514878489L; @@ -236,6 +237,13 @@ private static class SerializedNames { @SerializedName(SerializedNames.REFRESH_TOKEN_AGE) private String mRefreshTokenAge; + /** + * Server client data info from x-ms-clientdata response header (pipe-delimited format). + */ + @Nullable + @SerializedName(SerializedNames.CLIENT_DATA_INFO) + private String mClientDataInfoRaw; + /** * Boolean to indicate if the request succeeded without exceptions. */ @@ -347,6 +355,7 @@ private BrokerResult(@NonNull final Builder builder) { mCachedAt = builder.mCachedAt; mSpeRing = builder.mSpeRing; mRefreshTokenAge = builder.mRefreshTokenAge; + mClientDataInfoRaw = builder.mClientDataInfoRaw; mSuccess = builder.mSuccess; mTenantProfileData = builder.mTenantProfileData; mServicedFromCache = builder.mServicedFromCache; @@ -426,6 +435,11 @@ public String getSpeRing() { return mSpeRing; } + @Nullable + public String getClientDataInfoRaw() { + return mClientDataInfoRaw; + } + public long getCachedAt() { return mCachedAt; } @@ -518,6 +532,7 @@ public static class Builder { private long mCachedAt; private String mSpeRing; private String mRefreshTokenAge; + private String mClientDataInfoRaw; private boolean mSuccess; private String mNegotiatedBrokerProtocolVersion; private List mTenantProfileData; @@ -631,6 +646,11 @@ public Builder refreshTokenAge(final String refreshTokenAge) { return this; } + public Builder clientDataInfoRaw(@Nullable final String clientDataInfoRaw) { + this.mClientDataInfoRaw = clientDataInfoRaw; + return this; + } + public Builder success(boolean success) { this.mSuccess = success; return this; diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java index 1bff3a7f15..f50533c251 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java @@ -62,6 +62,7 @@ import com.microsoft.identity.common.java.providers.RawAuthorizationResult; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationRequest; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResponse; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsTokenRequest; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationRequest; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; @@ -168,6 +169,13 @@ public AcquireTokenResult acquireToken( ); acquireTokenResult.setAuthorizationResult(result); + // Wire ClientDataInfo from the authorization result (authorize endpoint). + if (result instanceof MicrosoftStsAuthorizationResult) { + acquireTokenResult.setClientDataInfo( + ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() + ); + } + ResultUtil.logResult(TAG, result); if (result.getAuthorizationStatus().equals(AuthorizationStatus.SUCCESS)) { @@ -181,6 +189,11 @@ public AcquireTokenResult acquireToken( acquireTokenResult.setTokenResult(tokenResult); + // Prefer ClientDataInfo from the token endpoint (later, more authoritative call). + if (tokenResult != null && tokenResult.getClientDataInfo() != null) { + acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } + if (tokenResult != null && tokenResult.getSuccess()) { //4) Save tokens in token cache final List records = saveTokens( @@ -205,6 +218,11 @@ public AcquireTokenResult acquireToken( false ) ); + + // Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation + final LocalAuthenticationResult localResult = + (LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult(); + localResult.setClientDataInfo(acquireTokenResult.getClientDataInfo()); } } @@ -742,6 +760,9 @@ public AcquireTokenResult acquireDeviceCodeFlowToken( // Assign token result acquireTokenResult.setTokenResult(tokenResult); + if (tokenResult != null) { + acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } // If the token is valid, save it into token cache final List records = saveTokens( @@ -764,6 +785,13 @@ public AcquireTokenResult acquireDeviceCodeFlowToken( false ) ); + + // Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation + if (tokenResult != null) { + final LocalAuthenticationResult localResult = + (LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult(); + localResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } } catch (Exception error) { Telemetry.emit( new ApiEndEvent() diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java index d9b31430c9..8faea6d324 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java @@ -98,6 +98,7 @@ import com.microsoft.identity.common.java.result.GenerateShrResult; import com.microsoft.identity.common.java.result.ILocalAuthenticationResult; import com.microsoft.identity.common.java.result.LocalAuthenticationResult; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import com.microsoft.identity.common.java.ui.PreferredAuthMethod; import com.microsoft.identity.common.java.util.BrokerProtocolVersionUtil; import com.microsoft.identity.common.java.util.HeaderSerializationUtil; @@ -284,6 +285,17 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth .success(true) .servicedFromCache(authenticationResult.isServicedFromCache()); + // Serialize ClientDataInfo as raw pipe-delimited string for IPC transfer. + // The raw field is populated by ClientDataInfo.fromPipeDelimited(), the only + // path that populates the parsed fields, so it is safe to ship as-is. + if (authenticationResult instanceof LocalAuthenticationResult) { + final ClientDataInfo clientDataInfo = + ((LocalAuthenticationResult) authenticationResult).getClientDataInfo(); + if (clientDataInfo != null) { + brokerResultBuilder.clientDataInfoRaw(clientDataInfo.getRaw()); + } + } + if (shouldRemoveRefreshTokenFromResult(authenticationResult, negotiatedBrokerProtocolVersion)){ brokerResultBuilder.tenantProfileRecords( removeRefreshTokenFromCacheRecords( @@ -411,6 +423,12 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception, .speRing(exception.getSpeRing()) .refreshTokenAge(exception.getRefreshTokenAge()); + // Serialize ClientDataInfo (server telemetry from x-ms-clientdata) so it + // survives the broker IPC boundary on error paths. + if (exception.getClientDataInfo() != null) { + builder.clientDataInfoRaw(exception.getClientDataInfo().getRaw()); + } + if (exception instanceof ServiceException) { final ServiceException serviceException = (ServiceException) exception; builder.subErrorCode(serviceException.getSubErrorCode()) @@ -476,12 +494,21 @@ public ILocalAuthenticationResult authenticationResultFromBundle(@NonNull final throw new ClientException(INVALID_BROKER_BUNDLE, "getTenantProfileData is null."); } - return new LocalAuthenticationResult( + final LocalAuthenticationResult localAuthResult = new LocalAuthenticationResult( tenantProfileCacheRecords.get(0), tenantProfileCacheRecords, SdkType.MSAL, brokerResult.isServicedFromCache() ); + + // Deserialize ClientDataInfo from the broker result if available + final ClientDataInfo clientDataInfo = + ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw()); + if (clientDataInfo != null) { + localAuthResult.setClientDataInfo(clientDataInfo); + } + + return localAuthResult; } @NonNull @@ -516,6 +543,14 @@ public BaseException getBaseExceptionFromBundle(@NonNull final Bundle resultBund baseException.setBrokerPerformanceMetrics(metrics); } + // Restore ClientDataInfo (server telemetry) from the broker result so callers + // catching the exception can inspect server-side error context. + if (!StringUtil.isNullOrEmpty(brokerResult.getClientDataInfoRaw())) { + baseException.setClientDataInfo( + ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw()) + ); + } + // Set broker app info if available if (resultBundle.containsKey(AuthenticationConstants.Broker.BROKER_VERSION)) { baseException.setBrokerAppVersion( @@ -994,7 +1029,16 @@ public AcquireTokenResult getDeviceCodeFlowTokenResultFromResultBundle(@NonNull if (resultBundle.getBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) { final AcquireTokenResult acquireTokenResult = new AcquireTokenResult(); - acquireTokenResult.setLocalAuthenticationResult(authenticationResultFromBundle(resultBundle)); + final ILocalAuthenticationResult authResult = authenticationResultFromBundle(resultBundle); + acquireTokenResult.setLocalAuthenticationResult(authResult); + + // Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult + if (authResult instanceof LocalAuthenticationResult) { + acquireTokenResult.setClientDataInfo( + ((LocalAuthenticationResult) authResult).getClientDataInfo() + ); + } + span.setStatus(StatusCode.OK); return acquireTokenResult; } else if (brokerResult.getErrorCode().equals(ErrorStrings.DEVICE_CODE_FLOW_AUTHORIZATION_PENDING_ERROR_CODE)) { @@ -1018,9 +1062,16 @@ AcquireTokenResult getAcquireTokenResultFromResultBundle(@NonNull final Bundle r final MsalBrokerResultAdapter resultAdapter = new MsalBrokerResultAdapter(); if (resultBundle.getBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS)) { final AcquireTokenResult acquireTokenResult = new AcquireTokenResult(); - acquireTokenResult.setLocalAuthenticationResult( - resultAdapter.authenticationResultFromBundle(resultBundle) - ); + final ILocalAuthenticationResult authResult = + resultAdapter.authenticationResultFromBundle(resultBundle); + acquireTokenResult.setLocalAuthenticationResult(authResult); + + // Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult + if (authResult instanceof LocalAuthenticationResult) { + acquireTokenResult.setClientDataInfo( + ((LocalAuthenticationResult) authResult).getClientDataInfo() + ); + } // Set broker performance metrics if available final BrokerPerformanceMetrics metrics = resultAdapter.getBrokerPerformanceMetricsFromBundle(resultBundle); if (metrics != null) { diff --git a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt index 09c720ab7a..a16ca126cb 100644 --- a/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt +++ b/common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt @@ -36,6 +36,7 @@ import com.microsoft.identity.common.java.exception.ClientException import com.microsoft.identity.common.java.exception.UiRequiredException import com.microsoft.identity.common.java.request.SdkType import com.microsoft.identity.common.java.result.LocalAuthenticationResult +import com.microsoft.identity.common.java.telemetry.ClientDataInfo import com.microsoft.identity.common.java.util.SchemaUtil import com.microsoft.identity.internal.testutils.MockRecords import lombok.SneakyThrows @@ -658,4 +659,81 @@ class MsalBrokerResultAdapterTests { resultString.contains(SchemaUtil.MISSING_FROM_THE_TOKEN_RESPONSE) ) } + + // ==================== ClientDataInfo IPC round-trip tests (PR #3109) ==================== + + private val clientDataRaw = "m|AADSTS50058|login_required|us|public" + + private fun newCacheRecord() = CacheRecord.builder() + .account(MockRecords.getMockAccountRecord_AAD()) + .idToken(MockRecords.getMockIdTokenRecord_AAD()) + .accessToken(MockRecords.getMockAccessTokenRecord_AAD()) + .refreshToken(MockRecords.getMockRefreshTokenRecord_AAD()) + .build() + + @Test + fun testClientDataInfo_RoundTripsThroughBrokerResult_OnSuccess() { + val cacheRecord = newCacheRecord() + val cacheRecords: MutableList = arrayListOf(cacheRecord) + val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false) + authResult.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw) + + val brokerResult = getInstance().buildBrokerResultFromAuthenticationResult(authResult, "16.0") + assertEquals("Raw payload should be serialized into BrokerResult", clientDataRaw, brokerResult.clientDataInfoRaw) + } + + @Test + fun testClientDataInfo_NullOnLocalAuthResult_ResultsInNullOnBrokerResult() { + val cacheRecord = newCacheRecord() + val cacheRecords: MutableList = arrayListOf(cacheRecord) + val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false) + // No ClientDataInfo set + + val brokerResult = getInstance().buildBrokerResultFromAuthenticationResult(authResult, "16.0") + assertNull(brokerResult.clientDataInfoRaw) + } + + @Test + fun testClientDataInfo_RoundTripsThroughBaseExceptionBundle() { + val exception = ClientException("invalid_grant", "token failure") + exception.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw) + + val resultAdapter = MsalBrokerResultAdapter() + val resultBundle = resultAdapter.bundleFromBaseException(exception, null) + val brokerResult = resultAdapter.brokerResultFromBundle(resultBundle) + assertEquals(clientDataRaw, brokerResult.clientDataInfoRaw) + + val received = resultAdapter.getBaseExceptionFromBundle(resultBundle) + assertNotNull("ClientDataInfo should be reconstructed on the exception", received.clientDataInfo) + assertEquals("AADSTS50058", received.clientDataInfo!!.error) + assertEquals("login_required", received.clientDataInfo!!.subError) + assertEquals(clientDataRaw, received.clientDataInfo!!.raw) + } + + @Test + fun testClientDataInfo_NullOnException_NotInBundle() { + val exception = ClientException("invalid_grant", "token failure") + // No ClientDataInfo set + + val resultAdapter = MsalBrokerResultAdapter() + val resultBundle = resultAdapter.bundleFromBaseException(exception, null) + val received = resultAdapter.getBaseExceptionFromBundle(resultBundle) + assertNull(received.clientDataInfo) + } + + @Test + fun testClientDataInfo_RoundTripsThroughGetAcquireTokenResultFromResultBundle() { + val cacheRecord = newCacheRecord() + val cacheRecords: MutableList = arrayListOf(cacheRecord) + val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false) + authResult.clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataRaw) + + val resultAdapter = MsalBrokerResultAdapter() + val resultBundle = resultAdapter.bundleFromAuthenticationResult(authResult, "16.0") + + val acquireTokenResult = resultAdapter.getAcquireTokenResultFromResultBundle(resultBundle) + assertNotNull("ClientDataInfo should be present on AcquireTokenResult", acquireTokenResult.clientDataInfo) + assertEquals("AADSTS50058", acquireTokenResult.clientDataInfo!!.error) + assertEquals(clientDataRaw, acquireTokenResult.clientDataInfo!!.raw) + } } \ No newline at end of file diff --git a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java index 514ed80812..7ae884b03c 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java +++ b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java @@ -219,6 +219,9 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma final TokenResult tokenResult = oAuth2Strategy.requestToken(ropcTokenRequest); acquireTokenResult.setTokenResult(tokenResult); + if (tokenResult != null) { + acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } @SuppressWarnings(WarningType.rawtype_warning) final OAuth2TokenCache tokenCache = parameters.getOAuth2TokenCache(); @@ -251,6 +254,9 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma Telemetry.emit(new CacheEndEvent()); } + // Set server client data info on the authentication result for IPC propagation + authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + // Set the AuthenticationResult on the final result object acquireTokenResult.setLocalAuthenticationResult(authenticationResult); } @@ -484,6 +490,7 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para ); acquireTokenSilentResult.setTokenResult(tokenResult); + acquireTokenSilentResult.setClientDataInfo(tokenResult.getClientDataInfo()); ResultUtil.logResult(methodTag, tokenResult); @@ -528,6 +535,9 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para Telemetry.emit(new CacheEndEvent()); } + // Set server client data info on the authentication result for IPC propagation + authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + // Set the AuthenticationResult on the final result object acquireTokenSilentResult.setLocalAuthenticationResult(authenticationResult); } else { diff --git a/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java b/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java index be1f95bba3..40b46059eb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java +++ b/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java @@ -44,6 +44,7 @@ import com.microsoft.identity.common.java.opentelemetry.AttributeName; import com.microsoft.identity.common.java.opentelemetry.SpanExtension; import com.microsoft.identity.common.java.providers.microsoft.MicrosoftAuthorizationErrorResponse; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationErrorResponse; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; import com.microsoft.identity.common.java.providers.oauth2.TokenErrorResponse; @@ -84,7 +85,15 @@ public static BaseException exceptionFromAcquireTokenResult(final AcquireTokenRe if (null != authorizationResult) { if (!authorizationResult.getSuccess()) { - return exceptionFromAuthorizationResult(authorizationResult, commandParameters); + final BaseException authException = exceptionFromAuthorizationResult(authorizationResult, commandParameters); + // Attach ClientDataInfo from the authorize redirect (clientdata query param) + // so callers can inspect server-side error context on auth failures. + if (authorizationResult instanceof MicrosoftStsAuthorizationResult) { + authException.setClientDataInfo( + ((MicrosoftStsAuthorizationResult) authorizationResult).getClientDataInfo() + ); + } + return authException; } } else { Logger.warn( @@ -183,6 +192,7 @@ public static ServiceException exceptionFromTokenResult(final TokenResult tokenR outErr = getExceptionFromTokenErrorResponse(commandParameters, tokenResult.getErrorResponse()); applyCliTelemInfo(tokenResult.getCliTelemInfo(), outErr); + outErr.setClientDataInfo(tokenResult.getClientDataInfo()); } else { Logger.warn( TAG + methodName, diff --git a/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java b/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java index be58d7e019..2ec6b7d139 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java +++ b/common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java @@ -23,6 +23,7 @@ package com.microsoft.identity.common.java.exception; import com.microsoft.identity.common.java.broker.IBrokerInfoProvider; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import com.microsoft.identity.common.java.telemetry.ITelemetryAccessor; import com.microsoft.identity.common.java.telemetry.Telemetry; import com.microsoft.identity.common.java.telemetry.events.ErrorEvent; @@ -70,6 +71,14 @@ public class BaseException extends Exception implements IErrorInformation, ITele @Nullable private String mCliTelemSubErrorCode; + /** + * Server-side telemetry from the x-ms-clientdata response header (/token error responses) + * or the clientdata query parameter (/authorize error redirects). Useful for diagnosing + * server-driven failures (account_type, error, sub_error, caller_data_boundary, cloud_instance). + */ + @Nullable + private ClientDataInfo mClientDataInfo; + private String mErrorCode; private String mSubErrorCode; @@ -213,6 +222,15 @@ public void setCliTelemSubErrorCode(@Nullable final String cliTelemSubErrorCode) this.mCliTelemSubErrorCode = cliTelemSubErrorCode; } + @Nullable + public ClientDataInfo getClientDataInfo() { + return mClientDataInfo; + } + + public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { + this.mClientDataInfo = clientDataInfo; + } + @Nullable public String getCorrelationId() { return mCorrelationId; diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/AbstractMicrosoftStsTokenResponseHandler.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/AbstractMicrosoftStsTokenResponseHandler.java index 2c5beb74b4..39249e9d48 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/AbstractMicrosoftStsTokenResponseHandler.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/AbstractMicrosoftStsTokenResponseHandler.java @@ -113,6 +113,7 @@ public TokenResult handleTokenResponse(@NonNull final HttpResponse response) thr final ClientDataInfo clientDataInfo = ClientDataInfo.fromPipeDelimited(clientDataHeader); if (null != clientDataInfo) { clientDataInfo.emitToSpan(); + result.setClientDataInfo(clientDataInfo); } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResult.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResult.java index d4f80771e0..58898c7d7e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResult.java @@ -26,6 +26,9 @@ import com.microsoft.identity.common.java.providers.oauth2.AuthorizationErrorResponse; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResponse; import com.microsoft.identity.common.java.providers.oauth2.AuthorizationStatus; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; + +import javax.annotation.Nullable; /** * Sub class of {@link MicrosoftAuthorizationResult}. @@ -34,6 +37,9 @@ public class MicrosoftStsAuthorizationResult extends MicrosoftAuthorizationResult { + @Nullable + private ClientDataInfo mClientDataInfo; + /** * Constructor of {@link MicrosoftStsAuthorizationResult}. * @@ -54,4 +60,22 @@ public MicrosoftStsAuthorizationResult(final AuthorizationStatus authStatus, fin super(authStatus, errorResponse); } + /** + * Gets the {@link ClientDataInfo} parsed from the clientdata redirect query parameter. + * + * @return The ClientDataInfo, or null if the parameter was absent or unparseable. + */ + @Nullable + public ClientDataInfo getClientDataInfo() { + return mClientDataInfo; + } + + /** + * Sets the {@link ClientDataInfo} parsed from the clientdata redirect query parameter. + * + * @param clientDataInfo The ClientDataInfo to set. + */ + public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { + mClientDataInfo = clientDataInfo; + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java index 50b03493ce..3674e14c5e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java @@ -75,8 +75,9 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization final Map urlParameters = UrlUtil.getParameters(redirectUri); + ClientDataInfo clientDataInfo = null; if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { - final ClientDataInfo clientDataInfo = ClientDataInfo.fromPipeDelimited(urlParameters.get(ClientDataInfo.CLIENTDATA_QUERY_PARAMETER)); + clientDataInfo = ClientDataInfo.fromPipeDelimited(urlParameters.get(ClientDataInfo.CLIENTDATA_QUERY_PARAMETER)); if (null != clientDataInfo) { clientDataInfo.emitToSpan(); } @@ -107,6 +108,8 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization ); } + result.setClientDataInfo(clientDataInfo); + return result; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenResult.java index aa10b49dfc..0d875c962d 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/oauth2/TokenResult.java @@ -23,6 +23,9 @@ package com.microsoft.identity.common.java.providers.oauth2; import com.microsoft.identity.common.java.telemetry.CliTelemInfo; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; + +import javax.annotation.Nullable; /** * Holds the request of a token request. The request will either contain the success result or the error result. @@ -32,6 +35,8 @@ public class TokenResult implements IResult { private TokenResponse mTokenResponse; private TokenErrorResponse mTokenErrorResponse; private CliTelemInfo mCliTelemInfo; + @Nullable + private ClientDataInfo mClientDataInfo; private boolean mSuccess = false; public TokenResult() { @@ -110,6 +115,25 @@ public void setCliTelemInfo(final CliTelemInfo cliTelemInfo) { mCliTelemInfo = cliTelemInfo; } + /** + * Gets the {@link ClientDataInfo} parsed from the x-ms-clientdata response header. + * + * @return The ClientDataInfo, or null if the header was absent or unparseable. + */ + @Nullable + public ClientDataInfo getClientDataInfo() { + return mClientDataInfo; + } + + /** + * Sets the {@link ClientDataInfo} parsed from the x-ms-clientdata response header. + * + * @param clientDataInfo The ClientDataInfo to set. + */ + public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { + mClientDataInfo = clientDataInfo; + } + /** * Returns whether the token request was successful or not. * diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java index 28fd66a2a3..cba52111eb 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java @@ -28,6 +28,7 @@ import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; import com.microsoft.identity.common.java.broker.BrokerPerformanceMetrics; import com.microsoft.identity.common.java.broker.IBrokerPerformanceMetricsProvider; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import javax.annotation.Nullable; @@ -49,6 +50,9 @@ public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IB private BrokerPerformanceMetrics mBrokerPerformanceMetrics; + @Nullable + private ClientDataInfo mClientDataInfo; + public void setLocalAuthenticationResult(ILocalAuthenticationResult result) { this.mLocalAuthenticationResult = result; this.mSucceeded = true; @@ -106,4 +110,24 @@ public String getBrokerAppVersion() { public String getBrokerAppPackageName() { return mBrokerAppPackageName; } + + /** + * Gets the {@link ClientDataInfo} containing server-side telemetry data from the + * x-ms-clientdata response header (/token) or clientdata redirect query parameter (/authorize). + * + * @return The ClientDataInfo, or null if not available. + */ + @Nullable + public ClientDataInfo getClientDataInfo() { + return mClientDataInfo; + } + + /** + * Sets the {@link ClientDataInfo} containing server-side telemetry data. + * + * @param clientDataInfo The ClientDataInfo to set. + */ + public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { + mClientDataInfo = clientDataInfo; + } } diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java index 7ce96b274f..aedd10273a 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java @@ -29,6 +29,7 @@ import com.microsoft.identity.common.java.logging.Logger; import com.microsoft.identity.common.java.request.ILocalAuthenticationCallback; import com.microsoft.identity.common.java.request.SdkType; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import com.microsoft.identity.common.java.telemetry.ITelemetryAccessor; import com.microsoft.identity.common.java.util.StringUtil; @@ -54,6 +55,8 @@ public class LocalAuthenticationResult implements ILocalAuthenticationResult, IT private String mFamilyId; private String mSpeRing; private String mRefreshTokenAge; + @Nullable + private ClientDataInfo mClientDataInfo; private List mCompleteResultFromCache; private boolean mServicedFromCache; private String mCorrelationId; @@ -203,6 +206,25 @@ public void setRefreshTokenAge(final String refreshTokenAge) { mRefreshTokenAge = refreshTokenAge; } + /** + * Gets the server client data info from the x-ms-clientdata response header. + * + * @return The ClientDataInfo, or null if not available. + */ + @Nullable + public ClientDataInfo getClientDataInfo() { + return mClientDataInfo; + } + + /** + * Sets the server client data info. + * + * @param clientDataInfo The ClientDataInfo to set. + */ + public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { + mClientDataInfo = clientDataInfo; + } + @Override @NonNull public AccessTokenRecord getAccessTokenRecord() { diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/ClientDataInfo.java b/common4j/src/main/com/microsoft/identity/common/java/telemetry/ClientDataInfo.java index 75844688db..fd19094f8e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/telemetry/ClientDataInfo.java +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/ClientDataInfo.java @@ -29,8 +29,9 @@ import edu.umd.cs.findbugs.annotations.Nullable; import io.opentelemetry.api.trace.Span; +import lombok.AccessLevel; import lombok.Getter; -import lombok.Setter; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; /** @@ -40,7 +41,7 @@ * Contains server-side error codes, account type, cloud instance, and data boundary info. */ @Getter -@Setter +@NoArgsConstructor(access = AccessLevel.PRIVATE) @Accessors(prefix = "m") public class ClientDataInfo { @@ -85,6 +86,14 @@ public class ClientDataInfo { private String mCloudInstance; private String mCallerDataBoundary; + /** + * The original raw pipe-delimited string this instance was parsed from. + * Always set when the instance was constructed via {@link #fromPipeDelimited(String)}, + * which is the only public entry point. Exposed for partner teams that want to + * inspect or forward the unparsed payload. + */ + private String mRaw; + /** * Parses an already-decoded pipe-delimited clientdata query parameter value. * The caller is responsible for URL-decoding before passing (e.g. values from @@ -110,6 +119,7 @@ public static ClientDataInfo fromPipeDelimited(@Nullable final String decodedVal } final ClientDataInfo info = new ClientDataInfo(); + info.mRaw = decodedValue; info.mAccountType = emptyToNull(segments[PIPE_INDEX_ACCOUNT_TYPE]); info.mError = emptyToNull(segments[PIPE_INDEX_ERROR]); info.mSubError = emptyToNull(segments[PIPE_INDEX_SUB_ERROR]); diff --git a/common4j/src/test/com/microsoft/identity/common/java/controllers/ExceptionAdapterTests.java b/common4j/src/test/com/microsoft/identity/common/java/controllers/ExceptionAdapterTests.java index 351ab1249e..0de716fc3d 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/controllers/ExceptionAdapterTests.java +++ b/common4j/src/test/com/microsoft/identity/common/java/controllers/ExceptionAdapterTests.java @@ -43,6 +43,8 @@ import com.microsoft.identity.common.java.exception.UiRequiredException; import com.microsoft.identity.common.java.providers.microsoft.MicrosoftTokenErrorResponse; import com.microsoft.identity.common.java.providers.oauth2.TokenErrorResponse; +import com.microsoft.identity.common.java.providers.oauth2.TokenResult; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import org.junit.Assert; import org.junit.Test; @@ -212,4 +214,41 @@ public void testGetExceptionFromTokenErrorResponse_NullCommandParameters() { assertEquals(OAuth2ErrorCode.INVALID_GRANT, exception.getErrorCode()); assertEquals("UI required.", exception.getMessage()); } + + // ----------------------------------------------------------------------- + // ClientDataInfo wiring tests (PR #3109) + // ----------------------------------------------------------------------- + + @Test + public void testExceptionFromTokenResult_attachesClientDataInfo() { + final TokenErrorResponse errorResponse = new TokenErrorResponse(); + errorResponse.setError(OAuth2ErrorCode.INVALID_GRANT); + errorResponse.setErrorDescription("token failure"); + + final ClientDataInfo clientDataInfo = ClientDataInfo.fromPipeDelimited("m|AADSTS50058|login_required|us|public"); + Assert.assertNotNull(clientDataInfo); + + final TokenResult tokenResult = new TokenResult(null, errorResponse); + tokenResult.setClientDataInfo(clientDataInfo); + + final ServiceException exception = ExceptionAdapter.exceptionFromTokenResult(tokenResult, null); + + Assert.assertNotNull("ClientDataInfo should be attached to the exception", exception.getClientDataInfo()); + assertEquals("AADSTS50058", exception.getClientDataInfo().getError()); + assertEquals("login_required", exception.getClientDataInfo().getSubError()); + assertEquals("m|AADSTS50058|login_required|us|public", exception.getClientDataInfo().getRaw()); + } + + @Test + public void testExceptionFromTokenResult_nullClientDataInfo_doesNotThrow() { + final TokenErrorResponse errorResponse = new TokenErrorResponse(); + errorResponse.setError(OAuth2ErrorCode.INVALID_GRANT); + errorResponse.setErrorDescription("token failure"); + + final TokenResult tokenResult = new TokenResult(null, errorResponse); + // No ClientDataInfo set + + final ServiceException exception = ExceptionAdapter.exceptionFromTokenResult(tokenResult, null); + Assert.assertNull(exception.getClientDataInfo()); + } } \ No newline at end of file diff --git a/common4j/src/test/com/microsoft/identity/common/java/telemetry/ClientDataInfoTest.java b/common4j/src/test/com/microsoft/identity/common/java/telemetry/ClientDataInfoTest.java index 7b4e0f37a4..ef5e4e1330 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/telemetry/ClientDataInfoTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/telemetry/ClientDataInfoTest.java @@ -54,7 +54,8 @@ public class ClientDataInfoTest { @Test public void fromPipeDelimited_validFiveSegments_allFieldsParsed() { // format: account_type|error|sub_error|caller_data_boundary|cloud_instance - final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("m|AADSTS50058|login_required|us|public"); + final String raw = "m|AADSTS50058|login_required|us|public"; + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited(raw); assertNotNull(info); assertEquals("m", info.getAccountType()); @@ -62,6 +63,7 @@ public void fromPipeDelimited_validFiveSegments_allFieldsParsed() { assertEquals("login_required", info.getSubError()); assertEquals("us", info.getCallerDataBoundary()); assertEquals("public", info.getCloudInstance()); + assertEquals(raw, info.getRaw()); } @Test @@ -108,12 +110,9 @@ public void fromPipeDelimited_emptyString_returnsNull() { @Test public void emitToSpan_allFieldsSet_allAttributesEmitted() { - final ClientDataInfo info = new ClientDataInfo(); - info.setError("AADSTS50058"); - info.setSubError("login_required"); - info.setAccountType("m"); - info.setCloudInstance("public"); - info.setCallerDataBoundary("us"); + // format: account_type|error|sub_error|caller_data_boundary|cloud_instance + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("m|AADSTS50058|login_required|us|public"); + assertNotNull(info); final Span mockSpan = mock(Span.class); when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); @@ -133,9 +132,9 @@ public void emitToSpan_allFieldsSet_allAttributesEmitted() { @Test public void emitToSpan_someFieldsNull_nullFieldsNotEmitted() { - final ClientDataInfo info = new ClientDataInfo(); - info.setError("AADSTS50058"); - // subError, accountType, cloudInstance, callerDataBoundary all null + // Only error populated; account_type and sub_error empty + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("|AADSTS50058|"); + assertNotNull(info); final Span mockSpan = mock(Span.class); when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); @@ -159,8 +158,9 @@ public void emitToSpan_someFieldsNull_nullFieldsNotEmitted() { @Test public void emitToSpan_accountTypeMsa_mappedToMSA() { - final ClientDataInfo info = new ClientDataInfo(); - info.setAccountType("m"); + // Only account_type populated (m for MSA) + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("m||"); + assertNotNull(info); final Span mockSpan = mock(Span.class); when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); @@ -176,8 +176,9 @@ public void emitToSpan_accountTypeMsa_mappedToMSA() { @Test public void emitToSpan_accountTypeAad_mappedToAAD() { - final ClientDataInfo info = new ClientDataInfo(); - info.setAccountType("e"); + // Only account_type populated (e for AAD) + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("e||"); + assertNotNull(info); final Span mockSpan = mock(Span.class); when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); @@ -207,8 +208,9 @@ public void emitToSpan_fieldExceeds256Chars_truncatedTo256() { expected256.append('A'); } - final ClientDataInfo info = new ClientDataInfo(); - info.setError(longValue); + // Construct via the only public entry point; long value goes in the error field + final ClientDataInfo info = ClientDataInfo.fromPipeDelimited("|" + longValue + "|"); + assertNotNull(info); final Span mockSpan = mock(Span.class); when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); From 2b5bda189c12bc35d9f2f92e9f323679a1985cfe Mon Sep 17 00:00:00 2001 From: fadidurah Date: Thu, 14 May 2026 18:36:14 -0400 Subject: [PATCH 02/14] Make AcquireTokenResult.getClientDataInfo() delegate to LocalAuthenticationResult LocalAuthenticationResult is the single canonical owner of ClientDataInfo (it's the only result type that survives the broker IPC boundary). Remove the redundant mClientDataInfo field/setter from AcquireTokenResult; getClientDataInfo() now delegates to the underlying LocalAuthenticationResult. Update controllers (LocalMSALController, BaseController) and MsalBrokerResultAdapter to write CDI directly to LocalAuthenticationResult, sourcing it from TokenResult (preferred) with fallback to MicrosoftStsAuthorizationResult. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../controllers/LocalMSALController.java | 27 +++++++------------ .../result/MsalBrokerResultAdapter.java | 13 --------- .../java/controllers/BaseController.java | 4 --- .../java/result/AcquireTokenResult.java | 17 +++--------- 4 files changed, 14 insertions(+), 47 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java index f50533c251..fbdc643d7b 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java @@ -169,13 +169,6 @@ public AcquireTokenResult acquireToken( ); acquireTokenResult.setAuthorizationResult(result); - // Wire ClientDataInfo from the authorization result (authorize endpoint). - if (result instanceof MicrosoftStsAuthorizationResult) { - acquireTokenResult.setClientDataInfo( - ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() - ); - } - ResultUtil.logResult(TAG, result); if (result.getAuthorizationStatus().equals(AuthorizationStatus.SUCCESS)) { @@ -189,11 +182,6 @@ public AcquireTokenResult acquireToken( acquireTokenResult.setTokenResult(tokenResult); - // Prefer ClientDataInfo from the token endpoint (later, more authoritative call). - if (tokenResult != null && tokenResult.getClientDataInfo() != null) { - acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); - } - if (tokenResult != null && tokenResult.getSuccess()) { //4) Save tokens in token cache final List records = saveTokens( @@ -219,10 +207,18 @@ public AcquireTokenResult acquireToken( ) ); - // Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation + // Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation. + // Prefer the token-endpoint value (later, more authoritative); fall back to the + // authorize-endpoint value. final LocalAuthenticationResult localResult = (LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult(); - localResult.setClientDataInfo(acquireTokenResult.getClientDataInfo()); + if (tokenResult != null && tokenResult.getClientDataInfo() != null) { + localResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } else if (result instanceof MicrosoftStsAuthorizationResult) { + localResult.setClientDataInfo( + ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() + ); + } } } @@ -760,9 +756,6 @@ public AcquireTokenResult acquireDeviceCodeFlowToken( // Assign token result acquireTokenResult.setTokenResult(tokenResult); - if (tokenResult != null) { - acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); - } // If the token is valid, save it into token cache final List records = saveTokens( diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java index 8faea6d324..eefb48e59b 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java @@ -1032,13 +1032,6 @@ public AcquireTokenResult getDeviceCodeFlowTokenResultFromResultBundle(@NonNull final ILocalAuthenticationResult authResult = authenticationResultFromBundle(resultBundle); acquireTokenResult.setLocalAuthenticationResult(authResult); - // Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult - if (authResult instanceof LocalAuthenticationResult) { - acquireTokenResult.setClientDataInfo( - ((LocalAuthenticationResult) authResult).getClientDataInfo() - ); - } - span.setStatus(StatusCode.OK); return acquireTokenResult; } else if (brokerResult.getErrorCode().equals(ErrorStrings.DEVICE_CODE_FLOW_AUTHORIZATION_PENDING_ERROR_CODE)) { @@ -1066,12 +1059,6 @@ AcquireTokenResult getAcquireTokenResultFromResultBundle(@NonNull final Bundle r resultAdapter.authenticationResultFromBundle(resultBundle); acquireTokenResult.setLocalAuthenticationResult(authResult); - // Propagate ClientDataInfo from LocalAuthenticationResult to AcquireTokenResult - if (authResult instanceof LocalAuthenticationResult) { - acquireTokenResult.setClientDataInfo( - ((LocalAuthenticationResult) authResult).getClientDataInfo() - ); - } // Set broker performance metrics if available final BrokerPerformanceMetrics metrics = resultAdapter.getBrokerPerformanceMetricsFromBundle(resultBundle); if (metrics != null) { diff --git a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java index 7ae884b03c..2a461be314 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java +++ b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java @@ -219,9 +219,6 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma final TokenResult tokenResult = oAuth2Strategy.requestToken(ropcTokenRequest); acquireTokenResult.setTokenResult(tokenResult); - if (tokenResult != null) { - acquireTokenResult.setClientDataInfo(tokenResult.getClientDataInfo()); - } @SuppressWarnings(WarningType.rawtype_warning) final OAuth2TokenCache tokenCache = parameters.getOAuth2TokenCache(); @@ -490,7 +487,6 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para ); acquireTokenSilentResult.setTokenResult(tokenResult); - acquireTokenSilentResult.setClientDataInfo(tokenResult.getClientDataInfo()); ResultUtil.logResult(methodTag, tokenResult); diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java index cba52111eb..26234b3770 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java @@ -50,9 +50,6 @@ public class AcquireTokenResult implements IBrokerPerformanceMetricsProvider, IB private BrokerPerformanceMetrics mBrokerPerformanceMetrics; - @Nullable - private ClientDataInfo mClientDataInfo; - public void setLocalAuthenticationResult(ILocalAuthenticationResult result) { this.mLocalAuthenticationResult = result; this.mSucceeded = true; @@ -119,15 +116,9 @@ public String getBrokerAppPackageName() { */ @Nullable public ClientDataInfo getClientDataInfo() { - return mClientDataInfo; - } - - /** - * Sets the {@link ClientDataInfo} containing server-side telemetry data. - * - * @param clientDataInfo The ClientDataInfo to set. - */ - public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { - mClientDataInfo = clientDataInfo; + if (mLocalAuthenticationResult instanceof LocalAuthenticationResult) { + return ((LocalAuthenticationResult) mLocalAuthenticationResult).getClientDataInfo(); + } + return null; } } From fa1d106ecf3ea5afefb53ff856dbe90c3186938d Mon Sep 17 00:00:00 2001 From: fadidurah Date: Thu, 14 May 2026 18:40:50 -0400 Subject: [PATCH 03/14] Add fallback chain in AcquireTokenResult.getClientDataInfo() Resolve in order: LocalAuthenticationResult -> TokenResult -> MicrosoftStsAuthorizationResult. This ensures callers can retrieve CDI even on failure paths where LocalAuthenticationResult was never constructed (e.g., token-error or authorize-error responses). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/result/AcquireTokenResult.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java index 26234b3770..69164a8b23 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/AcquireTokenResult.java @@ -28,6 +28,7 @@ import com.microsoft.identity.common.java.providers.oauth2.AuthorizationResult; import com.microsoft.identity.common.java.broker.BrokerPerformanceMetrics; import com.microsoft.identity.common.java.broker.IBrokerPerformanceMetricsProvider; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult; import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import javax.annotation.Nullable; @@ -112,12 +113,32 @@ public String getBrokerAppPackageName() { * Gets the {@link ClientDataInfo} containing server-side telemetry data from the * x-ms-clientdata response header (/token) or clientdata redirect query parameter (/authorize). * - * @return The ClientDataInfo, or null if not available. + *

Resolution order: + *

    + *
  1. {@link LocalAuthenticationResult} — authoritative on success paths; the only carrier + * that survives the broker IPC boundary.
  2. + *
  3. {@link TokenResult} — fallback for paths where token call succeeded but no + * {@code LocalAuthenticationResult} was constructed (e.g., error responses).
  4. + *
  5. {@link MicrosoftStsAuthorizationResult} — fallback for failures before the + * /token call (e.g., authorize-step errors).
  6. + *
+ * + * @return The ClientDataInfo, or null if not available from any source. */ @Nullable public ClientDataInfo getClientDataInfo() { if (mLocalAuthenticationResult instanceof LocalAuthenticationResult) { - return ((LocalAuthenticationResult) mLocalAuthenticationResult).getClientDataInfo(); + final ClientDataInfo fromLocalAuth = + ((LocalAuthenticationResult) mLocalAuthenticationResult).getClientDataInfo(); + if (fromLocalAuth != null) { + return fromLocalAuth; + } + } + if (mTokenResult != null && mTokenResult.getClientDataInfo() != null) { + return mTokenResult.getClientDataInfo(); + } + if (mAuthorizationResult instanceof MicrosoftStsAuthorizationResult) { + return ((MicrosoftStsAuthorizationResult) mAuthorizationResult).getClientDataInfo(); } return null; } From f849f514da20f69c2e7ab4a2f9d5efde19a9787c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:11:05 +0000 Subject: [PATCH 04/14] Fix: gate setClientDataInfo under null check; update LocalAuthenticationResult javadoc Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/a8f06ac0-2570-4e1e-85f1-ac6a44d64bc5 Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../microsoftsts/MicrosoftStsAuthorizationResultFactory.java | 4 +++- .../common/java/result/LocalAuthenticationResult.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java index 3674e14c5e..12459f9e3e 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java @@ -108,7 +108,9 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization ); } - result.setClientDataInfo(clientDataInfo); + if (null != clientDataInfo) { + result.setClientDataInfo(clientDataInfo); + } return result; } diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java index aedd10273a..7c87fa3695 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java @@ -207,7 +207,9 @@ public void setRefreshTokenAge(final String refreshTokenAge) { } /** - * Gets the server client data info from the x-ms-clientdata response header. + * Gets the server client data info parsed from the {@code x-ms-clientdata} response header + * returned by the {@code /token} endpoint, or from the {@code clientdata} query parameter + * returned in the {@code /authorize} redirect URI. * * @return The ClientDataInfo, or null if not available. */ From accc2181b630852f781a85f3093ac0420b0d0b01 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:20:05 +0000 Subject: [PATCH 05/14] Fix: only attach ClientDataInfo on AuthorizationStatus.SUCCESS; add state-mismatch and valid-state unit tests Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/e73b2124-5bf9-4475-b2ca-e2f3bd6cacbd Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- ...icrosoftStsAuthorizationResultFactory.java | 2 +- ...softStsAuthorizationResultFactoryTest.java | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java index 12459f9e3e..b69c53cdc0 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java @@ -108,7 +108,7 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization ); } - if (null != clientDataInfo) { + if (null != clientDataInfo && AuthorizationStatus.SUCCESS == result.getAuthorizationStatus()) { result.setClientDataInfo(clientDataInfo); } diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java index d5fe1f84ce..1a3ae91786 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java @@ -337,6 +337,50 @@ public void testClientDataParam_noClientDataParam_doesNotCrash() { assertEquals(AuthorizationStatus.SUCCESS, result.getAuthorizationStatus()); } + @Test + public void testClientDataParam_stateMismatch_clientDataInfoNotAttached() { + // Pipe-delimited format: account_type|error|sub_error|caller_data_boundary|cloud_instance + final String redirectUrl = MOCK_REDIRECT_URI + + "?" + MOCK_AUTH_CODE_AND_STATE + + "&" + ClientDataInfo.CLIENTDATA_QUERY_PARAMETER + "=m%7CAADSTS50058%7Clogin_required%7Cus%7Cpublic"; + + // Pass a mismatched state to trigger state validation failure (potential CSRF redirect) + final MicrosoftStsAuthorizationResult result = (MicrosoftStsAuthorizationResult) + mAuthorizationResultFactory.createAuthorizationResult( + RawAuthorizationResult.fromRedirectUri(redirectUrl), + getMstsAuthorizationRequestWithState("incorrect_state")); + + assertNotNull(result); + assertEquals(AuthorizationStatus.FAIL, result.getAuthorizationStatus()); + // ClientDataInfo must NOT be attached for untrusted/state-mismatch redirects + assertNull(result.getClientDataInfo()); + } + + @Test + public void testClientDataParam_validState_clientDataInfoAttached() { + // Pipe-delimited format: account_type|error|sub_error|caller_data_boundary|cloud_instance + final String redirectUrl = MOCK_REDIRECT_URI + + "?code=auth_code&state=" + MOCK_STATE_ENCODED + + "&" + ClientDataInfo.CLIENTDATA_QUERY_PARAMETER + "=m%7CAADSTS50058%7Clogin_required%7Cus%7Cpublic"; + + final Span mockSpan = mock(Span.class); + when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); + + try (MockedStatic mockedExtension = Mockito.mockStatic(SpanExtension.class)) { + mockedExtension.when(SpanExtension::current).thenReturn(mockSpan); + + final MicrosoftStsAuthorizationResult result = (MicrosoftStsAuthorizationResult) + mAuthorizationResultFactory.createAuthorizationResult( + RawAuthorizationResult.fromRedirectUri(redirectUrl), getMstsAuthorizationRequest()); + + assertNotNull(result); + assertEquals(AuthorizationStatus.SUCCESS, result.getAuthorizationStatus()); + // ClientDataInfo must be attached when state validation passes + assertNotNull(result.getClientDataInfo()); + } + } + + @Test public void testClientDataParam_flightDisabled_attributesNotEmitted() { final MockFlightsProvider provider = new MockFlightsProvider(); From 7f33c2d698a86a360d2e49842a42ac71ca03fa2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:25:25 +0000 Subject: [PATCH 06/14] Simplify LocalAuthenticationResult javadoc; add AcquireTokenResult getClientDataInfo tests Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/435b4e66-a11f-40e5-a60b-99e49c8ebb6f Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../result/LocalAuthenticationResult.java | 5 +- .../java/result/AcquireTokenResultTest.java | 216 ++++++++++++++++++ 2 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java diff --git a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java index 7c87fa3695..d820e54335 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java +++ b/common4j/src/main/com/microsoft/identity/common/java/result/LocalAuthenticationResult.java @@ -207,9 +207,8 @@ public void setRefreshTokenAge(final String refreshTokenAge) { } /** - * Gets the server client data info parsed from the {@code x-ms-clientdata} response header - * returned by the {@code /token} endpoint, or from the {@code clientdata} query parameter - * returned in the {@code /authorize} redirect URI. + * Gets the server client data info parsed from the {@code x-ms-clientdata} header, which can + * be returned by either the {@code /authorize} or {@code /token} endpoint. * * @return The ClientDataInfo, or null if not available. */ diff --git a/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java new file mode 100644 index 0000000000..76645ffb2b --- /dev/null +++ b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// All rights reserved. +// +// This code is licensed under the MIT License. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files(the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions : +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +package com.microsoft.identity.common.java.result; + +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResponse; +import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResult; +import com.microsoft.identity.common.java.providers.oauth2.AuthorizationStatus; +import com.microsoft.identity.common.java.providers.oauth2.TokenResult; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mockito; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(JUnit4.class) +public class AcquireTokenResultTest { + + private static final String PIPE_DELIMITED_A = "m|AADSTS50058|login_required|us|public"; + private static final String PIPE_DELIMITED_B = "e|AADSTS70011|invalid_scope|eu|sovereign"; + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private ClientDataInfo makeClientDataInfo(final String raw) { + return ClientDataInfo.fromPipeDelimited(raw); + } + + private LocalAuthenticationResult makeLocalAuthResult(final ClientDataInfo clientDataInfo) { + final LocalAuthenticationResult localAuthResult = mock(LocalAuthenticationResult.class); + when(localAuthResult.getClientDataInfo()).thenReturn(clientDataInfo); + return localAuthResult; + } + + private MicrosoftStsAuthorizationResult makeAuthResult(final ClientDataInfo clientDataInfo) { + final MicrosoftStsAuthorizationResult authResult = new MicrosoftStsAuthorizationResult( + AuthorizationStatus.SUCCESS, (MicrosoftStsAuthorizationResponse) null); + authResult.setClientDataInfo(clientDataInfo); + return authResult; + } + + // --------------------------------------------------------------------------- + // No sources populated + // --------------------------------------------------------------------------- + + @Test + public void getClientDataInfo_noSources_returnsNull() { + final AcquireTokenResult result = new AcquireTokenResult(); + assertNull(result.getClientDataInfo()); + } + + // --------------------------------------------------------------------------- + // LocalAuthenticationResult source + // --------------------------------------------------------------------------- + + @Test + public void getClientDataInfo_localAuthResultHasData_returnsIt() { + final ClientDataInfo expected = makeClientDataInfo(PIPE_DELIMITED_A); + final AcquireTokenResult result = new AcquireTokenResult(); + result.setLocalAuthenticationResult(makeLocalAuthResult(expected)); + + assertSame(expected, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_localAuthResultHasNull_fallsBackToTokenResult() { + final ClientDataInfo tokenData = makeClientDataInfo(PIPE_DELIMITED_A); + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(tokenData); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setLocalAuthenticationResult(makeLocalAuthResult(null)); + result.setTokenResult(tokenResult); + + assertSame(tokenData, result.getClientDataInfo()); + } + + // --------------------------------------------------------------------------- + // TokenResult fallback + // --------------------------------------------------------------------------- + + @Test + public void getClientDataInfo_tokenResultHasData_returnsIt() { + final ClientDataInfo expected = makeClientDataInfo(PIPE_DELIMITED_A); + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(expected); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setTokenResult(tokenResult); + + assertSame(expected, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_tokenResultHasNull_fallsBackToAuthResult() { + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_A); + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(null); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setTokenResult(tokenResult); + result.setAuthorizationResult(makeAuthResult(authData)); + + assertSame(authData, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_noTokenResult_fallsBackToAuthResult() { + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_A); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setAuthorizationResult(makeAuthResult(authData)); + + assertSame(authData, result.getClientDataInfo()); + } + + // --------------------------------------------------------------------------- + // MicrosoftStsAuthorizationResult fallback + // --------------------------------------------------------------------------- + + @Test + public void getClientDataInfo_authResultHasData_returnsIt() { + final ClientDataInfo expected = makeClientDataInfo(PIPE_DELIMITED_A); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setAuthorizationResult(makeAuthResult(expected)); + + assertSame(expected, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_authResultHasNull_returnsNull() { + final AcquireTokenResult result = new AcquireTokenResult(); + result.setAuthorizationResult(makeAuthResult(null)); + + assertNull(result.getClientDataInfo()); + } + + // --------------------------------------------------------------------------- + // Precedence: LocalAuthResult > TokenResult > AuthResult + // --------------------------------------------------------------------------- + + @Test + public void getClientDataInfo_allSourcesPopulated_prefersLocalAuthResult() { + final ClientDataInfo localData = makeClientDataInfo(PIPE_DELIMITED_A); + final ClientDataInfo tokenData = makeClientDataInfo(PIPE_DELIMITED_B); + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_A); + + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(tokenData); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setLocalAuthenticationResult(makeLocalAuthResult(localData)); + result.setTokenResult(tokenResult); + result.setAuthorizationResult(makeAuthResult(authData)); + + assertSame(localData, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_localAuthNullTokenPopulated_prefersTokenResult() { + final ClientDataInfo tokenData = makeClientDataInfo(PIPE_DELIMITED_A); + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_B); + + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(tokenData); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setLocalAuthenticationResult(makeLocalAuthResult(null)); + result.setTokenResult(tokenResult); + result.setAuthorizationResult(makeAuthResult(authData)); + + assertSame(tokenData, result.getClientDataInfo()); + } + + @Test + public void getClientDataInfo_localAuthNullTokenNull_returnsAuthResult() { + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_A); + + final TokenResult tokenResult = new TokenResult(); + tokenResult.setClientDataInfo(null); + + final AcquireTokenResult result = new AcquireTokenResult(); + result.setLocalAuthenticationResult(makeLocalAuthResult(null)); + result.setTokenResult(tokenResult); + result.setAuthorizationResult(makeAuthResult(authData)); + + assertSame(authData, result.getClientDataInfo()); + } +} From 0b4f8f8c74443648f246220e1bbc710658ff2f89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:26:03 +0000 Subject: [PATCH 07/14] Use distinct constants for all three precedence test sources in AcquireTokenResultTest Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/435b4e66-a11f-40e5-a60b-99e49c8ebb6f Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../identity/common/java/result/AcquireTokenResultTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java index 76645ffb2b..6532e1deff 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/result/AcquireTokenResultTest.java @@ -43,6 +43,7 @@ public class AcquireTokenResultTest { private static final String PIPE_DELIMITED_A = "m|AADSTS50058|login_required|us|public"; private static final String PIPE_DELIMITED_B = "e|AADSTS70011|invalid_scope|eu|sovereign"; + private static final String PIPE_DELIMITED_C = "m|AADSTS50076|mfa_required|us|public"; // --------------------------------------------------------------------------- // Helpers @@ -170,7 +171,7 @@ public void getClientDataInfo_authResultHasNull_returnsNull() { public void getClientDataInfo_allSourcesPopulated_prefersLocalAuthResult() { final ClientDataInfo localData = makeClientDataInfo(PIPE_DELIMITED_A); final ClientDataInfo tokenData = makeClientDataInfo(PIPE_DELIMITED_B); - final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_A); + final ClientDataInfo authData = makeClientDataInfo(PIPE_DELIMITED_C); final TokenResult tokenResult = new TokenResult(); tokenResult.setClientDataInfo(tokenData); From 60ae66fe21f31f13914ad3245234809a87f06314 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:38:06 +0000 Subject: [PATCH 08/14] Attach ClientDataInfo on all trusted redirects, not just SUCCESS; add server-error test Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/ce73c752-627d-482f-bf86-4d43eb172fad Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- ...icrosoftStsAuthorizationResultFactory.java | 9 ++++-- ...softStsAuthorizationResultFactoryTest.java | 28 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java index b69c53cdc0..da0cd72c53 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java @@ -108,8 +108,13 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization ); } - if (null != clientDataInfo && AuthorizationStatus.SUCCESS == result.getAuthorizationStatus()) { - result.setClientDataInfo(clientDataInfo); + if (null != clientDataInfo) { + final MicrosoftStsAuthorizationErrorResponse errorResponse = result.getAuthorizationErrorResponse(); + final boolean isStateMismatch = errorResponse != null + && ErrorStrings.STATE_MISMATCH.equals(errorResponse.getError()); + if (!isStateMismatch) { + result.setClientDataInfo(clientDataInfo); + } } return result; diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java index 1a3ae91786..68a8f33087 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java @@ -337,6 +337,34 @@ public void testClientDataParam_noClientDataParam_doesNotCrash() { assertEquals(AuthorizationStatus.SUCCESS, result.getAuthorizationStatus()); } + @Test + public void testClientDataParam_serverErrorRedirect_clientDataInfoAttached() { + // When the server returns an error in the redirect (e.g., access_denied), clientDataInfo + // must still be attached — this is a trusted failure we especially want telemetry for. + // Pipe-delimited format: account_type|error|sub_error|caller_data_boundary|cloud_instance + final String redirectUrl = MOCK_REDIRECT_URI + + "?error=access_denied" + + "&error_description=user+denied+consent" + + "&" + ClientDataInfo.CLIENTDATA_QUERY_PARAMETER + "=m%7CAADSTS65004%7Cconsent_required%7Cus%7Cpublic"; + + final Span mockSpan = mock(Span.class); + when(mockSpan.setAttribute(Mockito.anyString(), Mockito.anyString())).thenReturn(mockSpan); + + try (MockedStatic mockedExtension = Mockito.mockStatic(SpanExtension.class)) { + mockedExtension.when(SpanExtension::current).thenReturn(mockSpan); + + final MicrosoftStsAuthorizationResult result = (MicrosoftStsAuthorizationResult) + mAuthorizationResultFactory.createAuthorizationResult( + RawAuthorizationResult.fromRedirectUri(redirectUrl), getMstsAuthorizationRequest()); + + assertNotNull(result); + assertEquals(AuthorizationStatus.FAIL, result.getAuthorizationStatus()); + // ClientDataInfo must be attached even for server-error redirects (not state-mismatch) + assertNotNull(result.getClientDataInfo()); + assertEquals("AADSTS65004", result.getClientDataInfo().getError()); + } + } + @Test public void testClientDataParam_stateMismatch_clientDataInfoNotAttached() { // Pipe-delimited format: account_type|error|sub_error|caller_data_boundary|cloud_instance From f3c3f0bd41ae29a0a214d8880c5039f28695bbcd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 May 2026 23:41:34 +0000 Subject: [PATCH 09/14] Add security rationale comment; rename test to match established naming pattern Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/ce73c752-627d-482f-bf86-4d43eb172fad Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../microsoftsts/MicrosoftStsAuthorizationResultFactory.java | 2 ++ .../MicrosoftStsAuthorizationResultFactoryTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java index da0cd72c53..2fe5762acc 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java +++ b/common4j/src/main/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactory.java @@ -108,6 +108,8 @@ protected MicrosoftStsAuthorizationResult parseRedirectUriAndCreateAuthorization ); } + // Attach ClientDataInfo for both success and trusted server errors. + // Exclude state-mismatch (CSRF) redirects only, as they may originate from untrusted sources. if (null != clientDataInfo) { final MicrosoftStsAuthorizationErrorResponse errorResponse = result.getAuthorizationErrorResponse(); final boolean isStateMismatch = errorResponse != null diff --git a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java index 68a8f33087..38b0af52a1 100644 --- a/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java +++ b/common4j/src/test/com/microsoft/identity/common/java/providers/microsoft/microsoftsts/MicrosoftStsAuthorizationResultFactoryTest.java @@ -338,7 +338,7 @@ public void testClientDataParam_noClientDataParam_doesNotCrash() { } @Test - public void testClientDataParam_serverErrorRedirect_clientDataInfoAttached() { + public void testClientDataParam_serverError_clientDataInfoAttached() { // When the server returns an error in the redirect (e.g., access_denied), clientDataInfo // must still be attached — this is a trusted failure we especially want telemetry for. // Pipe-delimited format: account_type|error|sub_error|caller_data_boundary|cloud_instance From 3ff3ab5cd7f23a4c6f9af84f7ee0a7d2e1fb95bf Mon Sep 17 00:00:00 2001 From: fadidurah Date: Thu, 14 May 2026 23:32:00 -0400 Subject: [PATCH 10/14] Add gradle configure-on-demand for build perf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When configure-on-demand is enabled, Gradle only configures the projects that are actually in the dependency graph of the requested tasks. Previously building AADAuthenticator triggered configure of broker4j, brokerHost, java-linux-test-app, LinuxBroker, brokerautomationapp etc. — none of which are needed for AADAuthenticator:assembleDistRelease. Logs showed ~16 min spent in unnecessary configure phases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- gradle.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/gradle.properties b/gradle.properties index ae84629666..f652101c39 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,6 +6,7 @@ android.useAndroidX=true # https://office.visualstudio.com/Outlook%20Mobile/_wiki/wikis/Outlook-Mobile.wiki/3780/Android-Studio-Gradle-Performance-tips-and-tricks org.gradle.parallel=true org.gradle.daemon=true +org.gradle.configureondemand=true # See https://stackoverflow.com/questions/56075455/expiring-daemon-because-jvm-heap-space-is-exhausted # we must make sure that the total size is <7G, as that's the RAM size of VM on the build pipeline. From d0f38bf34d4150df21772abfa101428d6794af5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 08:24:30 +0000 Subject: [PATCH 11/14] Gate all ClientDataInfo propagation behind ENABLE_SERVER_CLIENT_DATA_TELEMETRY flight Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/22fcd36c-3156-4450-b90e-9bf04390f62b Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../controllers/LocalMSALController.java | 18 +++++++++++------- .../result/MsalBrokerResultAdapter.java | 19 ++++++++++++------- .../java/controllers/BaseController.java | 10 ++++++++-- .../java/controllers/ExceptionAdapter.java | 9 +++++++-- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java index fbdc643d7b..426d76bdbf 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java @@ -59,6 +59,8 @@ import com.microsoft.identity.common.java.result.LocalAuthenticationResult; import com.microsoft.identity.common.java.ui.PreferredAuthMethod; import com.microsoft.identity.common.java.util.ThreadUtils; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.providers.RawAuthorizationResult; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationRequest; import com.microsoft.identity.common.java.providers.microsoft.microsoftsts.MicrosoftStsAuthorizationResponse; @@ -212,12 +214,14 @@ public AcquireTokenResult acquireToken( // authorize-endpoint value. final LocalAuthenticationResult localResult = (LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult(); - if (tokenResult != null && tokenResult.getClientDataInfo() != null) { - localResult.setClientDataInfo(tokenResult.getClientDataInfo()); - } else if (result instanceof MicrosoftStsAuthorizationResult) { - localResult.setClientDataInfo( - ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() - ); + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + if (tokenResult != null && tokenResult.getClientDataInfo() != null) { + localResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } else if (result instanceof MicrosoftStsAuthorizationResult) { + localResult.setClientDataInfo( + ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() + ); + } } } } @@ -780,7 +784,7 @@ public AcquireTokenResult acquireDeviceCodeFlowToken( ); // Set ClientDataInfo on the LocalAuthenticationResult for IPC propagation - if (tokenResult != null) { + if (tokenResult != null && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { final LocalAuthenticationResult localResult = (LocalAuthenticationResult) acquireTokenResult.getLocalAuthenticationResult(); localResult.setClientDataInfo(tokenResult.getClientDataInfo()); diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java index e6afc087b2..b34a3df75a 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java @@ -288,7 +288,8 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth // Serialize ClientDataInfo as raw pipe-delimited string for IPC transfer. // The raw field is populated by ClientDataInfo.fromPipeDelimited(), the only // path that populates the parsed fields, so it is safe to ship as-is. - if (authenticationResult instanceof LocalAuthenticationResult) { + if (authenticationResult instanceof LocalAuthenticationResult + && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { final ClientDataInfo clientDataInfo = ((LocalAuthenticationResult) authenticationResult).getClientDataInfo(); if (clientDataInfo != null) { @@ -425,7 +426,8 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception, // Serialize ClientDataInfo (server telemetry from x-ms-clientdata) so it // survives the broker IPC boundary on error paths. - if (exception.getClientDataInfo() != null) { + if (exception.getClientDataInfo() != null + && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { builder.clientDataInfoRaw(exception.getClientDataInfo().getRaw()); } @@ -509,10 +511,12 @@ public ILocalAuthenticationResult authenticationResultFromBrokerResult(@NonNull ); // Deserialize ClientDataInfo from the broker result if available - final ClientDataInfo clientDataInfo = - ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw()); - if (clientDataInfo != null) { - localAuthResult.setClientDataInfo(clientDataInfo); + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + final ClientDataInfo clientDataInfo = + ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw()); + if (clientDataInfo != null) { + localAuthResult.setClientDataInfo(clientDataInfo); + } } return localAuthResult; @@ -552,7 +556,8 @@ public BaseException getBaseExceptionFromBundle(@NonNull final Bundle resultBund // Restore ClientDataInfo (server telemetry) from the broker result so callers // catching the exception can inspect server-side error context. - if (!StringUtil.isNullOrEmpty(brokerResult.getClientDataInfoRaw())) { + if (!StringUtil.isNullOrEmpty(brokerResult.getClientDataInfoRaw()) + && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { baseException.setClientDataInfo( ClientDataInfo.fromPipeDelimited(brokerResult.getClientDataInfoRaw()) ); diff --git a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java index 2a461be314..03aea0aee7 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java +++ b/common4j/src/main/com/microsoft/identity/common/java/controllers/BaseController.java @@ -58,6 +58,8 @@ import com.microsoft.identity.common.java.exception.ClientException; import com.microsoft.identity.common.java.exception.ErrorStrings; import com.microsoft.identity.common.java.exception.ServiceException; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.foci.FociQueryUtilities; import com.microsoft.identity.common.java.logging.DiagnosticContext; import com.microsoft.identity.common.java.logging.Logger; @@ -252,7 +254,9 @@ public AcquireTokenResult acquireTokenWithPassword(@NonNull final RopcTokenComma } // Set server client data info on the authentication result for IPC propagation - authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } // Set the AuthenticationResult on the final result object acquireTokenResult.setLocalAuthenticationResult(authenticationResult); @@ -532,7 +536,9 @@ protected void renewAccessToken(@NonNull final SilentTokenCommandParameters para } // Set server client data info on the authentication result for IPC propagation - authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + authenticationResult.setClientDataInfo(tokenResult.getClientDataInfo()); + } // Set the AuthenticationResult on the final result object acquireTokenSilentResult.setLocalAuthenticationResult(authenticationResult); diff --git a/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java b/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java index 40b46059eb..442065a911 100644 --- a/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java +++ b/common4j/src/main/com/microsoft/identity/common/java/controllers/ExceptionAdapter.java @@ -39,6 +39,8 @@ import com.microsoft.identity.common.java.exception.TerminalException; import com.microsoft.identity.common.java.exception.UiRequiredException; import com.microsoft.identity.common.java.exception.UserCancelException; +import com.microsoft.identity.common.java.flighting.CommonFlight; +import com.microsoft.identity.common.java.flighting.CommonFlightsManager; import com.microsoft.identity.common.java.logging.Logger; import com.microsoft.identity.common.java.net.HttpResponse; import com.microsoft.identity.common.java.opentelemetry.AttributeName; @@ -88,7 +90,8 @@ public static BaseException exceptionFromAcquireTokenResult(final AcquireTokenRe final BaseException authException = exceptionFromAuthorizationResult(authorizationResult, commandParameters); // Attach ClientDataInfo from the authorize redirect (clientdata query param) // so callers can inspect server-side error context on auth failures. - if (authorizationResult instanceof MicrosoftStsAuthorizationResult) { + if (authorizationResult instanceof MicrosoftStsAuthorizationResult + && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { authException.setClientDataInfo( ((MicrosoftStsAuthorizationResult) authorizationResult).getClientDataInfo() ); @@ -192,7 +195,9 @@ public static ServiceException exceptionFromTokenResult(final TokenResult tokenR outErr = getExceptionFromTokenErrorResponse(commandParameters, tokenResult.getErrorResponse()); applyCliTelemInfo(tokenResult.getCliTelemInfo(), outErr); - outErr.setClientDataInfo(tokenResult.getClientDataInfo()); + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + outErr.setClientDataInfo(tokenResult.getClientDataInfo()); + } } else { Logger.warn( TAG + methodName, From cf609651424aa0c203fb131097a7c5c120570d00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 08:28:15 +0000 Subject: [PATCH 12/14] Polish: fail-fast flight check order in MsalBrokerResultAdapter; null-guard in LocalMSALController else-if branch Agent-Logs-Url: https://github.com/AzureAD/microsoft-authentication-library-common-for-android/sessions/22fcd36c-3156-4450-b90e-9bf04390f62b Co-authored-by: fadidurah <88730756+fadidurah@users.noreply.github.com> --- .../common/internal/controllers/LocalMSALController.java | 8 +++++--- .../common/internal/result/MsalBrokerResultAdapter.java | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java index 426d76bdbf..b982859dd5 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java @@ -218,9 +218,11 @@ public AcquireTokenResult acquireToken( if (tokenResult != null && tokenResult.getClientDataInfo() != null) { localResult.setClientDataInfo(tokenResult.getClientDataInfo()); } else if (result instanceof MicrosoftStsAuthorizationResult) { - localResult.setClientDataInfo( - ((MicrosoftStsAuthorizationResult) result).getClientDataInfo() - ); + final ClientDataInfo authClientData = + ((MicrosoftStsAuthorizationResult) result).getClientDataInfo(); + if (authClientData != null) { + localResult.setClientDataInfo(authClientData); + } } } } diff --git a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java index b34a3df75a..37ba69afa4 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java @@ -288,8 +288,8 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth // Serialize ClientDataInfo as raw pipe-delimited string for IPC transfer. // The raw field is populated by ClientDataInfo.fromPipeDelimited(), the only // path that populates the parsed fields, so it is safe to ship as-is. - if (authenticationResult instanceof LocalAuthenticationResult - && CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY)) { + if (CommonFlightsManager.INSTANCE.getFlightsProvider().isFlightEnabled(CommonFlight.ENABLE_SERVER_CLIENT_DATA_TELEMETRY) + && authenticationResult instanceof LocalAuthenticationResult) { final ClientDataInfo clientDataInfo = ((LocalAuthenticationResult) authenticationResult).getClientDataInfo(); if (clientDataInfo != null) { From 843c6120c3543a974606f03d2f61ad2fe8af65a3 Mon Sep 17 00:00:00 2001 From: fadidurah Date: Fri, 15 May 2026 04:57:55 -0400 Subject: [PATCH 13/14] import --- .../common/internal/controllers/LocalMSALController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java index b982859dd5..d33b3dfedd 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java +++ b/common/src/main/java/com/microsoft/identity/common/internal/controllers/LocalMSALController.java @@ -57,6 +57,7 @@ import com.microsoft.identity.common.java.result.AcquireTokenResult; import com.microsoft.identity.common.java.result.GenerateShrResult; import com.microsoft.identity.common.java.result.LocalAuthenticationResult; +import com.microsoft.identity.common.java.telemetry.ClientDataInfo; import com.microsoft.identity.common.java.ui.PreferredAuthMethod; import com.microsoft.identity.common.java.util.ThreadUtils; import com.microsoft.identity.common.java.flighting.CommonFlight; From eff3ef848804a0da10f2c81079127bf8568a65e5 Mon Sep 17 00:00:00 2001 From: fadidurah Date: Fri, 15 May 2026 15:06:53 -0400 Subject: [PATCH 14/14] extra change removed --- gradle.properties | 1 - 1 file changed, 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f652101c39..ae84629666 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,6 @@ android.useAndroidX=true # https://office.visualstudio.com/Outlook%20Mobile/_wiki/wikis/Outlook-Mobile.wiki/3780/Android-Studio-Gradle-Performance-tips-and-tricks org.gradle.parallel=true org.gradle.daemon=true -org.gradle.configureondemand=true # See https://stackoverflow.com/questions/56075455/expiring-daemon-because-jvm-heap-space-is-exhausted # we must make sure that the total size is <7G, as that's the RAM size of VM on the build pipeline.