diff --git a/changelog.txt b/changelog.txt index a6936db3e5..c428e9633b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -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) 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..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(); } @@ -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()) @@ -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( 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/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..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 @@ -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 = 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 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; 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); +}