MAM onboarding Phase 1: install-referrer redirect to the calling app (flight-gated), Fixes AB#3676213 - #3193
Conversation
…(flight-gated) Adds the Company Portal install-referrer decoration so that, when an interactive auth request is interrupted by a Conditional-Access "install broker" (Company Portal) response, the Play Store install link is tagged with the calling app's package as the install referrer. Company Portal reads this referrer and redirects the user back to the originating app after install. This is product Phase 1 (redirect only) - no park / auto-resume, which ships separately as Phase 2. Scope (all in Common, all gated on CommonFlight.ENABLE_BROKER_INSTALL_RESUME, default off): - common4j: new MamInstallReferrerBuilder (+ unit tests) providing the CP-compatible bare-origin form decorateAppLinkWithOriginReferrer(appLink, originPkg) => "...&referrer=<originPkg>", allowlist-preserving and null-safe. - common4j: add the ENABLE_BROKER_INSTALL_RESUME flight constant. - common: decorate the install launch at the three broker-install call sites - BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment, AzureActiveDirectoryWebViewClient. originPkg is sourced locally from the Android context (getPackageName / getActivity().getPackageName()); no cross-repo / OneAuth plumbing is required on the build side. With the flight off the install link is launched exactly as before. End-to-end redirect-back requires the Company Portal read-side (separate CP feature); until then the decoration is a harmless flight-gated no-op. Verified: common4j MamInstallReferrerBuilderTest = 10 tests, 0 failures; :common:compileLocalDebugJavaWithJavac BUILD SUCCESSFUL. Feature AB#3676213 (Broker-Install Request Resume - Common Engine) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Work item link check complete. Description contains link AB#3676213 to an Azure Boards work item. |
|
❌ Invalid work item number: AB#3676213):. Work item number must be a valid integer. Click here to learn more. |
…thod The broker-install "Get the app" launch is triggered from three independent paths (BrowserAuthorizationFragment, CurrentTaskBrowserAuthorizationFragment, and AzureActiveDirectoryWebViewClient), and each duplicated the same flight-check + null-package guard + referrer decoration. Extract that policy into MamInstallReferrerBuilder.decorateAppLinkWithOriginReferrerIfEnabled (common4j) so the ENABLE_BROKER_INSTALL_RESUME gate is evaluated in exactly one place and is unit-testable as pure Java. The two browser fragments' identical adapter collapses into a shared protected decorateInstallLinkWithReferrer helper on their common AuthorizationFragment base; the WebViewClient keeps a 2-line adapter (it uses getActivity() and posts on a Handler) but no longer re-implements the flight logic. Behavior is unchanged (flight default off). Add unit tests for the gated method (flight on decorates identically to the ungated form; flight off / missing package / no flights manager all return the link unchanged). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
✅ Work item link check complete. Description contains link AB#3686094 to an Azure Boards work item. |
There was a problem hiding this comment.
Pull request overview
This PR introduces Phase 1 of improved MAM onboarding by (flight-gated) tagging the Company Portal Play Store install link with the calling app’s package as the Play install referrer, enabling Company Portal to redirect the user back to the originating app after install.
Changes:
- Added
MamInstallReferrerBuilder(and unit tests) to decorate Play Store install links with a CP-compatiblereferrer=<originPkg>form behindCommonFlight.ENABLE_BROKER_INSTALL_RESUME. - Added the
ENABLE_BROKER_INSTALL_RESUMEflight constant toCommonFlight. - Wired the decoration into broker-install launches across the browser fragments and embedded WebView flow.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| common4j/src/main/com/microsoft/identity/common/java/providers/MamInstallReferrerBuilder.java | Adds referrer-building and flight-gated decoration helpers for Company Portal redirect-back. |
| common4j/src/test/com/microsoft/identity/common/java/providers/MamInstallReferrerBuilderTest.java | Adds unit coverage for decoration/idempotency/allowlist/round-trip parsing behavior. |
| common4j/src/main/com/microsoft/identity/common/java/flighting/CommonFlight.java | Introduces the ENABLE_BROKER_INSTALL_RESUME flight toggle. |
| common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java | Decorates the broker install link launched from the embedded WebView flow. |
| common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationFragment.java | Centralizes install-link decoration for AuthorizationFragment subclasses. |
| common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/BrowserAuthorizationFragment.java | Uses the shared decoration helper before launching the broker install link. |
| common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/CurrentTaskBrowserAuthorizationFragment.java | Uses the shared decoration helper before launching the broker install link. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- decode(): also catch IllegalArgumentException so parseReferrer stays
defensive against malformed percent-encoding (e.g. a lone '%' or '%zz')
instead of crashing; added a regression test.
- MamInstallReferrerBuilder Javadoc: drop the individual name/date and
reference Feature 3676213 instead ("server-provided" app_link).
- ENABLE_BROKER_INSTALL_RESUME: reword as an explicit multi-phase gate so
the description matches what Phase 1 actually gates (referrer/redirect-only)
vs. later park-and-replay phases.
- AzureActiveDirectoryWebViewClient.processInstallRequest: cache the Activity
once inside the postDelayed runnable and bail out (with a warn) if the
Activity or app_link is gone, so the delayed launch never NPEs on a
detached fragment.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…on, drop Phase-2 surface Brings the install-referrer change in line with the phased MAM-CA design: - Scope the decoration to the MAM Conditional Access install path. The same msauth://wpj redirect also drives ordinary device-registration installs, which must keep behaving as they do today, so decoration now requires the server's intuneAppProtection=1 marker. New MamCaRedirect reads the markers (and logs the redirect's parameter names only - the redirect carries the user's UPN, so the URL itself is never logged). - Add ENABLE_MAM_CA_INSTALL_WITHOUT_MARKER (default off) so the client behavior can be validated and rolled out before the server marker ships. - Rename the gate to ENABLE_MAM_CA_INSTALL_REFERRER. The old name described a park-and-resume design that the phased plan replaces; the flight is default off and unshipped, so the rename is free. - Drop the Phase-2 park/replay surface from MamInstallReferrerBuilder (buildReferrerValue, decorateAppLinkWithReferrer, buildMarketFallbackUri, parseReferrer and their constants). Phase 1 is referrer-only. Tests: MamInstallReferrerBuilderTest 12/12, new MamCaRedirectTest 7/7. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…marker The server now appends intuneAppProtection=1 as a top-level query parameter on the broker-install redirect when Conditional Access blocks the request pending a Company Portal install, so the client no longer needs an escape hatch for running ahead of that marker. - Drop the ENABLE_MAM_CA_INSTALL_WITHOUT_MARKER flight and its fallback. - Collapse hasIntuneAppProtectionMarker into isMamCaInstall, now a pure predicate; logRedirectParameterNames remains the single diagnostic. - Document that the marker is a top-level parameter on the redirect, appended after app_link, and not nested inside the app_link value. - Replace the flight-dependent tests with strict marker tests, including one that pins the exact install-link shape the server produces. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Praveen Kumar (Prvnkmr337)
left a comment
There was a problem hiding this comment.
A few review comments — thanks for the clean phased scoping and the marker-based gating.
The query parser turns a trailing token with no '=' into a key with a null value, so a malformed broker-install redirect ending in a bare '?user@contoso.com' would put the UPN itself on the non-PII log channel. Only keys matching an ordinary parameter-name shape are printed now; the rest are counted. Identical to the change on copilot/mam-upn-hint-store (#3195), which carries the same file, so the two branches stay conflict-free. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New classes are expected to be Kotlin, so MamCaRedirect and MamInstallReferrerBuilder and their tests move over. Behaviour is unchanged by the conversion itself; the existing assertions are the safety net. Review fixes on top of that: - The parameter-name log fired on every broker-install redirect, ahead of the flight check, so turning the flight off did not turn the feature off. It now sits behind the gate, inside decorateAppLinkForMamCaInstall, which also extends it to the browser and custom-tab flows that never had it. - A referrer the server already put on the install link is now kept rather than overwritten. The server names the calling app; this code can only see the package of the process hosting the sign-in UI, which is the broker's when a broker is hosting the flow. Where they disagree the server is right. - The "tagged the install launch" log fired even when the link was returned unchanged, so the rollout signal could report success for a no-op. It now lives on the path that actually decorated the link. - Corrected the comment on the install-launch guard: it does not make the Runnable detach-safe, and it deliberately does not check isFinishing(), because the result callback finishes the Activity before this fires. Tests: MamCaRedirect 7, MamInstallReferrerBuilder 15. Two assertions the review called weak are now absolute, and the no-UPN-on-the-log property is pinned at the public call site rather than only on its helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The referrer builder itself is well covered in common4j, but the Android glue that reaches it - threading the redirect parameters from the WebView client and the authorization fragments into the install launch - had no tests, which is what dragged the common module's coverage down. Adds Robolectric coverage for both call sites: the flight gate, the MAM-CA marker gate, the calling-app package being named as the Play install referrer, and a referrer the server already set being preserved rather than overridden. Also pins that a non-https app_link is rejected by the broker-install allowlist before the launch is ever reached, which is worth stating explicitly because it makes the browser:// rewrite further down unreachable for app_links. Test-only change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Praveen Kumar (Prvnkmr337)
left a comment
There was a problem hiding this comment.
Follow-up on the latest changes.
|
Should this emit a metric while the flight is on? The only observability in this PR is
Add one attribute in this PR, or defer to a follow-up that lands before we ramp past 0%? |
… outcome Two review comments on #3193. 1. Comment on the "link already names a referrer" guard. Added the requested comment, but with a corrected rationale. The concern raised was that addParameterIfAbsent may be case-sensitive; it is not. CommonURIBuilder.addParameterIfAbsent delegates to containsParam, which compares with equalsIgnoreCase, so both paths already agree on a mixed-case "Referrer". Writing the suggested reason down would have baked a falsehood into the source. What the explicit branch actually earns is that we hand back the caller's original string instead of a re-serialised build(), and that we log "left it alone" rather than "tagged it". The comment says that. 2. Should the feature emit a metric while the flight is on? Yes - implemented here rather than deferred, because it needs no schema change. MamInstallReferrerBuilder now reports one of seven outcomes (FLIGHT_OFF, NOT_MAM_CA, NO_ORIGIN_PKG, NO_APP_LINK, SERVER_REFERRER, LINK_UNPARSEABLE, DECORATED) and AzureActiveDirectoryWebViewClient forwards the reportable ones to the existing OnboardingTelemetryRecorder.addUxFlowUsed, which surfaces as mo_ux_flow_used. That answers both ramp questions from Kusto: how often a marked redirect reaches the decoration, and whether the server already supplied a referrer. FLIGHT_OFF carries a null tag so nothing is reported at all when the flight is off. That keeps the earlier commitment that a disabled flight leaves no trace, now extended to telemetry, and it is enforced by a test rather than by convention. The marker check now runs before the origin-package check. Both branches return the link unchanged, so this is reporting-only: a missing package on a marked redirect is now visible as such instead of being masked as "not MAM-CA". Only the embedded-WebView path has a telemetry recorder. The browser fragments stay log-only, which is acceptable because MAM-CA onboarding runs through the WebView, but it is a real gap worth naming. Tests: MamInstallReferrerBuilderTest 20/20 (was 15), MamCaRedirectTest 7/7, AzureActiveDirectoryWebViewClientTest 92/92 (was 87), AuthorizationFragmentInstallReferrerTest 6/6. Negative-tested by giving FLIGHT_OFF a non-null tag: the three tests that pin the silent-when-off guarantee failed as intended, and nothing else moved. Public API unchanged - decorateAppLinkForMamCaInstall keeps its signature and delegates, so AuthorizationFragment and both browser fragments are untouched. MamCaRedirect.kt is deliberately not modified; it stays byte-identical with #3195 so the two PRs merge in either order. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Good call - done in 810bf9c rather than deferred, because it turned out to need no schema change.
That answers both ramp questions directly: Two things worth flagging:
Only the embedded-WebView path has a recorder. One behavioural note: the marker check now runs before the origin-package check. Both branches return the link unchanged, so this is reporting-only - it stops a missing package from being misreported as "not MAM-CA". |
The outcome was reported from inside the 1s-delayed install Runnable, but the completion callback fires first: it sends the result and finishes the Activity, and the onboarding blob is finalized off that same result (broker side, AccountChooserActivity#finalizeOnboardingBlobForFailure -> finalizeBlob). Anything added to the recorder a second later would be written into a blob that had already shipped, so mo_ux_flow_used would never have carried the ramp signal it exists for. Decoration and reporting now happen before the callback; the Runnable keeps only the launch. getActivity() is a final field, so hoisting it cannot change whether the launch happens - the same null check simply moves earlier. This mirrors STEP_BROKER_INSTALL_PROMPTED, which is already recorded at method entry. The existing telemetry tests could not catch this because they finalize the blob themselves at the end of the test, observing a tag production would never emit, so a test that snapshots the blob from inside the completion callback is added. Negative-tested: recording only from the Runnable fails it (plus the three outcome tests), and nothing else moves. AzureActiveDirectoryWebViewClientTest 94/94. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ux_flow_used records which UX-variant cohort a user was in - the "MobileOnboardingPhase0/1/2" family - not per-attempt result codes. This was writing six of them into it, five of which mean "the feature did not apply", onto a shared append-only array that other onboarding features also append to. It never reached MATS in any case. The only way to attach a recorder to AzureActiveDirectoryWebViewClient is setOnboardingTelemetryRecorder, which has no production caller anywhere in common, broker, msal, adal or oneauth - only tests. The hosts that build a recorder build their own and never hand it over, so recordMamCaReferrerOutcome always hit its null-recorder early return: the onboarding blob captured from a device run of this feature carries no ux_flow_used key at all, despite that run producing an outcome whose tag is non-null. This also settles the review point about the browser/custom-tab path decorating without reporting. With no telemetry on either launch site, the embedded WebView and the two browser fragments now make the same call and emit the same MamInstallReferrerBuilder log lines - which is what the ramp is read off, and what the E2E runs used. Outcome and the WithOutcome overload stay: six of the seven outcomes return the app_link unchanged, so without them the flight gate, the MAM-CA gate and the server-referrer branch are indistinguishable in tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Clarification on All three production call sites use the non-
|
Only three of the seven outcomes left a log line, so a capture could not say whether a link went untagged because the marker was missing, because the host could not name itself, or because there was no app_link at all. Each of those three now logs, and the three that already logged carry the outcome value too. FLIGHT_OFF stays silent by design: with the flight off this feature is meant to be indistinguishable from its absence, log lines included. The new test reads the expected value off the enum rather than hard-coding strings, so a rename cannot leave the log and the enum disagreeing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
1. Test-only, by design. Six of the seven outcomes return the app_link unchanged, so without the overload the flight gate, the MAM-CA gate and the server-referrer branch are indistinguishable in tests. No production consumer planned for Phase 1. 2. Partly intentional. 3. Not from these PRs. Mobile-onboarding telemetry is being handled as a separate follow-up project, and MAM-CA onboarding will be wired there. It is also not a small add here: at the OneAuth boundary a MAM-CA install currently arrives as plain |
|
LGTM |
…ay Store
The MAM-CA marker says the redirect is a MAM Conditional Access install, but it
does not say which app the redirect installs. A broker-install redirect's app_link
is allowed to name any of three destinations, and only one of them can act on a
Play install referrer:
- the Company Portal Play listing - reads the referrer to skip its own sign-in
UX and redirect back to the caller, which is the whole point of this change;
- the Authenticator Play listing - honours no such contract;
- the China Company Portal link - a go.microsoft.com fwlink rather than a Play
Store link, so a referrer on it is never delivered.
Decoration was package-agnostic, so a marked redirect carrying either of the last
two got tagged and reported as DECORATED. Appending an inert query parameter is
harmless in itself, but rollout is read off these log lines, so it would have
overstated how far the feature had actually reached - and the China link is the
more likely of the two to be hit.
Constrain decoration to the Company Portal Play listing and report the rest as a
distinct NOT_COMPANY_PORTAL outcome, so "skipped, wrong install target" stays
separable from "tagged". Only the package id is matched: BrokerInstallLinkValidator
has already restricted app_link to its allowlist, unflighted, on the path that
produces BROKER_INSTALLATION_TRIGGERED in the first place.
The check sits inside the existing parse rather than ahead of it, so it reuses the
one CommonURIBuilder and an unparseable app_link is still reported as
LINK_UNPARSEABLE instead of being mislabelled as the wrong target.
Behaviour is unchanged for every link the feature is meant to tag, and unchanged
for unmarked redirects: ordinary device-registration and MDM installs share this
redirect and are still identified solely by the absence of the marker.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Pushed one more scoping change ( The gap. The
Appending an inert parameter is harmless in itself, but rollout is read off these log lines, so both cases would have been reported as The change. Decoration is now constrained to the Company Portal Play listing; anything else reports a new MDM/device registration is unaffected — those share this redirect and are still identified solely by the absence of the marker, which this does not touch. 3 tests added, plus the new outcome wired into Praveen Kumar (@Prvnkmr337) sorry — this invalidates your approval from earlier, would you mind taking another look? |
Reverts 18a1984. That change narrowed decoration to app_links naming the Company Portal Play listing, on the reasoning that the MAM-CA marker says the redirect is a MAM-CA install but not which app it installs. Both premises behind it were wrong: - the server sets intuneAppProtection=1 only on MAM-CA flows, so the marker alone already establishes the flow, and the referrer should always be appended once it is present and the flight is on; - the China `go.microsoft.com/fwlink/?linkid=2134649` target is not a different app. It is Company Portal by another distribution channel, and BrokerInstallLinkValidator documents that the server itself may already set a `referrer` on it - so the claim that a referrer there is inert was speculation, and contradicted. The gate therefore did not tighten anything. It dropped the referrer on real MAM-CA installs in China, which is the one thing this class exists to add. Keep a test and a note pinning the decision, because re-adding the gate looks like a safe tightening and is not - that is exactly the mistake being reverted here. The class KDoc now says the marker is the whole of the scope and why, and gated_markedInstall_isDecoratedWhateverTheInstallTargetIs asserts all three allowed destinations are tagged. Re-introducing the gate fails that test and nothing else. Net effect against the last reviewed state is a comment and a test; production behaviour is byte-for-byte what was approved. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Correction to my previous comment — that scoping change is reverted ( Confirmed with Veena: the server sets
The gate didn't tighten anything; it would have dropped the referrer on real MAM-CA installs in China. Net effect against the state you last reviewed is a comment and a test — production behaviour is byte-for-byte identical to
Praveen Kumar (@Prvnkmr337) apologies for the churn — your approval was never dismissed and still stands, so no action needed from you. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Suppressed comments (2)
common4j/src/main/com/microsoft/identity/common/java/providers/MamInstallReferrerBuilder.kt:264
- The success log message is overly specific to “Company Portal”, but the implementation explicitly decorates any install target on a marked redirect (including non-CP targets like Authenticator / fwlinks, per tests). Recommend making this message target-agnostic (e.g., “Tagged the broker-install launch…” or “Tagged the install link…”) to avoid misleading field diagnostics.
Logger.info(
methodTag,
"Tagged the Company Portal install launch with the calling app as the install " +
"referrer. [outcome=${Outcome.DECORATED}]"
)
common4j/src/main/com/microsoft/identity/common/java/providers/MamInstallReferrerBuilder.kt:231
decorateWithOutcome(...)always uses thedecorateAppLinkWithOriginReferrermethod tag even when invoked from the gated MAM-CA entry point. This makes log correlation harder because some outcomes for the MAM-CA path will appear under a different method tag than others. Consider passing the caller’s method tag intodecorateWithOutcome(or computing the tag at the call site) so logs consistently identify the entry point being evaluated.
private fun decorateWithOutcome(appLink: String?, originPkg: String): Decoration {
val methodTag = "$TAG:decorateAppLinkWithOriginReferrer"
The two browser fragments hand app_link straight to Uri.parse, and it is not obvious from the launch sites themselves that it cannot be null: the presence check upstream is containsKey, which passes for a bare &app_link or an empty &app_link=. What actually rules those out is the broker-install allowlist, which rejects a blank link before the result code is ever set. Pin that with a test and note it at both call sites, so the guarantee is enforced rather than inferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (1)
common/src/main/java/com/microsoft/identity/common/internal/providers/oauth2/AuthorizationFragment.java:242
- The new helper is intentionally null-tolerant (tests pass null
appLink/redirectParametersand expect a null / unchanged result), and it delegates to a Kotlin API that returns a nullableString. Without@Nullableannotations here, Java/Kotlin callers lose the actual contract and static analysis can miss real null flows.
Recommendation: annotate the return value and both parameters as @Nullable to reflect the true behavior and improve interop.
protected String decorateInstallLinkWithReferrer(final String appLink,
final Map<String, String> redirectParameters) {
final Context context = getContext();
return MamInstallReferrerBuilder.decorateAppLinkForMamCaInstall(
appLink, context == null ? null : context.getPackageName(), redirectParameters);
…B#3706623) Resolve the sole CommonFlight.java enum conflict by keeping both new flights (ENABLE_MAM_CA_INSTALL_REFERRER from dev PR #3193 and ENABLE_PKEYAUTH_SUBMIT_URL_ORIGIN_VALIDATION), with the PKeyAuth flight last. Review findings addressed in the same commit: - Finding 1 (log-forging): drop the warnPII that logged the raw, attacker-shaped authority slice in the backslash guard; the existing non-PII Logger.warn plus the thrown ClientException carry all diagnostic signal. Added authorityLength (int only) to that warn for triage. - Finding 2 (coverage): add onPageStarted origin-tracking tests — https redirect target becomes the challenging origin; a non-https onPageStarted does not replace a recorded https origin; flight-off is a no-op. - Finding 3 (stale doc): reword the CHALLENGING_ORIGIN fixture comment to state the same-origin check compares scheme + host + normalized port (fixture differs only in path/query), not host-only. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…eferrer, Fixes AB#3686094 (#3212) ## Summary Follow-up to #3193. That PR gated the MAM-CA install referrer on `CommonFlight.ENABLE_MAM_CA_INSTALL_REFERRER`. That gate cannot be read in the app process that needs it, and in the one process where it *can* be read it would have produced the wrong answer. This moves the decision to a boolean supplied by the host SDK. PBI [AB#3686094](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3686094), Feature [AB#3676213](https://identitydivision.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_workitems/edit/3676213). ## Why the flight was the wrong mechanism **In the app process it could never be turned on.** The only production initialization of `CommonFlightsManager` in the tree is `AndroidBrokerPlatformComponentsFactory` → `AndroidBrokerFlightsManager.initializeFlightsManager` (`AndroidBrokerFlightsManager.kt:92`), which runs in the **broker**. OneAuth and MSAL have zero references to `CommonFlightsManager`, `CommonFlight` or `IFlightsManager`, so in an app process the flight always falls back to its compile-time default of `false`, and no ECS entry could change that. That matters because the referrer is only ever added on the **brokerless** path: the decoration uses the package of the process hosting the sign-in UI, which — when no broker is installed, the only case where the Company Portal install interstitial appears in-app — is the calling app. **In the broker process, where it could be turned on, it would have been wrong.** The broker also hosts an interactive WebView (`AbstractBrokerController` → `AndroidAuthorizationStrategyFactory` → `EmbeddedWebViewAuthorizationStrategy`), and "Authenticator installed, Company Portal not installed" is an ordinary enterprise state. There, `activity.getPackageName()` is the *broker's* package. So ramping the ECS flight would have tagged the Company Portal install with the broker, sending the user back to Authenticator after install instead of to the app they were signing in to — worse than not decorating at all. So the flight was unreadable where it was needed and actively harmful where it was readable. Removing it closes both by construction. ## What changed The gate is now an input from the host SDK, following the same path `isWebViewWebCpEnabled` and `enableSwitchBrowser` already take: ``` AuthorizationActivityParameters.enableMamCaInstallReferrer -> intent extra -> AuthorizationFragment -> AzureActiveDirectoryWebViewClient -> MamInstallReferrerBuilder ``` - `ENABLE_MAM_CA_INSTALL_REFERRER` is removed from `CommonFlight`. - `MamInstallReferrerBuilder`'s two entry points take a leading `enabled: Boolean`, and `Outcome.FLIGHT_OFF` becomes `Outcome.NOT_ENABLED`. The "nothing is logged while off" invariant is unchanged. - `AzureActiveDirectoryWebViewClient` gains a 9-arg constructor; the existing 8- and 7-arg ones delegate with `false` and are now test-only, so no caller breaks. - `AuthorizationFragment` saves and restores the flag alongside the request state, so it survives process death mid-flow. - `WebViewAuthorizationFragment` builds its client in a new `@VisibleForTesting createAADWebViewClient`, extracted from `onCreateView`. Behaviour-preserving - same call order and construction, the caller still assigns the field - so the fragment-to-client hop can be tested without inflating a layout, which this module's unit tests cannot do. - `CommonFlight`'s class doc now states that flights only resolve in the broker process. That warning is what would have caught this in review the first time. **Defaults to `false` everywhere, so MSAL and the broker are unchanged.** That matches the precedent: Common's own `BrowserAuthorizationStrategy` and `EmbeddedWebViewAuthorizationStrategy` do not set `isWebViewWebCpEnabled` either — OneAuth is its only supplier. ## Companion change and kill switch The OneAuth side supplies the boolean from the **`MamCaInstallReferrer` ChangeGate**, served by the **`OneAuth-MSAL` ECS project** (not the broker ECS portal), so the switch is server-flippable without an app update. It is up for review in parallel and cannot compile until this publishes — confirmed by building it against both this branch (`BUILD SUCCESSFUL`) and the currently pinned `common:24.5.0` (fails, as expected). Note that Common no longer owns a kill switch for this path. Two caveats are being tracked on the OneAuth PR: a pulled gate needs two app restarts to take effect, and gate fetching depends on `OneAuthFlight::ECS_FETCH`, which is not on by default for every host. ## Known gaps - **Brokered MAM-CA onboarding is not covered.** In the broker-hosted case above the referrer is simply not added (`false`). Doing it correctly there means using the caller's package (`InteractiveTokenCommandParameters.getCallerPackageName()`) rather than `Context#getPackageName()` — a design change, not a flag flip. Worth a Phase-2 PBI. - **No tenant scoping.** Neither mechanism available to the host is tenant-aware — OneAuth ECS config filters on `ApplicationID` / `OS` / `OneAuthVersion`, and Flights are per-app — so per-tenant rollout or exclusion is not achievable through this boolean. ## Testing | suite | result | | --- | --- | | `MamInstallReferrerBuilderTest` (common4j) | 20 / 0 | | `MamCaRedirectTest` (common4j) | 7 / 0 | | `AzureActiveDirectoryWebViewClientTest` | 87 / 0 | | `AuthorizationFragmentInstallReferrerTest` | 8 / 0 | | `AuthorizationActivityFactoryTest` | 12 / 0 | | `WebViewAuthorizationFragmentInstallReferrerTest` | 3 / 0 | All three new seams were verified by mutation rather than by passing alone: - Replacing the opt-in argument the fragment hands the client with a literal `false` - silently disabling the feature on the embedded-WebView path - left **every pre-existing suite green**, including the 87-test `AzureActiveDirectoryWebViewClientTest`, which builds the client directly and so pins the decoration rules but never the wiring. Only `WebViewAuthorizationFragmentInstallReferrerTest` catches it. - Deleting the `putExtra` in the factory failed exactly one test, and nothing else moved. - Deleting either the `extractState` read or the `onSaveInstanceState` write in `AuthorizationFragment` originally left the **entire suite green**. `AuthorizationFragmentInstallReferrerTest` now covers that hop; each deletion fails it independently. Each of the three new WebView tests was also checked to fail for a distinct, realistic defect: an inverted default (`getBoolean(key, true)`) fails only the absent-key test, and a presence-based read (`containsKey`) fails only the explicit-opt-out test. The coverage check reports a 0.01 pp line-coverage drop with `No per-class coverage regressions detected`. That is the arithmetic of deleting covered lines (the removed enum constant and its flight reads were all covered), not a regression, hence the `skip-coverage-check` label. --------- Co-authored-by: sim <sim@local> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Phase 1 of "Improve MAM onboarding on Android" (PBI AB#3686094, Feature AB#3676213).
When an interactive request is interrupted by a Conditional-Access "install Company Portal" response, tag the Play Store install link with the calling app's package as the Play install referrer. Company Portal reads it and, after install, sends the user back to the app they started in instead of stranding them in Company Portal.
Redirect-only. No park / auto-resume — that is Phase 2.
What changed
Everything is in Common and gated on
CommonFlight.ENABLE_MAM_CA_INSTALL_REFERRER(default off).common4j
MamCaRedirect— the single place that decides whether a broker-install redirect is the MAM-CA variety. Looks for theintuneAppProtectionmarker on the redirect, exposesgetUsername(), and provides a PII-safelogRedirectParameterNames()— names only, never values, and only names that are actually shaped like parameter names (the query parser turns a trailing token with no=into a key, so a malformed redirect could otherwise put a UPN on the log).MamInstallReferrerBuilder— two entry points:decorateAppLinkForMamCaInstall(appLink, originPkg, redirectParameters)— flight-gated and scoped to MAM-CA redirects only, so a plain WPJ/device-registration install is untouched.decorateAppLinkWithOriginReferrer(appLink, originPkg)— the ungated primitive.Allowlist-preserving, null-safe, idempotent (exactly one
referrerparam); on any parse problem it returns the original link so the existing install flow can never be broken.ENABLE_MAM_CA_INSTALL_REFERRER.common
AuthorizationFragment, both browser fragments, andAzureActiveDirectoryWebViewClient#processInstallRequest), which is what lets the decoration be scoped to MAM-CA.originPkgis still sourced locally from the Android context — no cross-repo plumbing.With the flight off the link is launched exactly as before.
Design alignment
Reworked to match the current phased design doc:
ENABLE_BROKER_INSTALL_RESUME→ENABLE_MAM_CA_INSTALL_REFERRER. Safe: default-off, never merged, no ECS entry.Server contract
The gate is the
intuneAppProtection=1marker the server puts on the broker-install redirect (ESTS-Main PR 16454630, server-side flighted, not yet merged). Shape:It is a top-level parameter on the redirect, appended after
app_link— not nested inside theapp_linkvalue — soStringExtensions.getUrlParameters(url)already surfaces it. OnlyintuneAppProtection=1counts; anything else is treated as an ordinary broker install and left alone.Both sides are default-off, so the order they ramp in doesn't matter: until the server marker is live the client is a no-op, and until the client flight is on the marker is simply ignored.
Testing
MamInstallReferrerBuilderTest— 15 tests, 0 failures.MamCaRedirectTest— 7 tests, 0 failures.providers/flighting/controllers/commands— 242 tests, 0 failures.:common:compileLocalDebugJavaWithJavac— BUILD SUCCESSFUL.MamInstallReferrerBuilder:decorateAppLinkForMamCaInstall: Tagged the Company Portal install launch with the calling app as the install referrer.Finsky: Capture referrer for com.microsoft.windowsintune.companyportalSTART … cmp=…/OneAuthTestActivity from uid … (com.microsoft.windowsintune.companyportal)Note for reviewers
MamCaRedirect.ktis added identically by #3195, and the two copies are kept byte-identical on purpose so the PRs can merge in either order. Whichever merges second will conflict in two purely additive spots —CommonFlight(adjacent enum constants) andAzureActiveDirectoryWebViewClient(imports +processInstallRequest). Both resolve by keeping both sides. Verified withgit merge-tree:MamCaRedirect.ktitself is not in the conflict set.All new files here are Kotlin, with
@JvmStaticon everything the existing Java call sites use.Rollout
Redirect-back also needs the Company Portal read-side (separate CP feature). Until that lands this is a flight-gated no-op and safe to merge.
Work item
PBI AB#3686094 · Feature AB#3676213