Skip to content

Commit 7a812bf

Browse files
authored
Merge branch 'dev' into fadi/publish-libraries-to-newandroid-feed
2 parents c8cd68b + 8638b3b commit 7a812bf

9 files changed

Lines changed: 439 additions & 33 deletions

File tree

changelog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
vNext
22
----------
3+
- [PATCH] Emit ipc_strategy telemetry attribute for successful device registration IPC strategy and refactor execute flow to pack protocol request once before strategy retries (#3124)
34
- [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)
45
- [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.
6+
- [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)
58
- [MINOR] Add provisionResourceAccountCredentials API to DeviceRegistrationClientApplication with V0 protocol params/response and add IPPhone to AppRegistry (#3086)
69
- [PATCH] Extend filter-then-clone optimization to deleteAccessTokensWithIntersectingScopes and add telemetry attributes (#3114)
710
- [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/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,18 @@
7979
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
8080
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge;
8181
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory;
82+
import com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder;
8283
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.PKeyAuthChallengeHandler;
8384
import com.microsoft.identity.common.java.WarningType;
8485
import com.microsoft.identity.common.java.exception.ClientException;
8586
import com.microsoft.identity.common.java.exception.ErrorStrings;
8687
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
88+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED;
89+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED;
90+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED;
91+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED;
92+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED;
93+
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED;
8794
import com.microsoft.identity.common.java.util.StringUtil;
8895
import com.microsoft.identity.common.logging.Logger;
8996

@@ -154,6 +161,16 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient {
154161

155162
private String mPasskeyRegistrationScript;
156163

164+
/**
165+
* Optional onboarding telemetry recorder. Set via {@link #setOnboardingTelemetryRecorder}
166+
* after this client is constructed (the recorder is created by the host fragment/activity
167+
* when a seed JSON arrives, which is typically later than WebView construction).
168+
* When non-null, key URL transitions (broker install, MDM enrollment, Company Portal
169+
* launch, etc.) and {@code lastLoadedDomain} are recorded for the onboarding telemetry blob.
170+
*/
171+
@Nullable
172+
private OnboardingTelemetryRecorder mOnboardingTelemetryRecorder;
173+
157174
/**
158175
* Callback for tracking URL load events.
159176
*/
@@ -201,11 +218,28 @@ public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final Str
201218
}
202219
}
203220

221+
/**
222+
* Attach an onboarding telemetry recorder so subsequent WebView page transitions
223+
* (broker install prompts, MDM enrollment redirects, Company Portal launches, etc.)
224+
* are recorded into the onboarding telemetry blob.
225+
*
226+
* Recorder is owned by the host fragment / activity (e.g. OneAuthNavigationFragment
227+
* on the OneAuth side, AuthorizationActivity on the broker side). May be null when
228+
* no seed JSON is available — in which case all hooks become no-ops.
229+
*/
230+
public void setOnboardingTelemetryRecorder(
231+
@Nullable final OnboardingTelemetryRecorder recorder) {
232+
mOnboardingTelemetryRecorder = recorder;
233+
}
234+
204235
@Override
205236
public void onPageFinished(final WebView view,
206237
final String url) {
207238
super.onPageFinished(view, url);
208239

240+
// Onboarding telemetry: record domain navigation (best-effort, no-op if no recorder).
241+
recordLastLoadedDomain(url);
242+
209243
if (mAuthUxJavaScriptInterfaceAdded) {
210244
// Add a function to the api. Must do this to first stringify the dict object, as Android @JavaScriptInterface does not support
211245
// passing dict objects through Javascript APIs, only Strings and primitive types. Server side will be sending message in a dict
@@ -722,6 +756,9 @@ private void processDeviceCaRequest(@NonNull final WebView view, @NonNull final
722756
final String methodTag = TAG + ":processDeviceCaRequest";
723757
Logger.info(methodTag, "This is a device CA request.");
724758

759+
// Onboarding telemetry: device CA blocking redirect → MDM enrollment phase.
760+
recordOnboardingStep(STEP_MDM_ENROLLMENT_STARTED);
761+
725762
if (shouldLaunchCompanyPortal()) {
726763
// If CP is installed, redirect to CP.
727764
// TODO: Until we get a signal from eSTS that CP is the MDM app, we cannot assume that.
@@ -831,6 +868,8 @@ private String getHomeTenantIdFromUrl(@NonNull final String url) {
831868
// This is a special case where the enrollment is not done in the WebView, but rather in the browser.
832869
private void processWebCpEnrollmentUrl(@NonNull final WebView view, @NonNull final String url) {
833870
final String methodTag = TAG + ":processWebCpEnrollmentUrl";
871+
// Onboarding telemetry: WebCP enrollment is a distinct enrollment path.
872+
recordOnboardingStep(STEP_WEB_CP_ENROLLMENT_STARTED);
834873
final Span span = createSpanWithAttributesFromParent(SpanName.ProcessWebCpEnrollmentRedirect.name());
835874
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
836875
view.stopLoading();
@@ -859,6 +898,8 @@ public void run() {
859898
// Opens the Google enrollment URL in the browser or the default intent handler (like DPC)
860899
private void openGoogleEnrollmentUrl(@NonNull final String url) {
861900
final String methodTag = TAG + ":openGoogleEnrollmentUrl";
901+
// Onboarding telemetry: Google enrollment redirect is a distinct enrollment path.
902+
recordOnboardingStep(STEP_GOOGLE_ENROLLMENT_STARTED);
862903
Logger.info(methodTag, "Opening Google enrollment URL");
863904
try {
864905
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@@ -883,6 +924,7 @@ private boolean processPlayStoreURL(@NonNull final WebView view, @NonNull final
883924
}
884925
final String appPackageName = getBrokerAppPackageNameFromUrl(url);
885926
Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'");
927+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
886928

887929
try {
888930
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_PREFIX + appPackageName));
@@ -902,6 +944,7 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N
902944

903945
final String appPackageName = getBrokerAppPackageNameFromUrl(url);
904946
Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'");
947+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
905948

906949
try {
907950
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(PLAY_STORE_INSTALL_APP_PREFIX + appPackageName));
@@ -920,6 +963,8 @@ private boolean processPlayStoreURLForBrokerApps(@NonNull final WebView view, @N
920963

921964
private void processAuthAppMFAUrl(String url) {
922965
final String methodTag = TAG + ":processAuthAppMFAUrl";
966+
// Onboarding telemetry: redirect to Authenticator for MFA linking.
967+
recordOnboardingStep(STEP_AUTHENTICATOR_MFA_LINKING_STARTED);
923968
Logger.verbose(methodTag, "Linking Account in Broker for MFA.");
924969
try {
925970
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
@@ -964,6 +1009,9 @@ void processAuthenticatorActivationAppLink(@NonNull final WebView view,
9641009
private void launchCompanyPortal() {
9651010
final String methodTag = TAG + ":launchCompanyPortal";
9661011

1012+
// Onboarding telemetry: Company Portal launch is a discrete onboarding step.
1013+
recordOnboardingStep(STEP_COMPANY_PORTAL_LAUNCHED);
1014+
9671015
Logger.verbose(methodTag, "Sending intent to launch the CompanyPortal.");
9681016
final Intent intent = new Intent();
9691017
intent.setComponent(new ComponentName(
@@ -1051,6 +1099,10 @@ private void processWebCpRequest(@NonNull final WebView view, @NonNull final Str
10511099
private void processInstallRequest(@NonNull final WebView view, @NonNull final String url) {
10521100
final String methodTag = TAG + ":processInstallRequest";
10531101

1102+
// Onboarding telemetry: broker install request reached the WebView client. Record the
1103+
// step at method entry so we capture intent regardless of the parsed result code.
1104+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
1105+
10541106
final RawAuthorizationResult result = RawAuthorizationResult.fromRedirectUri(url);
10551107

10561108
if (result.getResultCode() != RawAuthorizationResult.ResultCode.BROKER_INSTALLATION_TRIGGERED) {
@@ -1108,6 +1160,8 @@ private void processInvalidRedirectUri(@NonNull final WebView view,
11081160
*/
11091161
private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) {
11101162
final String methodTag = TAG + ":processIntentToInstallBrokerApp";
1163+
// Onboarding telemetry: alternate broker install path (intent-scheme).
1164+
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
11111165
try {
11121166
final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
11131167
if (intent != null && intent.getPackage() != null) {
@@ -1430,4 +1484,42 @@ private Span createSpanWithAttributesFromParent(@NonNull final String spanName)
14301484
public void addPasskeyRegistrationJsScript(@NonNull final String script) {
14311485
this.mPasskeyRegistrationScript = script;
14321486
}
1487+
1488+
/**
1489+
* Best-effort onboarding telemetry hook: records a step on the attached recorder
1490+
* if one is present. No-op when no recorder has been attached. Never throws.
1491+
*/
1492+
private void recordOnboardingStep(@NonNull final String stepId) {
1493+
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
1494+
if (recorder == null) {
1495+
return;
1496+
}
1497+
try {
1498+
recorder.addStep(stepId);
1499+
} catch (final Throwable t) {
1500+
Logger.warn(TAG, "Onboarding telemetry: failed to record step " + stepId + ": " + t.getMessage());
1501+
}
1502+
}
1503+
1504+
/**
1505+
* Best-effort onboarding telemetry hook: records the host of the most recently loaded
1506+
* page on the attached recorder. No-op when no recorder is attached or the URL has no
1507+
* extractable host. Never throws.
1508+
*/
1509+
private void recordLastLoadedDomain(@NonNull final String url) {
1510+
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
1511+
if (recorder == null || url.isEmpty()) {
1512+
return;
1513+
}
1514+
try {
1515+
final String host = Uri.parse(url).getHost();
1516+
if (host != null && !host.isEmpty()) {
1517+
recorder.setLastLoadedDomain(host);
1518+
} else {
1519+
Logger.verbose(TAG, "Onboarding telemetry: no host extracted from URL");
1520+
}
1521+
} catch (final Throwable t) {
1522+
Logger.warn(TAG, "Onboarding telemetry: failed to record last loaded domain: " + t.getMessage());
1523+
}
1524+
}
14331525
}

0 commit comments

Comments
 (0)