Skip to content

feat: macOS brokered authentication support#452

Merged
dggsax merged 16 commits intoAzureAD:user/danigon/yeehawfrom
dggsax:feature/macos-brokered-auth
Apr 23, 2026
Merged

feat: macOS brokered authentication support#452
dggsax merged 16 commits intoAzureAD:user/danigon/yeehawfrom
dggsax:feature/macos-brokered-auth

Conversation

@dggsax
Copy link
Copy Markdown
Contributor

@dggsax dggsax commented Apr 8, 2026

Summary

Adds macOS brokered authentication support via MSAL.NET, NativeInterop, and the macOS Enterprise SSO Extension (deployed through Company Portal).

What this does

  • Broker auth on macOS — opt-in via --mode broker. Supports interactive broker (first run) and silent broker (subsequent runs via MSAL cache).
  • Company Portal version gating — requires CP >= 5.2603.0. Warning logged with CP path if version is insufficient, then falls through to next auth flow.
  • Main thread dispatch — broker interactive calls are dispatched to the main thread via MacMainThreadScheduler, as required by MSAL on macOS.
  • Browser fallback — when both --mode broker --mode web are specified, falls back to browser auth if broker fails.
  • NuGet upgrades — MSAL 4.65.0 → 4.83.1, added NativeInterop v0.20.3.

Why broker is opt-in on macOS (not the default)

On Windows, the default auth mode is Broker | Web — broker is tried first, then web as fallback. On macOS, the default remains Web only, with broker opt-in via --mode broker.

This is intentional because apps configured with broker-required Conditional Access policies (e.g., token protection / compliant device, error 530084) will hang indefinitely if web auth is attempted: the browser shows an error page about needing a compliant device but never redirects back to localhost, so MSAL's HTTP listener waits forever. Making broker the default would mean that if Company Portal is unavailable and the flow falls through to web, the CLI hangs with no way to recover — a worse experience than a clear error.

Since --mode is repeatable (--mode broker --mode web tries both in order), users and tooling can explicitly opt into the desired combination. This matches the existing pattern where --mode broker on Windows Server silently skips broker (no WAM) and falls through.

Key design decisions

  1. Broker is opt-in on macOS (--mode broker) — see above for rationale.
  2. Broker silently skipped when unavailable — matches the existing Windows Server pattern. Warning logged, then falls through to next flow (Web, DeviceCode, etc.) instead of throwing.
  3. No OperatingSystemAccount on macOS — this Windows-only concept does not work on macOS. On macOS, TryToGetCachedAccountAsync resolves accounts from the MSAL Keychain cache for silent auth. If multiple accounts are cached (ambiguous), interactive auth is triggered — the safe default for a CLI tool.
  4. SSO Extension check removedapp-sso -l returned empty on test devices yet broker worked fine. The check and associated code were removed entirely as unnecessary.

Testing

  • ✅ All 364 unit tests pass (95 AdoPat + 109 AzureAuth + 160 MSALWrapper)
  • ✅ Functional end-to-end testing verified with Work IQ 3P Graph app registration
    • Interactive broker → account picker → token acquired in ~3.6s
    • Silent broker → cached account → token acquired in ~1s
  • ✅ New unit tests: BrokerMacOSTest (5 tests)
  • ✅ Functional test script at bin/mac/test-macos-broker.sh (6 tests)

Files changed

New files:

  • src/MSALWrapper.Test/AuthFlow/BrokerMacOSTest.cs — 5 macOS broker unit tests
  • bin/mac/test-macos-broker.sh — functional test script

Modified files:

  • src/MSALWrapper/MSALWrapper.csproj — NuGet upgrades
  • src/MSALWrapper/AuthFlow/Broker.cs — major rewrite for cross-platform
  • src/MSALWrapper/AuthFlow/AuthFlowFactory.cs — broker availability gating with fallthrough
  • src/MSALWrapper/PlatformUtils.cs — CP version detection, macOS checks
  • src/MSALWrapper/AuthMode.cs — broker opt-in on non-Windows (Default = Web)
  • src/MSALWrapper/Constants.cs — macOS broker redirect URI
  • src/AzureAuth/Program.cs — main thread scheduler pattern
  • src/AzureAuth/AuthModeExtensions.cs — preserve broker for silent auth in non-interactive mode
  • src/AzureAuth/Commands/CommandAad.cs--mode broker option, fixed help text ([default: web] on non-Windows)
  • docs/usage.md — macOS broker prerequisites and redirect URI config
  • README.md — feature matrix updated

