From 80aaba6d441861aa20e0e73e324651986439d803 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Mon, 18 May 2026 10:13:47 -0700 Subject: [PATCH 1/4] Propagate onboarding telemetry blob through broker failure path, Fixes AB#3462876 --- changelog.txt | 1 + .../result/MsalBrokerResultAdapter.java | 16 ++++++++++ .../request/MsalBrokerResultAdapterTests.kt | 30 +++++++++++++++++++ .../common/java/exception/BaseException.java | 26 ++++++++++++++++ 4 files changed, 73 insertions(+) diff --git a/changelog.txt b/changelog.txt index 6471f1c004..b7ee0a2331 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,6 @@ vNext ---------- +- [MINOR] Propagate the onboarding telemetry blob through the broker failure path: add BaseException.onboardingBlob and round-trip it through MsalBrokerResultAdapter so callers can emit onboarding telemetry on failure outcomes - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) 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 37ba69afa4..340c3f535f 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 @@ -431,6 +431,13 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception, builder.clientDataInfoRaw(exception.getClientDataInfo().getRaw()); } + // Serialize onboarding telemetry blob so it survives the broker IPC boundary on + // error paths — symmetric with the success path which carries the blob on + // BrokerResult. Telemetry-only — never affects auth logic. + if (!StringUtil.isNullOrEmpty(exception.getOnboardingBlob())) { + builder.onboardingBlob(exception.getOnboardingBlob()); + } + if (exception instanceof ServiceException) { final ServiceException serviceException = (ServiceException) exception; builder.subErrorCode(serviceException.getSubErrorCode()) @@ -563,6 +570,15 @@ public BaseException getBaseExceptionFromBundle(@NonNull final Bundle resultBund ); } + // Restore onboarding telemetry blob from the broker result so callers catching + // the exception (e.g., OneAuth) can include onboarding telemetry for failure + // outcomes — symmetric with the success path which attaches the blob to + // AcquireTokenResult. Telemetry-only — never affects auth logic. + final String onboardingBlob = getOnboardingBlobFromBundle(brokerResult); + if (!StringUtil.isNullOrEmpty(onboardingBlob)) { + baseException.setOnboardingBlob(onboardingBlob); + } + // Set broker app info if available if (resultBundle.containsKey(AuthenticationConstants.Broker.BROKER_VERSION)) { baseException.setBrokerAppVersion( 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 5031356269..eb3d95a006 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 @@ -766,4 +766,34 @@ class MsalBrokerResultAdapterTests { assertNull(deserialized.onboardingBlob) } + + @Test + fun testOnboardingBlob_RoundTripsThroughBaseExceptionBundle() { + val blobJson = """{"schema_version":"1.0.0","session_correlation_id":"abc-123","onboarding_mode":"brokered","blocking_errors":["BROKER_INSTALLATION_TRIGGERED"]}""" + val exception = ClientException("invalid_grant", "token failure") + exception.onboardingBlob = blobJson + + val resultAdapter = MsalBrokerResultAdapter() + val resultBundle = resultAdapter.bundleFromBaseException(exception, null) + val brokerResult = resultAdapter.brokerResultFromBundle(resultBundle) + assertEquals(blobJson, brokerResult.onboardingBlob) + + val received = resultAdapter.getBaseExceptionFromBundle(resultBundle) + assertEquals( + "Onboarding blob should be reconstructed on the exception", + blobJson, + received.onboardingBlob + ) + } + + @Test + fun testOnboardingBlob_NullOnException_NotInBundle() { + val exception = ClientException("invalid_grant", "token failure") + // No onboarding blob set + + val resultAdapter = MsalBrokerResultAdapter() + val resultBundle = resultAdapter.bundleFromBaseException(exception, null) + val received = resultAdapter.getBaseExceptionFromBundle(resultBundle) + assertNull(received.onboardingBlob) + } } \ No newline at end of file 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 2ec6b7d139..0a67ccc9bc 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 @@ -79,6 +79,15 @@ public class BaseException extends Exception implements IErrorInformation, ITele @Nullable private ClientDataInfo mClientDataInfo; + /** + * Onboarding telemetry blob (serialized JSON) carried through the brokered failure path. + * Populated by {@code MsalBrokerResultAdapter} when a {@code BrokerResult} on the failure + * path contains an onboarding blob, mirroring the success-path + * {@code AcquireTokenResult.setOnboardingBlob}. Telemetry-only — never affects auth logic. + */ + @Nullable + private String mOnboardingBlob; + private String mErrorCode; private String mSubErrorCode; @@ -231,6 +240,23 @@ public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) { this.mClientDataInfo = clientDataInfo; } + /** + * @return The onboarding telemetry blob (serialized JSON) attached on the broker + * failure path, or null if none was provided. + */ + @Nullable + public String getOnboardingBlob() { + return mOnboardingBlob; + } + + /** + * @param onboardingBlob The onboarding telemetry blob (serialized JSON) to attach + * on the broker failure path. + */ + public void setOnboardingBlob(@Nullable final String onboardingBlob) { + this.mOnboardingBlob = onboardingBlob; + } + @Nullable public String getCorrelationId() { return mCorrelationId; From edf8ab634d9f3b7994034fec30d5cba2ddd3d529 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Mon, 18 May 2026 10:26:43 -0700 Subject: [PATCH 2/4] MsalBrokerResultAdapter: add success-path onboarding blob overload --- changelog.txt | 2 +- .../result/MsalBrokerResultAdapter.java | 39 ++++++++++++++++++- .../request/MsalBrokerResultAdapterTests.kt | 27 +++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index b7ee0a2331..5a1c7dbf5e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ vNext ---------- -- [MINOR] Propagate the onboarding telemetry blob through the broker failure path: add BaseException.onboardingBlob and round-trip it through MsalBrokerResultAdapter so callers can emit onboarding telemetry on failure outcomes +- [MINOR] Propagate the onboarding telemetry blob through the broker failure path: add BaseException.onboardingBlob and round-trip it through MsalBrokerResultAdapter so callers can emit onboarding telemetry on failure outcomes. Also add an MsalBrokerResultAdapter overload that accepts an onboarding blob on the success path so the broker can attach the finalized blob to the success result bundle. - [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086) - [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114) - [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109) 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 340c3f535f..e90dac8dd0 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 @@ -152,11 +152,26 @@ public MsalBrokerResultAdapter(boolean shouldStopReturningRtWithAadResponse){ @Override public Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthenticationResult authenticationResult, @Nullable final String negotiatedBrokerProtocolVersion) { + return bundleFromAuthenticationResult(authenticationResult, null, negotiatedBrokerProtocolVersion); + } + + /** + * MSAL-only overload that attaches an onboarding telemetry blob (serialized JSON) to the + * success-path result bundle. The broker uses this to ship the finalized onboarding blob + * back to the client on a successful interactive token request. Symmetric with + * {@link #bundleFromBaseException} which carries the blob via {@link BaseException#getOnboardingBlob()}. + * + * @param onboardingBlob The finalized onboarding telemetry blob, or null if none. + */ + @NonNull + public Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthenticationResult authenticationResult, + @Nullable final String onboardingBlob, + @Nullable final String negotiatedBrokerProtocolVersion) { final String methodTag = TAG + ":bundleFromAuthenticationResult"; Logger.info(methodTag, "Constructing result bundle from ILocalAuthenticationResult"); final Bundle resultBundle = bundleFromBrokerResult( - buildBrokerResultFromAuthenticationResult(authenticationResult, negotiatedBrokerProtocolVersion), + buildBrokerResultFromAuthenticationResult(authenticationResult, onboardingBlob, negotiatedBrokerProtocolVersion), negotiatedBrokerProtocolVersion); resultBundle.putBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS, true); @@ -247,6 +262,22 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth public BrokerResult buildBrokerResultFromAuthenticationResult (@NonNull final ILocalAuthenticationResult authenticationResult, @Nullable final String negotiatedBrokerProtocolVersion){ + return buildBrokerResultFromAuthenticationResult(authenticationResult, null, negotiatedBrokerProtocolVersion); + } + + /** + * Overload that attaches a serialized onboarding telemetry blob to the resulting + * {@link BrokerResult}. Used by the broker to ship the finalized onboarding blob + * back to the client on a successful interactive token request. + * + * @param onboardingBlob The finalized onboarding telemetry blob, or null if none. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @NonNull + public BrokerResult buildBrokerResultFromAuthenticationResult + (@NonNull final ILocalAuthenticationResult authenticationResult, + @Nullable final String onboardingBlob, + @Nullable final String negotiatedBrokerProtocolVersion){ final IAccountRecord accountRecord = authenticationResult.getAccountRecord(); @@ -309,6 +340,12 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth .refreshTokenAge(authenticationResult.getRefreshTokenAge()); } + // Onboarding telemetry blob (success path) — carried back to the client adapter + // which attaches it to AcquireTokenResult. Telemetry-only, never affects auth. + if (!StringUtil.isNullOrEmpty(onboardingBlob)) { + brokerResultBuilder.onboardingBlob(onboardingBlob); + } + return brokerResultBuilder.build(); } 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 eb3d95a006..3a50049e82 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 @@ -796,4 +796,31 @@ class MsalBrokerResultAdapterTests { val received = resultAdapter.getBaseExceptionFromBundle(resultBundle) assertNull(received.onboardingBlob) } + + @Test + fun testOnboardingBlob_RoundTripsThroughAuthenticationResultBundle() { + val blobJson = """{"schema_version":"1.0.0","session_correlation_id":"abc-123","onboarding_mode":"brokered","steps_list":[{"step_id":"TokenIssued","ts":"2026-05-18T00:00:00.000Z"}]}""" + val cacheRecord = newCacheRecord() + val cacheRecords: MutableList = arrayListOf(cacheRecord) + val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false) + + val adapter = getInstance() + val resultBundle = adapter.bundleFromAuthenticationResult(authResult, blobJson, "16.0") + val deserialized = adapter.brokerResultFromBundle(resultBundle) + + assertEquals(blobJson, deserialized.onboardingBlob) + } + + @Test + fun testOnboardingBlob_NullOnAuthenticationResult_NotInBundle() { + val cacheRecord = newCacheRecord() + val cacheRecords: MutableList = arrayListOf(cacheRecord) + val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false) + + val adapter = getInstance() + val resultBundle = adapter.bundleFromAuthenticationResult(authResult, null, "16.0") + val deserialized = adapter.brokerResultFromBundle(resultBundle) + + assertNull(deserialized.onboardingBlob) + } } \ No newline at end of file From 91524dca8af69e931eafd757d6645cee0f20ae3a Mon Sep 17 00:00:00 2001 From: wzhipan Date: Mon, 18 May 2026 10:39:41 -0700 Subject: [PATCH 3/4] Add IOnboardingTelemetryRecorder interface for common4j-visible recorder access --- .../telemetry/OnboardingTelemetryRecorder.kt | 7 ++- .../IOnboardingTelemetryRecorder.java | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 common4j/src/main/com/microsoft/identity/common/java/telemetry/IOnboardingTelemetryRecorder.java diff --git a/common/src/main/java/com/microsoft/identity/common/internal/telemetry/OnboardingTelemetryRecorder.kt b/common/src/main/java/com/microsoft/identity/common/internal/telemetry/OnboardingTelemetryRecorder.kt index 4a2733ad0a..51089ba828 100644 --- a/common/src/main/java/com/microsoft/identity/common/internal/telemetry/OnboardingTelemetryRecorder.kt +++ b/common/src/main/java/com/microsoft/identity/common/internal/telemetry/OnboardingTelemetryRecorder.kt @@ -24,6 +24,7 @@ package com.microsoft.identity.common.internal.telemetry import android.content.Context +import com.microsoft.identity.common.java.telemetry.IOnboardingTelemetryRecorder import com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants import com.microsoft.identity.common.logging.Logger import org.json.JSONArray @@ -64,7 +65,7 @@ class OnboardingTelemetryRecorder( private val clientId: String, private val target: String, // sorted, space-joined scopes context: Context -) { +) : IOnboardingTelemetryRecorder { // Use applicationContext so this recorder, which may outlive the originating // Activity/Fragment, never holds a reference that would leak that context. @@ -119,7 +120,7 @@ class OnboardingTelemetryRecorder( * * @param stepId Step ID constant (from OnboardingTelemetryConstants) */ - fun addStep(stepId: String) { + override fun addStep(stepId: String) { val isoTimestamp = SimpleDateFormat(ISO_TIMESTAMP_FORMAT, Locale.US).format(Date()) stepsList.add(StepEntry(stepId, isoTimestamp)) } @@ -134,7 +135,7 @@ class OnboardingTelemetryRecorder( * or [OnboardingTelemetryConstants.BLOCKING_ERROR_MDM_FLOW]), * not a numeric service auth error code. */ - fun addBlockingError(errorCode: String) { + override fun addBlockingError(errorCode: String) { blockingErrors.add(errorCode) // Persist session correlation to SharedPreferences immediately on block diff --git a/common4j/src/main/com/microsoft/identity/common/java/telemetry/IOnboardingTelemetryRecorder.java b/common4j/src/main/com/microsoft/identity/common/java/telemetry/IOnboardingTelemetryRecorder.java new file mode 100644 index 0000000000..a31020fc67 --- /dev/null +++ b/common4j/src/main/com/microsoft/identity/common/java/telemetry/IOnboardingTelemetryRecorder.java @@ -0,0 +1,63 @@ +// 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.telemetry; + +import lombok.NonNull; + +/** + * Common4j-visible facet of the onboarding telemetry recorder. + * + *

