Skip to content

Commit 8638b3b

Browse files
authored
Propagate onboarding telemetry blob through broker success and failure paths, Fixes AB#3462876 (#3123)
Makes `MsalBrokerResultAdapter` carry the onboarding telemetry blob symmetrically across **every** broker outcome (success, failure, cancel), closing the failure-path gap Veena flagged on #3111 and unblocking the broker recorder-lifecycle work. **What this adds:** ### Failure path (was the original A3 scope) 1. `BaseException.onboardingBlob` field with getter/setter (common4j). 2. `MsalBrokerResultAdapter.bundleFromBaseException` now serializes `exception.onboardingBlob` into the result bundle (symmetric with the existing `ClientDataInfo` serialization right above it). 3. `MsalBrokerResultAdapter.getBaseExceptionFromBundle` extracts the blob from the deserialized `BrokerResult` and attaches it to the reconstructed exception via `setOnboardingBlob`. ### Success path (added in second commit) 4. MSAL-only overloads on `MsalBrokerResultAdapter`: - `bundleFromAuthenticationResult(result, onboardingBlob, version)` - `buildBrokerResultFromAuthenticationResult(result, onboardingBlob, version)` The broker calls these from `AccountChooserActivity.returnSuccessToCallingActivity` (upcoming PR) to ship the finalized recorder blob to the client. Existing overloads are preserved and delegate to the new ones with `null`, so no behavior change for existing callers (including `AdalBrokerResultAdapter` which doesn't need this). After this, OneAuth / Common can emit onboarding telemetry on the brokered **success**, **failure**, and **cancel** outcomes via: - `acquireTokenResult.getOnboardingBlob()` (success — already wired in #3111) - `exception.getOnboardingBlob()` (failure/cancel — new) **Why now:** Required by upcoming broker recorder-lifecycle work in PR #163 follow-ups, which writes the recorder blob on every termination path. **Tests:** `MsalBrokerResultAdapterTests` adds 4 new round-trip tests (2 failure-path, 2 success-path). All 6 `testOnboardingBlob_*` tests pass locally: `./gradlew :common:testLocalDebugUnitTest --tests *MsalBrokerResultAdapterTests*testOnboardingBlob*` **Safety:** Telemetry-only; never affects auth logic. No flight gate (blob is only present if a prior PR seeded it). Fixes [AB#3462876](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3462876)
1 parent cf1f134 commit 8638b3b

6 files changed

Lines changed: 205 additions & 4 deletions

