Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ vNext
- [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)
- [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] 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 @@ -79,11 +79,18 @@
import com.microsoft.identity.common.java.ui.webview.authorization.IAuthorizationCompletionCallback;
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallenge;
import com.microsoft.identity.common.java.challengehandlers.PKeyAuthChallengeFactory;
import com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder;
import com.microsoft.identity.common.internal.ui.webview.challengehandlers.PKeyAuthChallengeHandler;
import com.microsoft.identity.common.java.WarningType;
import com.microsoft.identity.common.java.exception.ClientException;
import com.microsoft.identity.common.java.exception.ErrorStrings;
import com.microsoft.identity.common.java.providers.RawAuthorizationResult;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_AUTHENTICATOR_MFA_LINKING_STARTED;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_BROKER_INSTALL_PROMPTED;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_COMPANY_PORTAL_LAUNCHED;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_GOOGLE_ENROLLMENT_STARTED;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_MDM_ENROLLMENT_STARTED;
import static com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.STEP_WEB_CP_ENROLLMENT_STARTED;
import com.microsoft.identity.common.java.util.StringUtil;
import com.microsoft.identity.common.logging.Logger;

Expand Down Expand Up @@ -154,6 +161,16 @@ public class AzureActiveDirectoryWebViewClient extends OAuth2WebViewClient {

private String mPasskeyRegistrationScript;

/**
* Optional onboarding telemetry recorder. Set via {@link #setOnboardingTelemetryRecorder}
* after this client is constructed (the recorder is created by the host fragment/activity
* when a seed JSON arrives, which is typically later than WebView construction).
* When non-null, key URL transitions (broker install, MDM enrollment, Company Portal
* launch, etc.) and {@code lastLoadedDomain} are recorded for the onboarding telemetry blob.
*/
@Nullable
private OnboardingTelemetryRecorder mOnboardingTelemetryRecorder;

/**
* Callback for tracking URL load events.
*/
Expand Down Expand Up @@ -201,11 +218,28 @@ public void initializeAuthUxJavaScriptApi(@NonNull final WebView view, final Str
}
}

/**
* Attach an onboarding telemetry recorder so subsequent WebView page transitions
* (broker install prompts, MDM enrollment redirects, Company Portal launches, etc.)
* are recorded into the onboarding telemetry blob.
*
* Recorder is owned by the host fragment / activity (e.g. OneAuthNavigationFragment
* on the OneAuth side, AuthorizationActivity on the broker side). May be null when
* no seed JSON is available — in which case all hooks become no-ops.
*/
public void setOnboardingTelemetryRecorder(
@Nullable final OnboardingTelemetryRecorder recorder) {
mOnboardingTelemetryRecorder = recorder;
}

