Skip to content

Commit 8ef1b8b

Browse files
authored
Merge pull request #453 from AzureAD/user/danigon/yeehaw
feat: macOS brokered authentication support
2 parents e234a1c + b458e26 commit 8ef1b8b

21 files changed

Lines changed: 799 additions & 47 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Added support for macOS brokered authentication via Enterprise SSO Extension (opt-in with `--mode broker`)
10+
11+
### Changed
12+
- Upgrade MSAL from `4.65.0` to `4.83.1`
13+
- Added `Microsoft.Identity.Client.NativeInterop` v0.20.3
814

915
## [0.9.5] - 2026-02-24
1016
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The CLI is designed for authenticating and returning an access token for public
2020
| Operating System | Integrated Windows Auth | Auth Broker Integration | Web Auth Flow | Device Code Flow | Token Caching | Multi-Account Support |
2121
| ------------------------------------------ | ----------------------- | ----------------------- | ------------------------ | ---------------- | ------------- | ------------------------------- |
2222
| Windows |||||| ⚠️ `--domain` account filtering |
23-
| OSX (MacOS) | N/A | ⚠️ via Web Browser |||| ⚠️ `--domain` account filtering |
23+
| OSX (macOS) | N/A | via Enterprise SSO |||| ⚠️ `--domain` account filtering |
2424
| Ubuntu (Linux) | N/A | ⚠️ via Edge | ⚠️ in GUI environments ||| ⚠️ `--domain` account filtering |
2525

2626
<br/>