File tree

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ vNext
44
- [PATCH] Fix Edge browser selection on devices where Microsoft Edge is the default browser: add the rotated Edge signing certificate hash to the Edge BrowserDescriptor and accept multi-signer browsers when any signature intersects the safelist, instead of requiring strict set-equality (resolves MSAL #2414)
55
- [MINOR] Refactor Auth Tab integration to use provider-based strategy selection. Adds AuthTabStrategyProvider and BrowserLaunchStrategy with Custom Tabs fallback. Compatible with androidx.browser:browser:1.7.0.
66
- [MINOR] Wire onboarding telemetry hooks into AzureActiveDirectoryWebViewClient for page-transition step capture (broker install, MDM enrollment, Company Portal launch, MFA linking) and last-loaded-domain tracking (#3121)
7+
- [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 (#3123)
78
- [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086)
89
- [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114)
910
- [PATCH] Wire ClientDataInfo through AcquireTokenResult, exceptions (#3109)

common/src/main/java/com/microsoft/identity/common/internal/result/MsalBrokerResultAdapter.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,26 @@ public MsalBrokerResultAdapter(boolean shouldStopReturningRtWithAadResponse){
152152
@Override
153153
public Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthenticationResult authenticationResult,
154154
@Nullable final String negotiatedBrokerProtocolVersion) {
155+
return bundleFromAuthenticationResult(authenticationResult, null, negotiatedBrokerProtocolVersion);
156+
}
157+
158+
/**
159+
* MSAL-only overload that attaches an onboarding telemetry blob (serialized JSON) to the
160+
* success-path result bundle. The broker uses this to ship the finalized onboarding blob
161+
* back to the client on a successful interactive token request. Symmetric with
162+
* {@link #bundleFromBaseException} which carries the blob via {@link BaseException#getOnboardingBlob()}.
163+
*
164+
* @param onboardingBlob The finalized onboarding telemetry blob, or null if none.
165+
*/
166+
@NonNull
167+
public Bundle bundleFromAuthenticationResult(@NonNull final ILocalAuthenticationResult authenticationResult,
168+
@Nullable final String onboardingBlob,
169+
@Nullable final String negotiatedBrokerProtocolVersion) {
155170
final String methodTag = TAG + ":bundleFromAuthenticationResult";
156171
Logger.info(methodTag, "Constructing result bundle from ILocalAuthenticationResult");
157172

158173
final Bundle resultBundle = bundleFromBrokerResult(
159-
buildBrokerResultFromAuthenticationResult(authenticationResult, negotiatedBrokerProtocolVersion),
174+
buildBrokerResultFromAuthenticationResult(authenticationResult, onboardingBlob, negotiatedBrokerProtocolVersion),
160175
negotiatedBrokerProtocolVersion);
161176
resultBundle.putBoolean(AuthenticationConstants.Broker.BROKER_REQUEST_V2_SUCCESS, true);
162177

@@ -247,6 +262,22 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth
247262
public BrokerResult buildBrokerResultFromAuthenticationResult
248263
(@NonNull final ILocalAuthenticationResult authenticationResult,
249264
@Nullable final String negotiatedBrokerProtocolVersion){
265+
return buildBrokerResultFromAuthenticationResult(authenticationResult, null, negotiatedBrokerProtocolVersion);
266+
}
267+
268+
/**
269+
* Overload that attaches a serialized onboarding telemetry blob to the resulting
270+
* {@link BrokerResult}. Used by the broker to ship the finalized onboarding blob
271+
* back to the client on a successful interactive token request.
272+
*
273+
* @param onboardingBlob The finalized onboarding telemetry blob, or null if none.
274+
*/
275+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
276+
@NonNull
277+
public BrokerResult buildBrokerResultFromAuthenticationResult
278+
(@NonNull final ILocalAuthenticationResult authenticationResult,
279+
@Nullable final String onboardingBlob,
280+
@Nullable final String negotiatedBrokerProtocolVersion){
250281

251282
final IAccountRecord accountRecord = authenticationResult.getAccountRecord();
252283

@@ -309,6 +340,12 @@ public Bundle bundleFromAuthenticationResultForWebApps(@NonNull final ILocalAuth
309340
.refreshTokenAge(authenticationResult.getRefreshTokenAge());
310341
}
311342

343+
// Onboarding telemetry blob (success path) — carried back to the client adapter
344+
// which attaches it to AcquireTokenResult. Telemetry-only, never affects auth.
345+
if (!StringUtil.isNullOrEmpty(onboardingBlob)) {
346+
brokerResultBuilder.onboardingBlob(onboardingBlob);
347+
}
348+
312349
return brokerResultBuilder.build();
313350
}
314351

@@ -431,6 +468,13 @@ public Bundle bundleFromBaseException(@NonNull final BaseException exception,
431468
builder.clientDataInfoRaw(exception.getClientDataInfo().getRaw());
432469
}
433470

471+
// Serialize onboarding telemetry blob so it survives the broker IPC boundary on
472+
// error paths — symmetric with the success path which carries the blob on
473+
// BrokerResult. Telemetry-only — never affects auth logic.
474+
if (!StringUtil.isNullOrEmpty(exception.getOnboardingBlob())) {
475+
builder.onboardingBlob(exception.getOnboardingBlob());
476+
}
477+
434478
if (exception instanceof ServiceException) {
435479
final ServiceException serviceException = (ServiceException) exception;
436480
builder.subErrorCode(serviceException.getSubErrorCode())
@@ -563,6 +607,15 @@ public BaseException getBaseExceptionFromBundle(@NonNull final Bundle resultBund
563607
);
564608
}
565609

610+
// Restore onboarding telemetry blob from the broker result so callers catching
611+
// the exception (e.g., OneAuth) can include onboarding telemetry for failure
612+
// outcomes — symmetric with the success path which attaches the blob to
613+
// AcquireTokenResult. Telemetry-only — never affects auth logic.
614+
final String onboardingBlob = getOnboardingBlobFromBundle(brokerResult);
615+
if (!StringUtil.isNullOrEmpty(onboardingBlob)) {
616+
baseException.setOnboardingBlob(onboardingBlob);
617+
}
618+
566619
// Set broker app info if available
567620
if (resultBundle.containsKey(AuthenticationConstants.Broker.BROKER_VERSION)) {
568621
baseException.setBrokerAppVersion(

common/src/main/java/com/microsoft/identity/common/internal/telemetry/OnboardingTelemetryRecorder.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package com.microsoft.identity.common.internal.telemetry
2525

2626
import android.content.Context
27+
import com.microsoft.identity.common.java.telemetry.IOnboardingTelemetryRecorder
2728
import com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants
2829
import com.microsoft.identity.common.logging.Logger
2930
import org.json.JSONArray
@@ -64,7 +65,7 @@ class OnboardingTelemetryRecorder(
6465
private val clientId: String,
6566
private val target: String, // sorted, space-joined scopes
6667
context: Context
67-
) {
68+
) : IOnboardingTelemetryRecorder {
6869

6970
// Use applicationContext so this recorder, which may outlive the originating
7071
// Activity/Fragment, never holds a reference that would leak that context.
@@ -119,7 +120,7 @@ class OnboardingTelemetryRecorder(
119120
*
120121
* @param stepId Step ID constant (from OnboardingTelemetryConstants)
121122
*/
122-
fun addStep(stepId: String) {
123+
override fun addStep(stepId: String) {
123124
val isoTimestamp = SimpleDateFormat(ISO_TIMESTAMP_FORMAT, Locale.US).format(Date())
124125
stepsList.add(StepEntry(stepId, isoTimestamp))
125126
}
@@ -134,7 +135,7 @@ class OnboardingTelemetryRecorder(
134135
* or [OnboardingTelemetryConstants.BLOCKING_ERROR_MDM_FLOW]),
135136
* not a numeric service auth error code.
136137
*/
137-
fun addBlockingError(errorCode: String) {
138+
override fun addBlockingError(errorCode: String) {
138139
blockingErrors.add(errorCode)
139140

140141
// Persist session correlation to SharedPreferences immediately on block

common/src/test/java/com/microsoft/identity/common/internal/request/MsalBrokerResultAdapterTests.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,4 +766,61 @@ class MsalBrokerResultAdapterTests {
766766

767767
assertNull(deserialized.onboardingBlob)
768768
}
769+
770+
@Test
771+
fun testOnboardingBlob_RoundTripsThroughBaseExceptionBundle() {
772+
val blobJson = """{"schema_version":"1.0.0","session_correlation_id":"abc-123","onboarding_mode":"brokered","blocking_errors":["BROKER_INSTALLATION_TRIGGERED"]}"""
773+
val exception = ClientException("invalid_grant", "token failure")
774+
exception.onboardingBlob = blobJson
775+
776+
val resultAdapter = MsalBrokerResultAdapter()
777+
val resultBundle = resultAdapter.bundleFromBaseException(exception, null)
778+
val brokerResult = resultAdapter.brokerResultFromBundle(resultBundle)
779+
assertEquals(blobJson, brokerResult.onboardingBlob)
780+
781+
val received = resultAdapter.getBaseExceptionFromBundle(resultBundle)
782+
assertEquals(
783+
"Onboarding blob should be reconstructed on the exception",
784+
blobJson,
785+
received.onboardingBlob
786+
)
787+
}
788+
789+
@Test
790+
fun testOnboardingBlob_NullOnException_NotInBundle() {
791+
val exception = ClientException("invalid_grant", "token failure")
792+
// No onboarding blob set
793+
794+
val resultAdapter = MsalBrokerResultAdapter()
795+
val resultBundle = resultAdapter.bundleFromBaseException(exception, null)
796+
val received = resultAdapter.getBaseExceptionFromBundle(resultBundle)
797+
assertNull(received.onboardingBlob)
798+
}
799+
800+
@Test
801+
fun testOnboardingBlob_RoundTripsThroughAuthenticationResultBundle() {
802+
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"}]}"""
803+
val cacheRecord = newCacheRecord()
804+
val cacheRecords: MutableList<ICacheRecord> = arrayListOf(cacheRecord)
805+
val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false)
806+
807+
val adapter = getInstance()
808+
val resultBundle = adapter.bundleFromAuthenticationResult(authResult, blobJson, "16.0")
809+
val deserialized = adapter.brokerResultFromBundle(resultBundle)
810+
811+
assertEquals(blobJson, deserialized.onboardingBlob)
812+
}
813+
814+
@Test
815+
fun testOnboardingBlob_NullOnAuthenticationResult_NotInBundle() {
816+
val cacheRecord = newCacheRecord()
817+
val cacheRecords: MutableList<ICacheRecord> = arrayListOf(cacheRecord)
818+
val authResult = LocalAuthenticationResult(cacheRecord, cacheRecords, SdkType.MSAL, false)
819+
820+
val adapter = getInstance()
821+
val resultBundle = adapter.bundleFromAuthenticationResult(authResult, null, "16.0")
822+
val deserialized = adapter.brokerResultFromBundle(resultBundle)
823+
824+
assertNull(deserialized.onboardingBlob)
825+
}
769826
}

common4j/src/main/com/microsoft/identity/common/java/exception/BaseException.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ public class BaseException extends Exception implements IErrorInformation, ITele
7979
@Nullable
8080
private ClientDataInfo mClientDataInfo;
8181

82+
/**
83+
* Onboarding telemetry blob (serialized JSON) carried through the brokered failure path.
84+
* Populated by {@code MsalBrokerResultAdapter} when a {@code BrokerResult} on the failure
85+
* path contains an onboarding blob, mirroring the success-path
86+
* {@code AcquireTokenResult.setOnboardingBlob}. Telemetry-only — never affects auth logic.
87+
*/
88+
@Nullable
89+
private String mOnboardingBlob;
90+
8291
private String mErrorCode;
8392

8493
private String mSubErrorCode;
@@ -231,6 +240,23 @@ public void setClientDataInfo(@Nullable final ClientDataInfo clientDataInfo) {
231240
this.mClientDataInfo = clientDataInfo;
232241
}
233242

243+
/**
244+
* @return The onboarding telemetry blob (serialized JSON) attached on the broker
245+
* failure path, or null if none was provided.
246+
*/
247+
@Nullable
248+
public String getOnboardingBlob() {
249+
return mOnboardingBlob;
250+
}
251+
252+
/**
253+
* @param onboardingBlob The onboarding telemetry blob (serialized JSON) to attach
254+
* on the broker failure path.
255+
*/
256+
public void setOnboardingBlob(@Nullable final String onboardingBlob) {
257+
this.mOnboardingBlob = onboardingBlob;
258+
}
259+
234260
@Nullable
235261
public String getCorrelationId() {
236262
return mCorrelationId;
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// All rights reserved.
3+
//
4+
// This code is licensed under the MIT License.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files(the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions :
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
package com.microsoft.identity.common.java.telemetry;
24+
25+
import lombok.NonNull;
26+
27+
/**
28+
* Common4j-visible facet of the onboarding telemetry recorder.
29+
*
30+
* <p>The concrete recorder (Kotlin {@code OnboardingTelemetryRecorder} in the {@code common}
31+
* Android module) depends on Android {@code Context} for SharedPreferences-backed session
32+
* correlation persistence, which makes it unavailable to pure-Java modules like
33+
* {@code broker4j}. This interface exposes only the recording surface — {@link #addStep}
34+
* and {@link #addBlockingError} — so broker4j code (e.g. interactive error handlers,
35+
* controllers) can populate the recorder without taking an Android dependency.
36+
*
37+
* <p>The owning Android-side caller (e.g. {@code AccountChooserActivity}) constructs the
38+
* concrete recorder from the seed JSON, passes the {@code IOnboardingTelemetryRecorder}
39+
* view down through broker4j call sites, and calls {@code finalizeBlob()} on the concrete
40+
* recorder once the flow completes.
41+
*/
42+
public interface IOnboardingTelemetryRecorder {
43+
44+
/**
45+
* Record a step in the onboarding flow. The implementation captures a timestamp
46+
* for each step internally.
47+
*
48+
* @param stepId Step ID constant from
49+
* {@link com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants}
50+
* (e.g. {@code STEP_AUTHENTICATION_STARTED}).
51+
*/
52+
void addStep(@NonNull String stepId);
53+
54+
/**
55+
* Record a blocking onboarding error detected during the flow.
56+
*
57+
* @param errorCode Blocking-error constant from
58+
* {@link com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants}
59+
* (e.g. {@code BLOCKING_ERROR_DEVICE_REGISTRATION_NEEDED}). Not a numeric
60+
* service auth error code.
61+
*/
62+
void addBlockingError(@NonNull String errorCode);
63+
}

0 commit comments

Comments
 (0)