From 72973540a72ba7b09481a12e8b39f8b32d06042f Mon Sep 17 00:00:00 2001 From: Jonathan Behrens Date: Wed, 22 Jul 2026 17:35:07 -0700 Subject: [PATCH] Add Linux brokered authentication via Microsoft Identity Broker --- CHANGELOG.md | 1 + .../AuthFlow/AuthFlowFactoryTest.cs | 61 +++++++++++++++++++ src/MSALWrapper.Test/AuthFlow/BrokerTest.cs | 1 + src/MSALWrapper/AuthFlow/AuthFlowFactory.cs | 11 +++- src/MSALWrapper/AuthFlow/Broker.cs | 15 +++-- src/MSALWrapper/AuthMode.cs | 9 +-- src/MSALWrapper/Constants.cs | 5 +- src/MSALWrapper/IPlatformUtils.cs | 14 +++++ src/MSALWrapper/PlatformUtils.cs | 45 ++++++++++++++ 9 files changed, 150 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0279c34..98ce0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Added support for macOS brokered authentication via Enterprise SSO Extension (opt-in with `--mode broker`) +- Added support for Linux brokered authentication via the Microsoft Identity Broker (opt-in with `--mode broker`) ### Changed - Upgrade MSAL from `4.65.0` to `4.83.1` diff --git a/src/MSALWrapper.Test/AuthFlow/AuthFlowFactoryTest.cs b/src/MSALWrapper.Test/AuthFlow/AuthFlowFactoryTest.cs index 1f8691f..1e32143 100644 --- a/src/MSALWrapper.Test/AuthFlow/AuthFlowFactoryTest.cs +++ b/src/MSALWrapper.Test/AuthFlow/AuthFlowFactoryTest.cs @@ -130,7 +130,9 @@ public void Windows_Defaults() { this.MockIsWindows10Or11(false); this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); this.MockIsMacOS(false); + this.MockIsLinux(false); IEnumerable subject = this.Subject(AuthMode.Default); @@ -167,7 +169,9 @@ public void Windows_All() this.MockIsWindows(true); this.MockIsWindows10Or11(false); this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); this.MockIsMacOS(false); + this.MockIsLinux(false); IEnumerable subject = this.Subject(AuthMode.All); @@ -205,7 +209,9 @@ public void AllModes_Windows() this.MockIsWindows(true); this.MockIsWindows10Or11(false); this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); this.MockIsMacOS(false); + this.MockIsLinux(false); IEnumerable subject = this.Subject(AuthMode.All); @@ -291,6 +297,7 @@ public void BrokerRequested_Mac_CP_Unavailable_SkipsBroker() this.MockIsWindows10Or11(false); this.MockIsMacOS(true); this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); // Broker is silently skipped; only CachedAuth remains when no other modes are requested. IEnumerable subject = this.Subject(AuthMode.Broker); @@ -306,6 +313,7 @@ public void BrokerAndWeb_Mac_CP_Unavailable_FallsThrough() this.MockIsWindows10Or11(false); this.MockIsMacOS(true); this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); // Broker is skipped but web is still added — fall-through pattern. IEnumerable subject = this.Subject(AuthMode.Broker | AuthMode.Web); @@ -319,6 +327,49 @@ public void BrokerAndWeb_Mac_CP_Unavailable_FallsThrough() typeof(Web)); } + [Test] + [Platform("Linux")] + public void BrokerRequested_Linux_BrokerAvailable() + { + this.MockIsWindows10Or11(false); + this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(true); + + IEnumerable subject = this.Subject(AuthMode.Broker); + + subject.Should().ContainSingle().Which.Should().BeOfType(); + } + + [Test] + [Platform("Linux")] + public void BrokerRequested_Linux_BrokerUnavailable_SkipsBroker() + { + this.MockIsWindows10Or11(false); + this.MockIsMacOSBrokerAvailable(false); + this.MockIsLinuxBrokerAvailable(false); + this.MockIsMacOS(false); + this.MockIsLinux(true); + + // Broker is silently skipped; only CachedAuth remains when no other modes are requested. + IEnumerable subject = this.Subject(AuthMode.Broker); + + subject.Should().ContainSingle().Which.Should().BeOfType(); + } + + [Test] + [Platform("Linux")] + public void DefaultModes_Linux() + { + // On Linux, default mode is Web only (broker is opt-in via --mode broker). + IEnumerable subject = this.Subject(AuthMode.Default); + + subject.Should().HaveCount(2); + subject + .Select(a => a.GetType()) + .Should() + .ContainInOrder(typeof(CachedAuth), typeof(Web)); + } + private void MockIsWindows10Or11(bool value) { this.platformUtilsMock.Setup(p => p.IsWindows10Or11()).Returns(value); @@ -338,5 +389,15 @@ private void MockIsMacOSBrokerAvailable(bool value) { this.platformUtilsMock.Setup(p => p.IsMacOSBrokerAvailable()).Returns(value); } + + private void MockIsLinux(bool value) + { + this.platformUtilsMock.Setup(p => p.IsLinux()).Returns(value); + } + + private void MockIsLinuxBrokerAvailable(bool value) + { + this.platformUtilsMock.Setup(p => p.IsLinuxBrokerAvailable()).Returns(value); + } } } diff --git a/src/MSALWrapper.Test/AuthFlow/BrokerTest.cs b/src/MSALWrapper.Test/AuthFlow/BrokerTest.cs index 11a7b2c..f5b70d7 100644 --- a/src/MSALWrapper.Test/AuthFlow/BrokerTest.cs +++ b/src/MSALWrapper.Test/AuthFlow/BrokerTest.cs @@ -32,6 +32,7 @@ internal class BrokerTest : AuthFlowTestBase this.mockPlatformUtils = new Mock(MockBehavior.Strict); // Default to Windows behavior so existing tests keep working. this.mockPlatformUtils.Setup(p => p.IsMacOS()).Returns(false); + this.mockPlatformUtils.Setup(p => p.IsLinux()).Returns(false); } public AuthFlow.Broker Subject() => new AuthFlow.Broker(this.logger, this.authParameters, pcaWrapper: this.mockPca.Object, promptHint: PromptHint, platformUtils: this.mockPlatformUtils.Object); diff --git a/src/MSALWrapper/AuthFlow/AuthFlowFactory.cs b/src/MSALWrapper/AuthFlow/AuthFlowFactory.cs index 105af1b..619150b 100644 --- a/src/MSALWrapper/AuthFlow/AuthFlowFactory.cs +++ b/src/MSALWrapper/AuthFlow/AuthFlowFactory.cs @@ -43,7 +43,7 @@ public static IEnumerable Create( // already tries CachedAuth with its PCAWrapper object built using withBroker(options). // The same applies on macOS when the broker is available. // If broker is requested but unavailable, CachedAuth is still added as a first-pass attempt. - bool brokerWillRun = authMode.IsBroker() && (platformUtils.IsWindows10Or11() || platformUtils.IsMacOSBrokerAvailable()); + bool brokerWillRun = authMode.IsBroker() && (platformUtils.IsWindows10Or11() || platformUtils.IsMacOSBrokerAvailable() || platformUtils.IsLinuxBrokerAvailable()); if (!brokerWillRun) { flows.Add(new CachedAuth(logger, authParams, preferredDomain, pcaWrapper)); @@ -62,7 +62,7 @@ public static IEnumerable Create( // https://github.com/AzureAD/microsoft-authentication-cli/issues/55 if (authMode.IsBroker()) { - if (platformUtils.IsWindows10Or11() || platformUtils.IsMacOSBrokerAvailable()) + if (platformUtils.IsWindows10Or11() || platformUtils.IsMacOSBrokerAvailable() || platformUtils.IsLinuxBrokerAvailable()) { flows.Add(new Broker(logger, authParams, preferredDomain: preferredDomain, pcaWrapper: pcaWrapper, promptHint: promptHint, platformUtils: platformUtils)); } @@ -74,6 +74,13 @@ public static IEnumerable Create( $"(checked: {PlatformUtils.CompanyPortalAppPath}). " + "Skipping broker and falling through to next auth flow."); } + else if (platformUtils.IsLinux()) + { + logger.LogWarning( + "Broker authentication was requested but is not available on this machine. " + + "Linux broker requires the Microsoft Identity Broker (installed via Intune enrollment). " + + "Skipping broker and falling through to next auth flow."); + } } if (authMode.IsWeb()) diff --git a/src/MSALWrapper/AuthFlow/Broker.cs b/src/MSALWrapper/AuthFlow/Broker.cs index ab5df33..963c27a 100644 --- a/src/MSALWrapper/AuthFlow/Broker.cs +++ b/src/MSALWrapper/AuthFlow/Broker.cs @@ -15,7 +15,8 @@ namespace Microsoft.Authentication.MSALWrapper.AuthFlow using Microsoft.Identity.Client.Utils; /// - /// The broker auth flow. Supports Windows (WAM) and macOS (Enterprise SSO Extension). + /// The broker auth flow. Supports Windows (WAM), macOS (Enterprise SSO Extension), + /// and Linux (Microsoft Identity Broker). /// public class Broker : AuthFlowBase { @@ -130,9 +131,9 @@ private async Task ResolveAccountAsync() return account; } - if (this.platformUtils.IsMacOS()) + if (this.platformUtils.IsMacOS() || this.platformUtils.IsLinux()) { - // On macOS, OperatingSystemAccount is not supported. + // On macOS and Linux, OperatingSystemAccount is not supported. // If MSAL cache has no single matching account, trigger interactive auth. return null; } @@ -226,9 +227,15 @@ private IPCAWrapper BuildPCAWrapper(Guid clientId, string tenantId) if (this.platformUtils.IsMacOS()) { clientBuilder - .WithRedirectUri(Constants.MacOSBrokerRedirectUri.ToString()) + .WithRedirectUri(Constants.UnsignedAppBrokerRedirectUri.ToString()) .WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.OSX)); } + else if (this.platformUtils.IsLinux()) + { + clientBuilder + .WithRedirectUri(Constants.UnsignedAppBrokerRedirectUri.ToString()) + .WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.Linux)); + } else { #if PlatformWindows diff --git a/src/MSALWrapper/AuthMode.cs b/src/MSALWrapper/AuthMode.cs index 93a10c5..831ac21 100644 --- a/src/MSALWrapper/AuthMode.cs +++ b/src/MSALWrapper/AuthMode.cs @@ -50,7 +50,7 @@ public enum AuthMode : short Default = Broker | Web, #else /// - /// Broker auth mode (macOS Enterprise SSO Extension). + /// Broker auth mode (macOS Enterprise SSO Extension or Linux Microsoft Identity Broker). /// Broker = 1 << 2, @@ -60,9 +60,10 @@ public enum AuthMode : short All = Broker | Web | DeviceCode, /// - /// Default auth mode. On macOS, broker is opt-in via --mode broker because - /// it requires Company Portal and apps using broker-required CA policies - /// will hang indefinitely if web auth is attempted as fallback. + /// Default auth mode. On macOS/Linux, broker is opt-in via --mode broker because + /// it requires a platform broker (Company Portal / Microsoft Identity Broker) and + /// apps using broker-required CA policies will hang indefinitely if web auth is + /// attempted as fallback. /// Default = Web, #endif diff --git a/src/MSALWrapper/Constants.cs b/src/MSALWrapper/Constants.cs index 8244604..206be37 100644 --- a/src/MSALWrapper/Constants.cs +++ b/src/MSALWrapper/Constants.cs @@ -23,9 +23,10 @@ public static class Constants public static readonly Uri AadRedirectUri = new Uri("http://localhost"); /// - /// The redirect uri for macOS brokered authentication (unsigned runtime apps). + /// Redirect URI for MSAL runtime brokered auth of unsigned apps. Used by both the + /// macOS Enterprise SSO Extension broker and the Linux Microsoft Identity Broker. /// - public static readonly Uri MacOSBrokerRedirectUri = new Uri("msauth.com.msauth.unsignedapp://auth"); + public static readonly Uri UnsignedAppBrokerRedirectUri = new Uri("msauth.com.msauth.unsignedapp://auth"); /// /// The name of an environment variable used to disable file cache configuration. diff --git a/src/MSALWrapper/IPlatformUtils.cs b/src/MSALWrapper/IPlatformUtils.cs index 787664b..7eef17c 100644 --- a/src/MSALWrapper/IPlatformUtils.cs +++ b/src/MSALWrapper/IPlatformUtils.cs @@ -31,5 +31,19 @@ public interface IPlatformUtils /// /// - true if macOS broker prerequisites are met. bool IsMacOSBrokerAvailable(); + + /// + /// Check if running on Linux. + /// + /// - true if running on Linux. + bool IsLinux(); + + /// + /// Check if Linux brokered authentication is available. + /// Requires Linux and the Microsoft Identity Broker (installed/run by + /// Intune enrollment) to be present on the machine. + /// + /// - true if Linux broker prerequisites are met. + bool IsLinuxBrokerAvailable(); } } diff --git a/src/MSALWrapper/PlatformUtils.cs b/src/MSALWrapper/PlatformUtils.cs index 8abb905..466d39c 100644 --- a/src/MSALWrapper/PlatformUtils.cs +++ b/src/MSALWrapper/PlatformUtils.cs @@ -19,6 +19,8 @@ public class PlatformUtils : IPlatformUtils private Lazy isWindows10; private Lazy isMacOS; private Lazy isMacOSBrokerAvailable; + private Lazy isLinux; + private Lazy isLinuxBrokerAvailable; /// /// Initializes a new instance of the class. @@ -31,6 +33,8 @@ public PlatformUtils(ILogger logger) this.isWindows10 = new Lazy(() => this.CheckWindows10()); this.isMacOS = new Lazy(() => this.CheckMacOS()); this.isMacOSBrokerAvailable = new Lazy(() => this.CheckMacOSBrokerAvailable()); + this.isLinux = new Lazy(() => this.CheckLinux()); + this.isLinuxBrokerAvailable = new Lazy(() => this.CheckLinuxBrokerAvailable()); } /// @@ -57,6 +61,18 @@ public bool IsMacOSBrokerAvailable() return this.isMacOSBrokerAvailable.Value; } + /// + public bool IsLinux() + { + return this.isLinux.Value; + } + + /// + public bool IsLinuxBrokerAvailable() + { + return this.isLinuxBrokerAvailable.Value; + } + private bool CheckMacOS() { this.logger.LogTrace($"IsMacOS: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) = {RuntimeInformation.IsOSPlatform(OSPlatform.OSX)}"); @@ -146,6 +162,35 @@ private bool CheckMacOSBrokerAvailable() } } + /// + /// Install location of the Microsoft Identity Broker binary on Linux + /// (from the microsoft-identity-broker package). + /// + private const string LinuxBrokerPath = "/opt/microsoft/identity-broker/bin/microsoft-identity-broker"; + + private bool CheckLinux() + { + this.logger.LogTrace($"IsLinux: RuntimeInformation.IsOSPlatform(OSPlatform.Linux) = {RuntimeInformation.IsOSPlatform(OSPlatform.Linux)}"); + return RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + } + + private bool CheckLinuxBrokerAvailable() + { + if (!this.IsLinux()) + { + return false; + } + + if (File.Exists(LinuxBrokerPath)) + { + this.logger.LogTrace($"Linux broker found at: {LinuxBrokerPath}"); + return true; + } + + this.logger.LogDebug($"Linux broker unavailable: Microsoft Identity Broker not found at {LinuxBrokerPath}"); + return false; + } + private bool CheckWindows() { this.logger.LogTrace($"IsWindows: RuntimeInformation.IsOSPlatform(OSPlatform.Windows) = {RuntimeInformation.IsOSPlatform(OSPlatform.Windows)}");