Prerequisites for broker auth

  • Company Portal >= 5.2603.0 installed
  • Device MDM-compliant
  • App registration configured with redirect URI: msauth.com.msauth.unsignedapp://auth

dggsax and others added 11 commits April 6, 2026 16:30
Add support for macOS Enterprise SSO Extension brokered authentication,
bringing feature parity with Windows WAM broker support.

Key changes:
- Upgrade MSAL 4.65.0 → 4.83.1, add NativeInterop v0.20.3
- Extend Broker auth flow with macOS-specific PCA config, account
  resolution, and browser fallback
- Add DefaultAccountStore to persist account username for silent auth
  (OperatingSystemAccount not supported on macOS)
- Add MacMainThreadScheduler message loop in Program.cs
- Extend AuthMode enum and CLI parsing to support broker on macOS
- Update docs with macOS broker prerequisites and redirect URI config

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skip macOS broker auth if Company Portal is not installed or is below
version 2603 (which added redirect_uri validation fix for unsigned apps).
Falls back to web auth transparently in those cases.

- Add IsMacOSBrokerAvailable() to IPlatformUtils/PlatformUtils
- AuthFlowFactory uses IsMacOSBrokerAvailable() as gatekeeper
- Broker.cs still uses IsMacOS() for runtime behavior (fallback, persist)
- Reads CP version from Info.plist via 'defaults read'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
On macOS, broker is no longer included in default auth modes.
Users must explicitly pass '--mode broker' to use it. If broker
is requested but Company Portal >= 5.2603.0 is not installed,
AuthFlowFactory throws a clear InvalidOperationException instead
of silently falling through to web auth (which hangs for apps
with broker-required CA policies like token protection).

- AuthMode.Default on non-Windows: Broker|Web -> Web
- AuthFlowFactory: explicit error when broker requested but CP unavailable
- New test: BrokerRequested_Mac_CP_Unavailable_Throws

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Include CP path in broker unavailable error message
- Add trace-level logging: CP path, raw version output, stderr,
  parsed version parts (major/release/build)
- Expose CompanyPortalAppPath as public const for error messages
- Update test script to reflect broker-opt-in behavior:
  new tests for broker+web combined, trace diagnostics, reordered

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The macOS broker requires AcquireTokenInteractive to run on the main
thread. Program.cs starts the MacMainThreadScheduler message loop on
main and dispatches CLI work to Task.Run, but the broker interactive
calls were still executing on the background thread.

Now GetTokenInteractive/WithClaims dispatch through
MacMainThreadScheduler.RunOnMainThreadAsync when running on macOS
with the scheduler active (IsRunning check prevents deadlock in tests).

Also: improved CP diagnostics trace logging and included CP path in
the broker unavailable error message.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Check 'app-sso -l' for registered Enterprise SSO Extensions before
attempting broker auth. If no extensions are registered (MDM profile
not applied), gives a clear error instead of a cryptic broker failure.

Designed for easy revert:
- Set AZUREAUTH_SKIP_SSO_CHECK=1 to bypass the check entirely
- If app-sso fails or isn't available, assumes broker may work (non-fatal)
- The check is a single method call in CheckMacOSBrokerAvailable()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move test-macos-broker.sh and swap-cp.sh from repo root to bin/mac/
alongside existing macOS build scripts. Update REPO_ROOT to resolve
two levels up from the new location.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add AZUREAUTH_TEST_VERBOSITY env var (default: debug) to control
  log level across all tests (Test 3 always uses trace)