bin/mac/test-macos-broker.sh

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Functional test script for macOS brokered auth changes
5+
# Tests AzureAuth CLI with Work IQ's 3P Graph app registration
6+
#
7+
# Usage:
8+
# ./bin/mac/test-macos-broker.sh # defaults (debug verbosity, 120s timeout)
9+
# AZUREAUTH_TEST_VERBOSITY=info ./bin/mac/test-macos-broker.sh # less noise
10+
# AZUREAUTH_TEST_VERBOSITY=trace ./bin/mac/test-macos-broker.sh # max detail
11+
# AZUREAUTH_TEST_TIMEOUT=60 ./bin/mac/test-macos-broker.sh # shorter timeout
12+
#
13+
# Each interactive test has a timeout (default 120s). If azureauth hangs
14+
# waiting for browser/broker, it will be killed and you can choose to
15+
# mark it as SKIP or FAIL, then the script continues to the next test.
16+
#
17+
# You can also Ctrl+C during any individual test — the script traps it
18+
# and moves on.
19+
20+
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
21+
AZUREAUTH="$REPO_ROOT/src/AzureAuth/bin/Debug/net8.0/azureauth"
22+
CLIENT="ba081686-5d24-4bc6-a0d6-d034ecffed87"
23+
TENANT="common"
24+
RESOURCE="https://graph.microsoft.com"
25+
TIMEOUT="${AZUREAUTH_TEST_TIMEOUT:-120}"
26+
VERBOSITY="${AZUREAUTH_TEST_VERBOSITY:-debug}" # debug, trace, info, warn
27+
28+
PASS=0
29+
FAIL=0
30+
SKIP=0
31+
32+
header() {
33+
echo ""
34+
echo "========================================"
35+
echo " $1"
36+
echo "========================================"
37+
}
38+
39+
result() {
40+
local name="$1" exit_code="$2" expected="$3"
41+
if [ "$exit_code" -eq "$expected" ]; then
42+
echo "✅ PASS: $name (exit=$exit_code, expected=$expected)"
43+
PASS=$((PASS + 1))
44+
else
45+
echo "❌ FAIL: $name (exit=$exit_code, expected=$expected)"
46+
FAIL=$((FAIL + 1))
47+
fi
48+
}
49+
50+
# Run azureauth with a timeout. On Ctrl+C or timeout, offer skip/fail.
51+
# Usage: run_test "Test Name" expected_exit args...
52+
run_test() {
53+
local test_name="$1" expected_exit="$2"
54+
shift 2
55+
56+
echo ""
57+
echo "→ Running: azureauth $*"
58+
echo " (timeout: ${TIMEOUT}s — Ctrl+C to abort this test)"
59+
echo ""
60+
61+
local interrupted=false
62+
local pid=""
63+
64+
# Trap SIGINT (Ctrl+C) for this test only
65+
trap 'interrupted=true; [ -n "$pid" ] && kill "$pid" 2>/dev/null' INT
66+
67+
set +e
68+
# Run azureauth in background, then wait with timeout
69+
"$AZUREAUTH" "$@" 2>&1 &
70+
pid=$!
71+
72+
# Wait up to TIMEOUT seconds for the process to finish
73+
local elapsed=0
74+
while [ "$elapsed" -lt "$TIMEOUT" ] && kill -0 "$pid" 2>/dev/null; do
75+
sleep 1
76+
((elapsed++))
77+
if [ "$interrupted" = true ]; then
78+
break
79+
fi
80+
done
81+
82+
# If still running after timeout, kill it
83+
if kill -0 "$pid" 2>/dev/null; then
84+
kill "$pid" 2>/dev/null
85+
wait "$pid" 2>/dev/null
86+
if [ "$interrupted" = false ]; then
87+
echo ""
88+
echo "⏱️ Test timed out after ${TIMEOUT}s"
89+
fi
90+
interrupted=true
91+
EXIT_CODE=124
92+
else
93+
wait "$pid"
94+
EXIT_CODE=$?
95+
fi
96+
pid=""
97+
set -e
98+
99+
# Restore default SIGINT behavior
100+
trap - INT
101+
102+
if [ "$interrupted" = true ]; then
103+
echo ""
104+
echo "Test was interrupted/timed out."
105+
read -p "Mark as [s]kip or [f]ail? (s/f, default=s): " choice </dev/tty || choice="s"
106+
choice="${choice:-s}"
107+
if [[ "$choice" =~ ^[fF] ]]; then
108+
echo "❌ FAIL: $test_name (interrupted, marked as fail)"
109+
FAIL=$((FAIL + 1))
110+
else
111+
echo "⏭️ SKIP: $test_name (interrupted)"
112+
SKIP=$((SKIP + 1))
113+
fi
114+
return
115+
fi
116+
117+
result "$test_name" "$EXIT_CODE" "$expected_exit"
118+
}
119+
120+
# ── Step 0: Build ──────────────────────────────────────────────
121+
header "Step 0: Building AzureAuth"
122+
dotnet build "$REPO_ROOT/AzureAuth.sln" \
123+
--no-restore -c Debug -v quiet 2>&1 | tail -3
124+
125+
if [ ! -x "$AZUREAUTH" ]; then
126+
echo "❌ Build failed — binary not found at $AZUREAUTH"
127+
exit 1
128+
fi
129+
echo "✅ Binary ready: $AZUREAUTH"
130+
echo " Version: $("$AZUREAUTH" --version)"
131+
132+
# ── Step 0.5: CP version info ─────────────────────────────────
133+
header "Step 0.5: Company Portal status"
134+
CP_PLIST="/Applications/Company Portal.app/Contents/Info.plist"
135+
if [ -f "$CP_PLIST" ]; then
136+
CP_VERSION=$(defaults read "/Applications/Company Portal.app/Contents/Info" CFBundleShortVersionString 2>/dev/null || echo "unknown")
137+
echo "Company Portal version: $CP_VERSION"
138+
# Extract release number (middle segment of 5.RRRR.B)
139+
RELEASE=$(echo "$CP_VERSION" | awk -F. '{print $2}')
140+
if [ "$RELEASE" -ge 2603 ] 2>/dev/null; then
141+
echo "⚡ CP >= 2603 — broker tests WILL attempt real broker auth"
142+
BROKER_AVAILABLE=true
143+
else
144+
echo "⚠️ CP $CP_VERSION (release $RELEASE) < 2603 — broker will be gated off"
145+
BROKER_AVAILABLE=false
146+
fi
147+
else
148+
echo "⚠️ Company Portal not installed — broker will be gated off"
149+
BROKER_AVAILABLE=false
150+
fi
151+
152+
# ── Test 1: Broker-only mode (opt-in) ────────────────────────
153+
header "Test 1: Broker-only mode (--mode broker)"
154+
if [ "$BROKER_AVAILABLE" = true ]; then
155+
echo "CP >= 2603 detected — this will attempt real broker auth"
156+
echo "Expect: broker interactive prompt via Enterprise SSO Extension"
157+
EXPECTED_EXIT=0
158+
else
159+
echo "CP < 2603 or not installed — expecting clear error about Company Portal"
160+
echo "Expect: InvalidOperationException with CP version/path info"
161+
EXPECTED_EXIT=1
162+
fi
163+
run_test "Broker-only (opt-in)" "$EXPECTED_EXIT" \
164+
aad --client "$CLIENT" --tenant "$TENANT" \
165+
--resource "$RESOURCE" \
166+
--mode broker --output json --verbosity "$VERBOSITY"
167+
168+
# ── Test 2: Trace verbosity — verify CP diagnostics in logs ───
169+
header "Test 2: Trace verbosity — CP diagnostic logging"
170+
echo "Running with --verbosity trace to verify Company Portal metadata is logged."
171+
echo "🔍 Watch for: CP path, raw version output, release parsing"
172+
if [ "$BROKER_AVAILABLE" = true ]; then
173+
EXPECTED_EXIT=0
174+
else
175+
EXPECTED_EXIT=1
176+
fi
177+
run_test "Trace CP diagnostics" "$EXPECTED_EXIT" \
178+
aad --client "$CLIENT" --tenant "$TENANT" \
179+
--resource "$RESOURCE" \
180+
--mode broker --output json --verbosity trace
181+
182+
# ── Test 3: Clear cache ───────────────────────────────────────
183+
header "Test 3: Clear token cache"
184+
run_test "Cache clear" 0 \
185+
aad --client "$CLIENT" --tenant "$TENANT" \
186+
--resource "$RESOURCE" \
187+
--clear --verbosity "$VERBOSITY"
188+
189+
# ── Test 4: Clear cache (before re-testing broker interactive) ─
190+
header "Test 4: Clear token cache"
191+
run_test "Cache clear (pre-broker retest)" 0 \
192+
aad --client "$CLIENT" --tenant "$TENANT" \
193+
--resource "$RESOURCE" \
194+
--clear --verbosity "$VERBOSITY"
195+
196+
# ── Test 5: Broker interactive again (after cache clear) ──────
197+
header "Test 5: Broker interactive (after cache clear)"
198+
if [ "$BROKER_AVAILABLE" = true ]; then
199+
echo "Cache was just cleared — broker must prompt interactively again"
200+
echo "Expect: broker account picker / SSO Extension prompt"
201+
EXPECTED_EXIT=0
202+
else
203+
echo "CP unavailable — broker skipped, CachedAuth only (will fail)"
204+
EXPECTED_EXIT=1
205+
fi
206+
run_test "Broker interactive (re-prompt)" "$EXPECTED_EXIT" \
207+
aad --client "$CLIENT" --tenant "$TENANT" \
208+
--resource "$RESOURCE" \
209+
--mode broker --output json --verbosity "$VERBOSITY"
210+
211+
# ── Test 6: Final cache clear ─────────────────────────────────
212+
header "Test 6: Final cache clear"
213+
run_test "Cache clear (final)" 0 \
214+
aad --client "$CLIENT" --tenant "$TENANT" \
215+
--resource "$RESOURCE" \
216+
--clear --verbosity "$VERBOSITY"
217+
218+
# ── Summary ────────────────────────────────────────────────────
219+
header "Results"
220+
echo "✅ Passed: $PASS"
221+
echo "⏭️ Skipped: $SKIP"
222+
echo "❌ Failed: $FAIL"
223+
echo ""
224+
echo "Broker available: $BROKER_AVAILABLE"
225+
if [ "$BROKER_AVAILABLE" = false ]; then
226+
echo "ℹ️ To test actual broker auth, upgrade Company Portal to >= 5.2603.x"
227+
fi
228+
echo ""
229+
echo "Tip: Set AZUREAUTH_TEST_TIMEOUT=60 to change the per-test timeout (default: 30s)"
230+
echo ""
231+
232+
if [ "$FAIL" -gt 0 ]; then
233+
exit 1
234+
fi