The concrete recorder (Kotlin {@code OnboardingTelemetryRecorder} in the {@code common} + * Android module) depends on Android {@code Context} for SharedPreferences-backed session + * correlation persistence, which makes it unavailable to pure-Java modules like + * {@code broker4j}. This interface exposes only the recording surface — {@link #addStep} + * and {@link #addBlockingError} — so broker4j code (e.g. interactive error handlers, + * controllers) can populate the recorder without taking an Android dependency. + * + *

The owning Android-side caller (e.g. {@code AccountChooserActivity}) constructs the + * concrete recorder from the seed JSON, passes the {@code IOnboardingTelemetryRecorder} + * view down through broker4j call sites, and calls {@code finalizeBlob()} on the concrete + * recorder once the flow completes. + */ +public interface IOnboardingTelemetryRecorder { + + /** + * Record a step in the onboarding flow. The implementation captures a timestamp + * for each step internally. + * + * @param stepId Step ID constant from + * {@link com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants} + * (e.g. {@code STEP_AUTHENTICATION_STARTED}). + */ + void addStep(@NonNull String stepId); + + /** + * Record a blocking onboarding error detected during the flow. + * + * @param errorCode Blocking-error constant from + * {@link com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants} + * (e.g. {@code BLOCKING_ERROR_DEVICE_REGISTRATION_NEEDED}). Not a numeric + * service auth error code. + */ + void addBlockingError(@NonNull String errorCode); +} From 5df2fe0f4a75c6fa50442f2739c1490458b0fac3 Mon Sep 17 00:00:00 2001 From: wzhipan Date: Thu, 21 May 2026 16:57:24 -0700 Subject: [PATCH 4/4] Re-trigger CI (previous run had infra-only failures: flaky coroutine test + corrupted Robolectric JAR cache + equal-coverage compare bug)