- Comment out SSO Extension check (confirmed unnecessary for broker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Match the existing Windows pattern where broker is silently skipped
on unsupported platforms (e.g., Windows Server). On macOS, if broker
is requested but Company Portal is insufficient, log a warning and
continue to the next flow (Web, DeviceCode, etc.) instead of throwing.

This means '--mode broker --mode web' will fall through to web when
CP is unavailable, and '--mode broker' alone will try CachedAuth only.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Web auth hangs for this broker-required app (token protection CA
policy), so comment out Tests 2 and 5. Add cache clear + broker
interactive re-prompt cycle (Tests 6-8) to verify full broker
lifecycle: authenticate → clear → re-authenticate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread bin/mac/swap-cp.sh Outdated
Comment thread bin/mac/test-macos-broker.sh Outdated
Comment thread src/AzureAuth.Test/AuthModeExtensionsTest.cs
Comment thread src/MSALWrapper/PlatformUtils.cs Outdated
Comment thread src/AzureAuth/Commands/CommandAad.cs Outdated
- Delete swap-cp.sh (reviewer: no need for checked-in CP swap utility)
- Remove commented-out web mode tests from test script (reviewer: keep
  checked-in tests clean)
- Add [Platform] attributes to non-Windows AuthModeExtensions tests
  (reviewer: make OS-specificity explicit)
- Fix help text: non-Windows default is 'web' not 'broker, then web'
  (reviewer: broker may confuse Linux users; --mode is repeatable so
  --mode broker explicitly opts in)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dggsax dggsax marked this pull request as ready for review April 22, 2026 02:51
@dggsax dggsax requested a review from a team as a code owner April 22, 2026 02:51
dggsax and others added 4 commits April 21, 2026 19:52
- Broker is opt-in via --mode broker, not automatic
- Removed SSO Extension prerequisite (confirmed unnecessary)
- Added CP version requirement (>= 5.2603.0)
- Added example CLI invocations
- Clarified fallthrough behavior when broker unavailable

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The IsSSOExtensionRegistered method and its TODO were confirmed unnecessary
during testing — broker auth works without SSO Extension registration
showing in app-sso output. Removes dead code and the AZUREAUTH_SKIP_SSO_CHECK
env var that was only needed for this check.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Changed PersistDefaultAccount from sync .Wait()/.Result to async/await.
The previous pattern (calling task.Wait() on an async method inside an
async context) is a classic .NET deadlock risk, especially dangerous
with the macOS MacMainThreadScheduler SynchronizationContext.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DefaultAccountStore persisted account usernames (PII) to plaintext JSON
files in ~/.azureauth/ with world-readable permissions. This was only
needed to disambiguate multi-account scenarios (TryToGetCachedAccountAsync
returns null when >1 accounts are cached).

Without it, multi-account users get an extra broker prompt — which is
the safe default for a CLI tool and avoids writing PII to disk.

Changes:
- Delete DefaultAccountStore.cs and DefaultAccountStoreTest.cs
- Simplify Broker.ResolveAccountAsync (no more persisted username lookup)
- Remove PersistDefaultAccountAsync from Broker.cs
- Simplify BrokerMacOSTest.cs (remove store setup/teardown)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dggsax
Copy link
Copy Markdown
Contributor Author

dggsax commented Apr 22, 2026

Re: this comment — good callout. Linux is safe here: AuthFlowFactory (line 63-84) only adds the Broker flow when IsWindows10Or11() or IsMacOS() && IsMacOSBrokerAvailable(). On Linux, --mode broker is silently skipped and falls through to the next flow (web, devicecode) — same behavior as Windows Server where broker is unavailable.

The help text already shows [default: web] on the #else branch (both macOS and Linux), and broker is opt-in only. A Linux user passing --mode broker would just get a no-op skip, which is consistent with the existing pattern.

@dggsax dggsax enabled auto-merge (squash) April 23, 2026 15:13
Copy link
Copy Markdown
Contributor

@ChristopherJMiller ChristopherJMiller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@dggsax dggsax changed the base branch from main to user/danigon/yeehaw April 23, 2026 16:21
@dggsax dggsax merged commit 09894cf into AzureAD:user/danigon/yeehaw Apr 23, 2026
1 check passed
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.

3 participants