docs/usage.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# Usage
22
AzureAuth is a generic Azure credential provider. It currently supports the following modes of [public client authentication](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-applications) (i.e., authenticating a human user.)
3-
* [IWA (Integrated Windows Authentication)](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-integrated-windows-authentication)
4-
* [WAM (Web Account Manager)](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam) (Windows only brokered authentication)
3+
* [IWA (Integrated Windows Authentication)](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-integrated-windows-authentication) (Windows only)
4+
* [Brokered Authentication](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam) (Windows via WAM, macOS via Enterprise SSO Extension)
55
* [Embedded Web View](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-web-browsers) (Windows Only)
6-
* [System Web Browser](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-web-browsers) (Used on OSX in-place of Embedded Web View)
6+
* [System Web Browser](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-web-browsers) (Used on macOS in-place of Embedded Web View)
77
* [Device Code Flow](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Device-Code-Flow) (All platforms, terminal interface only).
88

99
## `aad` subcommand
@@ -23,6 +23,28 @@ The `azureauth aad` subcommand is a "pass-through" for using [MSAL.NET](https://
2323
2424
![WAM redirect URI configuration](wam-config.png "A screenshot of a WAM URI being configured as a custom redirect URI.")
2525
26+
2b. Configure redirect URIs for the **macOS Enterprise SSO Extension** (the macOS broker)
27+
1. Select the **Authentication** blade.
28+
2. Under Platform configurations, find **Mobile and desktop applications**.
29+
3. Select **Add URI** and enter
30+
```
31+
msauth.com.msauth.unsignedapp://auth
32+
```
33+
4. Select **Save**.
34+
35+
> **Note:** macOS brokered authentication is **opt-in** via `--mode broker` and requires:
36+
> - **Company Portal** version 5.2603.0 or later installed on the device
37+
> - Device is **MDM-compliant**
38+
>
39+
> If Company Portal is unavailable or below the minimum version, broker is
40+
> silently skipped and the next auth flow in the chain is attempted.
41+
>
42+
> Example usage:
43+
> ```
44+
> azureauth aad --client <clientID> --resource <resourceID> --tenant <tenantID> --mode broker
45+
> azureauth aad --client <clientID> --resource <resourceID> --tenant <tenantID> --mode broker --mode web
46+
> ```
47+
2648
3. Configure redirect URIs for the **system web browser**
2749
1. Select the **Authentication** blade.
2850
2. Under Platform configurations, find **Mobile and desktop applications**

src/AdoPat/AdoPat.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<AdditionalFiles Include="..\stylecop\stylecop.json" Link="stylecop.json" />
1111
<Compile Include="..\stylecop\GlobalSuppressions.cs" Link="GlobalSuppressions.cs" />
1212
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
13-
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.65.0" />
13+
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.83.1" />
1414
<PackageReference Include="Microsoft.VisualStudio.Services.Client" Version="19.239.0-preview" />
1515
<PackageReference Include="System.Data.SqlClient" Version="4.8.6" />
1616
<!-- Transitive dependencies of Microsoft.VisualStudio.Services.Client temporarily pinned for security reasons. -->

src/AzureAuth.Test/AuthModeExtensionsTest.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,16 @@ public void Filterinteraction_Interactive_Auth_Disabled(string envVar)
5858
this.logTarget.Logs.Should().ContainInOrder("Interactive authentication is disabled.", "Only Integrated Windows Authentication will be attempted.");
5959
}
6060
#else
61+
[Test]
62+
[Platform("MacOsX,Linux")]
6163
public void CombinedAuthMode_Allowed()
6264
{
6365
// Arrange
6466
this.envMock.Setup(e => e.Get(EnvVars.NoUser)).Returns(string.Empty);
6567
this.envMock.Setup(e => e.Get("Corext_NonInteractive")).Returns(string.Empty);
6668

67-
var subject = new[] { AuthMode.Web, AuthMode.DeviceCode };
69+
// Default on macOS is Web only (broker is opt-in).
70+
var subject = new[] { AuthMode.Web };
6871

6972
// Act + Assert
7073
subject.Combine().PreventInteractionIfNeeded(this.envMock.Object, this.logger).Should().Be(AuthMode.Default);
@@ -73,7 +76,8 @@ public void CombinedAuthMode_Allowed()
7376

7477
[TestCase("AZUREAUTH_NO_USER")]
7578
[TestCase("Corext_NonInteractive")]
76-
public void Filterinteraction_Interactive_Auth_Disabled(string envVar)
79+
[Platform("MacOsX,Linux")]
80+
public void Filterinteraction_Interactive_Auth_Disabled_NoBroker(string envVar)
7781
{
7882
// Arrange
7983
this.envMock.Setup(e => e.Get(envVar)).Returns("1");
@@ -83,6 +87,20 @@ public void Filterinteraction_Interactive_Auth_Disabled(string envVar)
8387
subject.Combine().PreventInteractionIfNeeded(this.envMock.Object, this.logger).Should().Be(AuthMode.None);
8488
this.logTarget.Logs.Should().ContainInOrder("Interactive authentication is disabled.");
8589
}
90+
91+
[TestCase("AZUREAUTH_NO_USER")]
92+
[TestCase("Corext_NonInteractive")]
93+
[Platform("MacOsX,Linux")]
94+
public void Filterinteraction_Interactive_Auth_Disabled_WithBroker(string envVar)
95+
{
96+
// Arrange
97+
this.envMock.Setup(e => e.Get(envVar)).Returns("1");
98+
var subject = new[] { AuthMode.Broker, AuthMode.Web, AuthMode.DeviceCode };
99+
100+
// Act + Assert
101+
subject.Combine().PreventInteractionIfNeeded(this.envMock.Object, this.logger).Should().Be(AuthMode.Broker);
102+
this.logTarget.Logs.Should().ContainInOrder("Interactive authentication is disabled.", "Only Broker silent authentication will be attempted.");
103+
}
86104
#endif
87105
}
88106
}

src/AzureAuth/AuthModeExtensions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ public static AuthMode PreventInteractionIfNeeded(this AuthMode authMode, IEnv e
2828
logger.LogWarning($"Only Integrated Windows Authentication will be attempted.");
2929
return AuthMode.IWA;
3030
#else
31-
return 0;
31+
// Keep broker for silent auth on macOS, where the broker can resolve tokens silently.
32+
var silentMode = authMode & AuthMode.Broker;
33+
if (silentMode != 0)
34+
{
35+
logger.LogWarning($"Only Broker silent authentication will be attempted.");
36+
}
37+
38+
return silentMode;
3239
#endif
3340
}
3441

src/AzureAuth/Commands/CommandAad.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public class CommandAad
9090
#if PlatformWindows
9191
public const string AuthModeAllowedValues = "all, iwa, broker, web, devicecode";
9292
#else
93-
public const string AuthModeAllowedValues = "all, web, devicecode";
93+
public const string AuthModeAllowedValues = "all, broker, web, devicecode";
9494
#endif
9595

9696
private const string ResourceOption = "--resource";

0 commit comments

Comments
 (0)