Skip to content

MAM broker-install request resume — Common engine (flag off) [AB#3676213], Fixes AB#3676213 - #3189

Draft
wzhipan wants to merge 9 commits into
devfrom
copilot/mam-broker-install-resume
Draft

MAM broker-install request resume — Common engine (flag off) [AB#3676213], Fixes AB#3676213#3189
wzhipan wants to merge 9 commits into
devfrom
copilot/mam-broker-install-resume

Conversation

@wzhipan

@wzhipan wzhipan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What & why

Adds the Common engine for MAM broker‑install request resume (Feature AB#3676213, under Epic 3676182). When Conditional Access blocks interactive sign‑in and requires Company Portal (CP), this parks the interactive request, injects the Play Store install referrer, and — once CP is installed and the app returns to the foreground — force‑fresh re‑discovers the broker and resumes the parked request so a token is delivered on the app's original callback, with no re‑authentication. This matches the iOS behavior.

Entirely behind a feature flag that is OFF by default (CommonFlight.ENABLE_BROKER_INSTALL_RESUME = false) — no behavior change in production until the flight is enabled.

What's in this PR (Common only)

  • Park engineBrokerInstallResumeManager / …Registry / …Parker / ParkedRecord: in‑memory park of the blocked interactive request keyed by correlation id, stashing the UPN and keeping the broker sink pending.
  • Install referrerMamInstallReferrerBuilder: injects &referrer=<originAppPackage> into the Play Store install launch (the calling‑app side of the CP redirect story).
  • Resume detectionBrowserAuthorizationFragment / CurrentTaskBrowserAuthorizationFragment / AzureActiveDirectoryWebViewClient: detect the mam_resume redirect and validate the caller.
  • Resume engine + funnelBrokerInstallResumeEngine / …Coordinator / …ParamsFactory: project the parked interactive request to a silent, force‑fresh, login_hint=UPN retry and deliver the outcome once on the original callback.
  • Foreground triggerBrokerInstallResumeForegroundObserver.
  • Force‑fresh broker discovery fixBrokerDiscoveryClient: a force‑fresh discovery (getActiveBroker(shouldSkipCache=true)) now also refreshes the in‑memory cache, so a broker installed mid‑process is picked up by the eligibility path (previously it stayed pinned to the stale "no broker" result when the BROKER_DISCOVERY_CACHE flight is on). This is the fix that lets the resume actually route through the freshly‑installed CP.
  • Error taxonomy + telemetryBrokerInstallationRequiredException, ExceptionAdapter mapping, new SpanName / AttributeName entries, BrokerInstallResumeTelemetryHelper.

Verification

  • 79 unit tests across 11 new/updated classes, all green (park/registry/parker/engine/coordinator/params‑factory/referrer‑builder/telemetry/exception/exception‑adapter/discovery‑cache).
  • On‑device E2E (emulator, com.microsoft.Office config, MAM‑CA account): request parked across CP install → on foreground the request resumed → force‑fresh discovery re‑ran and found CPbroker eligibility passed (request_eligible_for_broker=true, broker_app_used=true) → CP's BrokerAuthorizationActivity launched to service the resumed request. Before the discovery fix this same path bailed immediately with RequiredBrokerMissing / broker_app_used=false.

Why draft / what's NOT covered here

  • The final leg — CP completing MAM device enrollment and returning the token to the calling app — was not automatable on the emulator (CP's auth WebView is FLAG_SECURE; also needs real Intune enrollment). It is downstream of this code (CP/Intune), not a defect here.
  • CP's auto‑redirect back to the calling app after install is a CP‑side feature (sibling Feature #3686092, "August release of CP") driven by the install referrer this PR injects; it isn't verifiable against current prod CP. The Android side handles both the CP‑redirect and the manual‑return triggers via the same (verified) foreground‑resume path.

Cross‑repo (follow‑ups, not required for this PR to be correct)

  • OneAuth‑MSAL glue (Feature #3686091, tracked by AB#3696097) depends on this (flight + force‑fresh discovery + sink‑wait). Held until Common lands.
  • Broker broker4j telemetry mirror of the new AttributeNames (cross‑repo consistency, §6.5) — held as a paired change.

Work items

Feature AB#3676213; PBIs AB#3686093 / AB#3686094 / AB#3686095 / AB#3686097.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Zhipan Wang and others added 9 commits July 19, 2026 20:57
Adds the foundational, flag-gated building blocks for the MAM broker-install
request-resume engine (Feature AB#3676213, PBI AB#3686093). No behavior change
when the flight is off.

- CommonFlight.ENABLE_BROKER_INSTALL_RESUME (ECS "EnableBrokerInstallResume",
  default off) - single gate for the whole engine.
- SpanName.BrokerInstallResume + broker_install_resume_* AttributeName entries
  for the parked -> referrer-fired -> resume-received -> retry-success ->
  delivered funnel (+ resume-received-no-park). NOTE: mirror the new
  AttributeName values in broker4j (separate broker-repo change).
- BrokerInstallationRequiredException carrying the WPJ username (UPN) + install
  link; ExceptionAdapter returns it for broker_needs_to_be_installed ONLY when
  the flight is on, otherwise the pre-existing ServiceException is unchanged.
- ParkedRecord + BrokerInstallResumeRegistry: in-memory, per-process,
  cid-keyed park store with single-use match, 7-min TTL sweep, and a
  single-resolution guard so the parked sink fires exactly once.

Unit tests: registry/record semantics (single-use, concurrency, expiry, sweep,
single-resolution), the exception, and ExceptionAdapter flag on/off gating.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the in-memory park store into the command pipeline so a Conditional-Access
"install broker" (Company Portal) interactive request is parked instead of
surfacing the terminal install-required error (Feature AB#3676213, PBI AB#3686093).
Flight-gated; no behavior change when ENABLE_BROKER_INSTALL_RESUME is off.

- BrokerInstallResumeParker: extracted, unit-testable decision logic.
  * parkIfEligible(): parks when flight on + InteractiveTokenCommand + ERROR
    result carrying BrokerInstallationRequiredException; stashes the UPN and a
    7-min expiry. Silent commands and non-broker-install errors are never parked.
  * isCallbackSuppressed(): true while a matching parked record exists.
- CommandDispatcher: park in executeCommand() after the result is finalized;
  suppress the terminal callback in returnCommandResult() for parked cids so
  the app's sink stays pending (to be resumed/expired in Phase 4).

Unit tests: parker eligibility (flight on/off, interactive vs silent, broker-
install vs other error, completed) and callback suppression. Full controllers +
commands package suites pass (54 tests) - no regression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the production Play Store install-referrer builder for the MAM broker-install
request-resume flow (Feature AB#3676213, PBI AB#3686094), graduating the earlier
proof-of-concept into a tested common4j component that both the MSAL launch path
and the prod broker-WPJ launch path consume.

- MamInstallReferrerBuilder (common4j): builds the frozen wire contract
  src=mamca&originPkg=..&redirectUri=..&cid=.. ; decorates the eSTS app_link with
  a single, allow-list-safe referrer parameter (primary); builds the
  market://details?id=CP&referrer=.. fallback; and parses the referrer back
  (Company Portal blueprint). decorateAppLinkWithReferrer is null-safe: it returns
  the original app_link unchanged if any input is missing, so the existing install
  flow can never be broken. Routing data only - no UPN/secrets.
- Removes the superseded MamResumeReferrerPocTest (its logic is now production
  code with fuller coverage).

Unit tests: packed-referrer shape/length, allow-list compatibility against the
real BrokerInstallLinkValidator, exactly-one-referrer-param, byte-for-byte
round-trip of a base64 redirect URI (+ / =), null-safety, and the market://
fallback. Full providers package passes (193 tests).

Note: the concrete launch-site call (supplying cid + redirectUri from the request
context) lands in the prod broker-side WPJ path (cross-repo, D4). getThreadCorrelationId
is not a safe cid source at the MSAL WebView thread (it fabricates a UUID when
unset), so the fragile MSAL-WebView wiring is intentionally not forced here.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the pure-Java detection primitive for the Company Portal resume redirect
(Feature AB#3676213, PBI AB#3686095). When CP finishes install it redirects back
to the calling app via msauth://<app>/<hash>?mam_resume=<cid>; this classifies
that redirect and exposes the parked correlation id for the resume engine (Phase 4).

- AuthenticationConstants.AAD.MAM_RESUME_KEY = "mam_resume".
- RawAuthorizationResult.ResultCode.BROKER_INSTALL_RESUME(2012).
- getResultCodeFromFinalRedirectUri(): on the msauth redirect scheme, presence of
  mam_resume is the discriminator and takes precedence over other classifications.
- RawAuthorizationResult.getMamResumeCorrelationId(): extracts the cid (or null).

Unit tests: mam_resume classification, cid extraction, precedence over app_link,
scheme-scoping, and a regression asserting the existing broker-install-required
classification is unchanged. Full providers package passes (27 tests).

Note: the Android routing of this result code into the resume path, plus the
caller == Company Portal signature validation, land with the resume engine in
Phase 4 (they consume this primitive and the Phase 1 registry).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Closes the resume loop on the common side (Feature AB#3676213, PBI AB#3686097):
project the parked interactive request to a silent broker retry, deliver the
result to the app's original callback exactly once, and emit the funnel. The
live silent submit + force-fresh discovery (getActiveBroker(shouldSkipCache=true))
are driven by the platform/OneAuth glue that holds the controller factory; this
change provides the common-side, testable guarantees they build on.

- BrokerInstallResumeParamsFactory.toSilentParameters(): projects
  InteractiveTokenCommandParameters -> SilentTokenCommandParameters (all shared
  CommandParameters + TokenCommandParameters fields) and sets login_hint = UPN
  (preserving the existing hint when no UPN is available).
- BrokerInstallResumeEngine: deliverSuccess/deliverError fire the parked callback
  exactly once via ParkedRecord.tryResolve() (single-resolution guard);
  sweepAndResolveExpired resolves TTL-expired parks with the original
  install-required error so a parked request never hangs.
- BrokerInstallResumeTelemetryHelper: stateful funnel span
  (parked -> referrer_fired -> resume_received -> retry_success -> delivered,
  plus resume_received_no_park), OK on delivery / ERROR + recordException on
  failure, span ended exactly once. Modeled on CertBasedAuthTelemetryHelper.

Unit tests: single-resolution races (success/error/after-success), TTL sweep
resolution, the full param projection + login_hint behavior, and the funnel
lifecycle on success/failure paths. Full commands + opentelemetry packages pass
(48 tests).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the common4j resume orchestration spine that ties the park registry,
params projection, engine, and funnel together behind one entry point:

- BrokerInstallResumeCoordinator.resume(record, submitter, telemetry):
  projects the parked interactive request to silent params (login_hint = WPJ
  UPN), invokes a platform-supplied ISilentResumeSubmitter (force-fresh broker
  discovery + silent submit), and delivers the outcome to the app's original
  callback exactly once via BrokerInstallResumeEngine. Emits the resume funnel
  when a telemetry helper is supplied. The submit is delegated because
  IControllerFactory exposes no cache-skip knob and the parked command's
  controllers were resolved before Company Portal was installed.

- BrokerInstallResumeRegistry.claimAllPending(): foreground-fallback lookup for
  the cid-less resume path (CP redirect that only foregrounds the app). Claims
  and removes every still-parked record single-use.

Unit tests: BrokerInstallResumeCoordinatorTest (success projection/delivery,
submitter-throws error delivery, already-resolved no-op, missing-params guard),
plus claimAllPending cases in BrokerInstallResumeRegistryTest.

Flight-gated end-to-end via CommonFlight.ENABLE_BROKER_INSTALL_RESUME.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the common-engine primitives into the Android common module so a
parked broker-install request can actually be resumed on-device, and
tags the Company Portal install launch with the calling app package.

Android (common):
- BrokerInstallResumeManager: Android orchestrator with two triggers -
  onResumeRedirect (automatic; capability trust = unguessable cid match +
  Company Portal installed-and-valid, since the browser redirect does not
  expose the caller package) and onAppForegrounded (foreground fallback;
  resumes any parked request once Company Portal is a valid broker). It
  validates Company Portal via BrokerValidator, and delegates the actual
  force-fresh discovery + silent submit to a platform-registered
  ISilentResumeSubmitter (MSAL/OneAuth). With no submitter registered it
  resolves the parked sink with the original install-required error.
- BrokerInstallResumeForegroundObserver: dependency-free
  ActivityLifecycleCallbacks that drives the foreground-fallback resume on
  app foreground. install(Application) registers it once per process. This
  is the trigger that makes the flow testable without any Company Portal
  change.
- BrowserAuthorizationFragment / CurrentTaskBrowserAuthorizationFragment:
  inject the install referrer (bare origin package, the CP-confirmed
  &referrer=<originPkg> pattern) onto the app_link launch, and route a
  BROKER_INSTALL_RESUME redirect to the manager. Both flight-gated.
- AzureActiveDirectoryWebViewClient: same referrer injection on the
  WebView install-launch path.

common4j:
- MamInstallReferrerBuilder.decorateAppLinkWithOriginReferrer: CP-compatible
  bare-origin referrer form (Veena, 2026-07-17), kept on the install-link
  allow-list.
- RawAuthorizationResult: gate the mam_resume classification behind the
  flight so flight-off behavior is byte-for-byte unchanged.

Tests: BrokerInstallResumeManagerTest (flight/trust/foreground gates,
capability trust, resume delivery, no-submitter fallback), new
decorateAppLinkWithOriginReferrer cases, and a flight-off classification
regression in RawAuthorizationResultMamResumeTest.

All flight-gated behind CommonFlight.ENABLE_BROKER_INSTALL_RESUME.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Hardens BrokerInstallResumeManager so a resume trigger carrying an
empty/absent correlation id returns false without touching the registry
(avoids ConcurrentHashMap.remove(null) on the browser-redirect path where
the mam_resume value could be empty). Adds a regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n force-fresh

getActiveBroker(shouldSkipCache=true) previously updated only the persistent active-broker cache, leaving the in-memory cachedData pinned to CachedBrokerData(null) for the process lifetime. With BROKER_DISCOVERY_CACHE on, the eligibility path (getActiveBrokerWithInMemoryCache) then kept returning 'no broker', so a broker installed mid-process (MAM broker-install resume: Company Portal) was never surfaced and the resumed request could not complete through it.

Fix: a force-fresh discovery now also overwrites the in-memory cache with its authoritative result. Adds a regression test (broker installed mid-process -> force-fresh -> in-memory read returns it). AB#3696097 / AB#3676213.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown

✅ Work item link check complete. Description contains link AB#3676213 to an Azure Boards work item.

@github-actions github-actions Bot changed the title MAM broker-install request resume — Common engine (flag off) [AB#3676213] MAM broker-install request resume — Common engine (flag off) [AB#3676213], Fixes AB#3676213 Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant