Skip to content

[common] Expose a telemetry-only Auth UX JS bridge to brokerless (OneAuth) hosts, AB#3688630, Fixes AB#3688630 - #3209

Draft
wzhipan wants to merge 7 commits into
copilot/pbi-3708195-recorder-wiringfrom
copilot/pbi-3688630-telemetry-only-bridge
Draft

wzhipan wants to merge 7 commits into
copilot/pbi-3708195-recorder-wiringfrom
copilot/pbi-3688630-telemetry-only-bridge

Conversation

@wzhipan

@wzhipan wzhipan commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Related work item: AB#3688630

Stacked PR. Base is copilot/pbi-3708195-recorder-wiring (#3204). Merge order for the series:
#3197 (merged) → #3201#3204this PR. Review only the top commits; the rest is the base branch.

Why

Non-brokered (OneAuth) flows never reach the Auth UX JavaScript bridge. Exposure is gated on
ProcessUtil.isRunningOnAuthService(), which is permanently false outside the broker's isolated
:auth process — no OneAuth manifest declares that process. So a brokerless flow cannot report the
onboarding error codes log_telemetry exists to carry, even though the OneAuth core already builds
the onboarding seed for it (InteractiveRequest.cpp creates the seed for MODE_NON_BROKERED too).

The obvious fix — relax isRunningOnAuthService — is the wrong one. That predicate is the only thing
keeping the full bridge, including the number-match device store, inside the broker's isolated
process. Removing it would hand that surface to every MSAL client that renders an Auth UX page.

What

A second, narrower surface instead of a wider single one.

1. The bridge takes a telemetryOnly capability. When set it serves log_telemetry and refuses
every other action, so the only effect a page can have is appending to the onboarding telemetry blob.

Enforced at dispatch, not only at the registration gate. A restriction that lives only in the
gate is one forgotten check away from being void; enforcing it inside the bridge means a future
caller that constructs it for a brokerless host cannot re-expose the mutating path from its own call
site. The when block already matched action_name == log_telemetry first (deliberately, so a
params.operation smuggled into a telemetry message can never reach the number-match store), so the
guard sits on the one remaining mutating branch.

2. Exposure is gated by host — and outside :auth, deliberately not by a flight.

Host Gate
broker :auth process allow-list and ENABLE_JS_API_FOR_AUTHUX (unchanged)
anything else allow-list and this request carries an onboarding telemetry recorder

An earlier revision of this PR used a second CommonFlight here. That flight could never have been
turned on
, so the feature would have shipped as dead code: CommonFlightsManager only returns real
ECS values in a process that called initializeCommonFlightsManager, and the broker is its only
caller — neither oneauth nor msal ever does. The brokered flow works because its WebView runs
inside :auth; a brokerless host falls back to DefaultValueFlightsProvider and reads the
compiled-in default, forever.

Gating on the seed is better than the flight was even setting that aside:

  • self-limiting — the bridge exists only to append to the onboarding blob, so with no recorder it
    can have no effect at all;
  • plain MSAL clients are never exposed — they never seed onboarding telemetry, which was the
    widening a second flight was meant to prevent;
  • the off switch is "stop seeding" — a decision the caller already owns per request, in a process
    where it actually takes effect.

3. The capability is resolved in createAuthUxJavaScriptInterface(). onPageStarted rebuilds
the bridge on every navigation. A capability passed at a single call site would be silently dropped
on the next page load — which is exactly how the telemetry sink was lost before #3201 fixed it.
hasOnboardingTelemetryRecorder() is likewise read per navigation rather than captured, so a
recorder attached or cleared later is honoured. The recorder is available in time:
WebViewAuthorizationFragment attaches it (L393) before both initializeAuthUxJavaScriptApi (L399)
and launchWebView (L400).

Behaviour inside the broker is unchanged: the constructor default is false, and the :auth branch
of the gate is the same predicate and the same flight it was.

Tests

+11, all revert-tested.

Test Pins
telemetry-only mode makes number_matching inert the security property itself
AuthService_KeepsTheFullBridge the control — the same payload does reach the store in :auth
telemetry-only mode still forwards log_telemetry the restriction does not break the action it exists to serve
telemetry-only mode does not suppress the whole message a refused action does not abort the page's later telemetry
default mode is not telemetry-only the constructor default, against an accidental flip
BrokerlessHost_ClosedWithoutARecorder no seed ⇒ no bridge, even with the broker's flight on
BrokerlessHost_OpenedByTheRecorder a seeded request does get the telemetry-only bridge
BrokerlessHost_ClosesWhenRecorderCleared the gate is re-evaluated, not captured
BrokerlessHost_StillHonoursUriAllowList H1 still applies to the new surface
AuthService_UsesTheBrokerFlight in :auth the flight decides; a recorder is neither required nor sufficient

The control test matters more than it looks: NumberMatchHelper.storeNumberMatch silently drops a
session id that is not a GUID or 8-char alphanumeric, so a malformed fixture would have made "the
store stayed empty" true for the wrong reason and the security assertion vacuous.

212 tests green in area.

Verified on a device

Non-brokered E2E on a real device (OneAuthTestApp, broker deliberately uninstalled): the recorder is
registered, found by correlation id, the bridge binds with telemetryOnly=true, a log_telemetry
posted over real JavaScript reaches the blob as "onboarding_mode":"non-brokered", and a
number_matching message is refused with the store left empty.

⚠️ That run used a build with the (then-present) flight default flipped to true, which is precisely
what masked the flight problem described above — worth knowing when weighing the E2E evidence.

What this does not do

No host turns this on yet. The OneAuth-side work — registering its already-built
OnboardingTelemetryRecorder in OnboardingRecorderRegistry so the WebView fragment can find it —
lives in the OneAuth repo and is tracked separately. It is ~6 lines, verified on device.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

❌ Invalid work item number: AB#3688630

. Work item number must be a valid integer.

Click here to learn more.

…s, AB#3688630

Non-brokered (OneAuth) flows never reach the Auth UX JavaScript bridge: it is
gated on ProcessUtil.isRunningOnAuthService(), which is permanently false
outside the broker's isolated :auth process. Those flows therefore cannot
report the onboarding error codes the log_telemetry action exists to carry,
even though the OneAuth core already builds the onboarding seed for them.

Rather than widening the existing gate -- which would hand every MSAL client
the full bridge, including the number-match device store -- this adds a
second, narrower surface:

- AuthUxJavaScriptInterface takes a telemetryOnly capability. When set it
  serves log_telemetry and refuses every other action, so the only effect a
  page can have is appending to the onboarding telemetry blob. Enforced at
  dispatch rather than only at the registration gate, so a future caller that
  constructs the bridge for a brokerless host cannot re-expose the mutating
  path by forgetting a check at its own call site.

- shouldExposeJavaScriptInterface now selects the flight by host: the broker's
  :auth process keeps ENABLE_JS_API_FOR_AUTHUX, and any other host is gated by
  the new ENABLE_BROKERLESS_TELEMETRY_JS_API_FOR_AUTHUX (default off). Keeping
  the flights independent means the brokerless surface can be turned off
  without also disabling number-matching for the broker. The host allow-list
  (H1) still applies to both.

- The capability is resolved in createAuthUxJavaScriptInterface(), because
  onPageStarted rebuilds the bridge on every navigation; a capability passed
  at a single call site would be silently dropped on the next page load, which
  is exactly how the telemetry sink was lost before #3201.

Behaviour inside the broker is unchanged: the constructor default is false and
the :auth branch of the gate is the same predicate it was.

Tests (+8): number_matching inert in telemetry-only mode and its :auth-process
control (so "the store stayed empty" cannot pass for the wrong reason);
log_telemetry still served; a refused message does not abort the page; the
constructor default; and the four gate combinations. Revert-tested -- dropping
the dispatch guard fails "a telemetry-only bridge must never write to the
number-match store", and collapsing the two flights fails "the broker's flight
must not expose a bridge outside the :auth process".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@wzhipan
wzhipan force-pushed the copilot/pbi-3688630-telemetry-only-bridge branch from a1b3abd to 433445e Compare August 8, 2026 04:07
@github-actions github-actions Bot changed the title [common] Expose a telemetry-only Auth UX JS bridge to brokerless (OneAuth) hosts, AB#3688630 [common] Expose a telemetry-only Auth UX JS bridge to brokerless (OneAuth) hosts, AB#3688630, Fixes AB#3688630 Aug 8, 2026
Zhipan Wang and others added 3 commits August 9, 2026 16:28
…#3197 squash-merge) into the telemetry-only bridge branch
# Conflicts:
#	common/src/main/java/com/microsoft/identity/common/internal/ui/webview/AzureActiveDirectoryWebViewClient.java
…ht, AB#3688630

The ENABLE_BROKERLESS_TELEMETRY_JS_API_FOR_AUTHUX flight added earlier in this PR
could never have been turned on in production, so the feature would have shipped
as dead code.

CommonFlightsManager only returns real ECS values in a process that called
initializeCommonFlightsManager, and the broker is its ONLY caller -- neither
oneauth nor msal ever does. The brokered flow works because its WebView runs
inside the broker's :auth process; the non-brokered flow runs in the host
application's own process, where the manager falls back to
DefaultValueFlightsProvider and every flight resolves to its compiled-in default.
The brokerless flight was default-off, so it would have read false forever, in
every host, regardless of how it was ramped.

Exposure now follows the onboarding seed instead: the telemetry-only bridge is
registered only when the request carries an onboarding telemetry recorder. This
is a better gate than the flight was even setting the ECS problem aside --

  - it is self-limiting: the bridge exists only to append to the onboarding blob,
    so without a recorder it could have no effect anyway;
  - plain MSAL clients never seed onboarding telemetry and are therefore never
    exposed, which was the widening the two-flight split existed to prevent;
  - the off switch becomes "stop seeding", a decision the caller already owns per
    request, in a process where it actually takes effect.

The brokered path is untouched: inside :auth the full bridge (which includes the
number-match device store) stays behind ENABLE_JS_API_FOR_AUTHUX, which is real
there. The host allow-list still gates both.

The recorder is available in time: WebViewAuthorizationFragment attaches it
(L393) before both initializeAuthUxJavaScriptApi (L399) and launchWebView (L400),
and hasOnboardingTelemetryRecorder() is read per navigation rather than captured,
so a recorder attached or cleared later is honoured.

Tests: the four flight-based gate tests become five recorder-based ones, adding
one for the gate closing again when the recorder is cleared. Revert-tested --
forcing the gate open fails "the broker's flight must not expose a bridge outside
the :auth process" and "the gate must be re-evaluated, not captured at first
navigation". 212 green.

Note this is exactly what the local E2E run could not catch: it flipped the
flight default to true in the build under test, which masked the deployment
problem while validating the code path.

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#3688630 to an Azure Boards work item.

Zhipan Wang and others added 3 commits August 11, 2026 10:50
One entry per PR across the stack, matching the repo convention (89 of 90
released entries are single-entry; median ~101 chars). The #3209 line drops from
562 to 224 characters and now leads with what a reader needs -- non-brokered
flows get a telemetry-only bridge, gated on the onboarding seed -- leaving the
rationale to the PR description.

Also de-duplicated #3201 and #3204: merging each consolidation up the stack
APPENDED the new line rather than replacing the old ones, so the block had to be
rebuilt canonically. Worth remembering -- a changelog edit merged upward is not
idempotent, and the count has to be re-censused after every merge.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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