Skip to content

feat: macOS brokered authentication support#453

Merged
dggsax merged 4 commits intomainfrom
user/danigon/yeehaw
Apr 24, 2026
Merged

feat: macOS brokered authentication support#453
dggsax merged 4 commits intomainfrom
user/danigon/yeehaw

Conversation

@dggsax
Copy link
Copy Markdown
Contributor

@dggsax dggsax commented Apr 23, 2026

  • feat: add macOS brokered authentication support

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
  • feat: add Company Portal version check before broker auth

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'
  • feat: make broker opt-in on macOS, error on missing CP

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
  • improve: CP diagnostics logging and error messages
  • 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
  • fix: dispatch broker interactive calls to main thread on macOS

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.

  • feat: add SSO Extension registration pre-flight check

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()
  • chore: move macOS test scripts to bin/mac/

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.

  • improve: configurable verbosity in test script, disable SSO check
  • 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)
  • docs: add usage examples to test script header

  • fix: broker falls through to next auth flow when unavailable on macOS

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.

  • test: comment out web mode tests, add broker re-prompt after clear

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.

  • fix: address PR review comments
  • 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)
  • docs: update usage.md to reflect broker opt-in on macOS
  • 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
  • Remove unused SSO Extension check code

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.

  • Fix async deadlock in PersistDefaultAccount

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.

  • Remove DefaultAccountStore — rely on MSAL cache instead

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)

* feat: add macOS brokered authentication support

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>

* feat: add Company Portal version check before broker auth

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>

* feat: make broker opt-in on macOS, error on missing CP

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>

* improve: CP diagnostics logging and error messages

- 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>

* fix: dispatch broker interactive calls to main thread on macOS

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>

* feat: add SSO Extension registration pre-flight check

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>

* chore: move macOS test scripts to bin/mac/

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>

* improve: configurable verbosity in test script, disable SSO check

- 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>

* docs: add usage examples to test script header

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

* fix: broker falls through to next auth flow when unavailable on macOS

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>

* test: comment out web mode tests, add broker re-prompt after clear

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>

* fix: address PR review comments

- 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>

* docs: update usage.md to reflect broker opt-in on macOS

- 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>

* Remove unused SSO Extension check code

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>

* Fix async deadlock in PersistDefaultAccount

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>

* Remove DefaultAccountStore — rely on MSAL cache instead

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>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dggsax dggsax requested a review from a team as a code owner April 23, 2026 16:21
@dggsax dggsax enabled auto-merge April 23, 2026 16:22
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

…n Windows tests

The AuthFlowFactory now checks IsMacOSBrokerAvailable() and IsMacOS() in
the broker code path. Windows tests with strict mocks that set
IsWindows10Or11(false) need these additional setups since the || and
else-if branches evaluate the macOS checks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/AzureAuth/Commands/CommandAad.cs
Comment thread src/AzureAuth/Program.cs
Comment thread src/MSALWrapper/AuthFlow/AuthFlowFactory.cs Outdated
Comment thread src/MSALWrapper/AuthFlow/Broker.cs Outdated
- AuthFlowFactory: collapse nested broker if/else into single condition
  (IsWindows10Or11 || IsMacOSBrokerAvailable) with one flows.Add call
- Broker.cs: remove FallbackToBrowserAuthAsync — browser fallback is
  already handled by AuthFlowExecutor iterating to the next flow (Web)
- Broker.cs: remove broad catch(Exception) for macOS — let MSAL
  exceptions propagate to AuthFlowBase which handles them properly
- Update AuthFlowFactoryTest to match simplified factory logic

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/MSALWrapper/AuthMode.cs
Copy link
Copy Markdown
Collaborator

@kyle-rader-msft kyle-rader-msft left a comment

Choose a reason for hiding this comment

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

Also please update the CHANGELOG.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dggsax dggsax force-pushed the user/danigon/yeehaw branch from d74a4c9 to b458e26 Compare April 24, 2026 18:58
@dggsax dggsax dismissed kyle-rader-msft’s stale review April 24, 2026 19:41

I have addressed Kyle's feedback and concerns, he will be out for a few hours, so we will get this in and if there are any other concerns I will open follow-up items and tackle them immediately.

@dggsax dggsax merged commit 8ef1b8b into main Apr 24, 2026
19 checks passed
@dggsax dggsax deleted the user/danigon/yeehaw branch April 24, 2026 19:43
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.

4 participants