Skip to content
Merged
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ vNext
- [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)
- [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.
- [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)
- [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)
- [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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -431,6 +468,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())
Expand Down Expand Up @@ -563,6 +607,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,4 +766,61 @@ 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)
}

@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<ICacheRecord> = 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<ICacheRecord> = 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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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);
Comment thread
wzhipan marked this conversation as resolved.

/**
* 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);
}
Loading