@Override
public void onPageFinished(final WebView view,
final String url) {
super.onPageFinished(view, url);

// Onboarding telemetry: record domain navigation (best-effort, no-op if no recorder).
recordLastLoadedDomain(url);

if (mAuthUxJavaScriptInterfaceAdded) {
// Add a function to the api. Must do this to first stringify the dict object, as Android @JavaScriptInterface does not support
// passing dict objects through Javascript APIs, only Strings and primitive types. Server side will be sending message in a dict
Expand Down Expand Up @@ -722,6 +756,9 @@ private void processDeviceCaRequest(@NonNull final WebView view, @NonNull final
final String methodTag = TAG + ":processDeviceCaRequest";
Logger.info(methodTag, "This is a device CA request.");

// Onboarding telemetry: device CA blocking redirect → MDM enrollment phase.
recordOnboardingStep(STEP_MDM_ENROLLMENT_STARTED);

if (shouldLaunchCompanyPortal()) {
// If CP is installed, redirect to CP.
// TODO: Until we get a signal from eSTS that CP is the MDM app, we cannot assume that.
Expand Down Expand Up @@ -831,6 +868,8 @@ private String getHomeTenantIdFromUrl(@NonNull final String url) {
// This is a special case where the enrollment is not done in the WebView, but rather in the browser.
private void processWebCpEnrollmentUrl(@NonNull final WebView view, @NonNull final String url) {
final String methodTag = TAG + ":processWebCpEnrollmentUrl";
// Onboarding telemetry: WebCP enrollment is a distinct enrollment path.
recordOnboardingStep(STEP_WEB_CP_ENROLLMENT_STARTED);
final Span span = createSpanWithAttributesFromParent(SpanName.ProcessWebCpEnrollmentRedirect.name());
try (final Scope scope = SpanExtension.makeCurrentSpan(span)) {
view.stopLoading();
Expand Down Expand Up @@ -859,6 +898,8 @@ public void run() {
// Opens the Google enrollment URL in the browser or the default intent handler (like DPC)
private void openGoogleEnrollmentUrl(@NonNull final String url) {
final String methodTag = TAG + ":openGoogleEnrollmentUrl";
// Onboarding telemetry: Google enrollment redirect is a distinct enrollment path.
recordOnboardingStep(STEP_GOOGLE_ENROLLMENT_STARTED);
Logger.info(methodTag, "Opening Google enrollment URL");
try {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
Expand All @@ -883,6 +924,7 @@ private boolean processPlayStoreURL(@NonNull final WebView view, @NonNull final
}
final String appPackageName = getBrokerAppPackageNameFromUrl(url);
Logger.info(methodTag, "Request to open PlayStore to install package : '" + appPackageName + "'");
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);

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

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

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

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

// Onboarding telemetry: Company Portal launch is a discrete onboarding step.
recordOnboardingStep(STEP_COMPANY_PORTAL_LAUNCHED);

Logger.verbose(methodTag, "Sending intent to launch the CompanyPortal.");
final Intent intent = new Intent();
intent.setComponent(new ComponentName(
Expand Down Expand Up @@ -1051,6 +1099,10 @@ private void processWebCpRequest(@NonNull final WebView view, @NonNull final Str
private void processInstallRequest(@NonNull final WebView view, @NonNull final String url) {
final String methodTag = TAG + ":processInstallRequest";

// Onboarding telemetry: broker install request reached the WebView client. Record the
// step at method entry so we capture intent regardless of the parsed result code.
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);

Comment thread
wzhipan marked this conversation as resolved.
final RawAuthorizationResult result = RawAuthorizationResult.fromRedirectUri(url);

if (result.getResultCode() != RawAuthorizationResult.ResultCode.BROKER_INSTALLATION_TRIGGERED) {
Expand Down Expand Up @@ -1108,6 +1160,8 @@ private void processInvalidRedirectUri(@NonNull final WebView view,
*/
private void processIntentToInstallBrokerApp(@NonNull final WebView view, @NonNull final String intentUrl) {
final String methodTag = TAG + ":processIntentToInstallBrokerApp";
// Onboarding telemetry: alternate broker install path (intent-scheme).
recordOnboardingStep(STEP_BROKER_INSTALL_PROMPTED);
try {
final Intent intent = Intent.parseUri(intentUrl, Intent.URI_INTENT_SCHEME);
if (intent != null && intent.getPackage() != null) {
Expand Down Expand Up @@ -1430,4 +1484,42 @@ private Span createSpanWithAttributesFromParent(@NonNull final String spanName)
public void addPasskeyRegistrationJsScript(@NonNull final String script) {
this.mPasskeyRegistrationScript = script;
}

/**
* Best-effort onboarding telemetry hook: records a step on the attached recorder
* if one is present. No-op when no recorder has been attached. Never throws.
*/
private void recordOnboardingStep(@NonNull final String stepId) {
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
if (recorder == null) {
return;
}
try {
recorder.addStep(stepId);
} catch (final Throwable t) {
Logger.warn(TAG, "Onboarding telemetry: failed to record step " + stepId + ": " + t.getMessage());
}
}

/**
* Best-effort onboarding telemetry hook: records the host of the most recently loaded
* page on the attached recorder. No-op when no recorder is attached or the URL has no
* extractable host. Never throws.
*/
private void recordLastLoadedDomain(@NonNull final String url) {
final OnboardingTelemetryRecorder recorder = mOnboardingTelemetryRecorder;
if (recorder == null || url.isEmpty()) {
return;
}
try {
final String host = Uri.parse(url).getHost();
if (host != null && !host.isEmpty()) {
recorder.setLastLoadedDomain(host);
} else {
Logger.verbose(TAG, "Onboarding telemetry: no host extracted from URL");
}
} catch (final Throwable t) {
Logger.warn(TAG, "Onboarding telemetry: failed to record last loaded domain: " + t.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ public void onPageLoaded(final String url) {
@After
public void cleanUp(){
CommonFlightsManager.INSTANCE.resetFlightsManager();
// Clear onboarding session-correlation SharedPreferences to keep tests isolated;
// OnboardingTelemetryRecorder.addBlockingError persists to this store.
if (mContext != null) {
new com.microsoft.identity.common.internal.telemetry.OnboardingSessionCorrelationStore(mContext)
.save("");
}
}

@Test(expected = IllegalArgumentException.class)
Expand Down Expand Up @@ -1095,4 +1101,101 @@ public void testProcessAuthAppMFAUrl_startsViewIntentWithNewTaskFlag() {
assertTrue("MFA activation intent must carry FLAG_ACTIVITY_NEW_TASK",
(launched.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0);
}

// -----------------------------------------------------------------------
// Onboarding telemetry hooks
// -----------------------------------------------------------------------

/**
* Verifies that when an OnboardingTelemetryRecorder is attached to the WebView client,
* a broker install request URL produces a populated blob containing the
* {@code BrokerInstallPrompted} step. We construct a recorder with a synthetic seed
* containing a session correlation id and a blocking error so {@code finalizeBlob}
* returns non-empty.
*/
@Test
public void testProcessInstallRequest_RecordsBrokerInstallPromptedStep() throws Exception {
final String seedJson = "{\"schema_version\":\"1.0.0\","
+ "\"session_correlation_id\":\"abc-123\","
+ "\"onboarding_mode\":\"non-brokered\"}";
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
seedJson, "client-id", "scope1", mContext);
// Record a blocking error so finalizeBlob() emits a populated blob.
recorder.addBlockingError(
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);

mWebViewClient.setOnboardingTelemetryRecorder(recorder);

// Trigger a broker install URL through the WebView client (delegates to processInstallRequest).
mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL);

final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
final org.json.JSONArray steps = blob.getJSONArray("steps_list");
boolean foundStep = false;
for (int i = 0; i < steps.length(); i++) {
if (com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants
.STEP_BROKER_INSTALL_PROMPTED.equals(steps.getJSONObject(i).getString("step_id"))) {
foundStep = true;
break;
}
}
assertTrue("Expected BrokerInstallPrompted step in onboarding blob", foundStep);
Comment thread
wzhipan marked this conversation as resolved.
}

/**
* No recorder attached → no crash, hook is a no-op.
*/
@Test
public void testProcessInstallRequest_NoRecorder_IsNoOp() {
// Default mWebViewClient has no recorder attached. This must not throw.
assertTrue(mWebViewClient.shouldOverrideUrlLoading(mMockWebView, TEST_INSTALL_REQUEST_URL));
}

/**
* Verifies that {@code onPageFinished} extracts the host from a real URL and stores it on
* the recorder as {@code lastLoadedDomain}. Requires a blocking error to have been recorded
* so the finalized blob is non-empty.
*/
@Test
public void testOnPageFinished_RecordsLastLoadedDomain() throws Exception {
final String seedJson = "{\"schema_version\":\"1.0.0\","
+ "\"session_correlation_id\":\"abc-123\","
+ "\"onboarding_mode\":\"non-brokered\"}";
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
seedJson, "client-id", "scope1", mContext);
recorder.addBlockingError(
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);

mWebViewClient.setOnboardingTelemetryRecorder(recorder);
mWebViewClient.onPageFinished(mMockWebView, "https://login.microsoftonline.com/common/oauth2/authorize");

final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
assertEquals("login.microsoftonline.com", blob.getString(
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.LAST_LOADED_DOMAIN));
}
Comment thread
wzhipan marked this conversation as resolved.

/**
* onPageFinished with a URL that has no host (e.g. about:blank) does not throw and
* does not set lastLoadedDomain.
*/
@Test
public void testOnPageFinished_BlankUrl_DoesNotSetDomain() throws Exception {
final String seedJson = "{\"schema_version\":\"1.0.0\","
+ "\"session_correlation_id\":\"abc-123\","
+ "\"onboarding_mode\":\"non-brokered\"}";
final com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder recorder =
new com.microsoft.identity.common.internal.telemetry.OnboardingTelemetryRecorder(
seedJson, "client-id", "scope1", mContext);
recorder.addBlockingError(
com.microsoft.identity.common.java.telemetry.OnboardingTelemetryConstants.BLOCKING_ERROR_BROKER_INSTALL);

mWebViewClient.setOnboardingTelemetryRecorder(recorder);
mWebViewClient.onPageFinished(mMockWebView, TEST_BLANK_PAGE_REQUEST_URL);

final org.json.JSONObject blob = new org.json.JSONObject(recorder.finalizeBlob());
assertFalse("blank URL should not produce a last_loaded_domain entry",
blob.has("last_loaded_domain"));
}
}
Loading