Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
61 changes: 61 additions & 0 deletions src/MSALWrapper.Test/AuthFlow/AuthFlowFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@ public void Windows_Defaults()
{
this.MockIsWindows10Or11(false);
this.MockIsMacOSBrokerAvailable(false);
this.MockIsLinuxBrokerAvailable(false);
this.MockIsMacOS(false);
this.MockIsLinux(false);

IEnumerable<IAuthFlow> subject = this.Subject(AuthMode.Default);

Expand Down Expand Up @@ -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<IAuthFlow> subject = this.Subject(AuthMode.All);

Expand Down Expand Up @@ -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<IAuthFlow> subject = this.Subject(AuthMode.All);

Expand Down Expand Up @@ -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<IAuthFlow> subject = this.Subject(AuthMode.Broker);
Expand All @@ -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<IAuthFlow> subject = this.Subject(AuthMode.Broker | AuthMode.Web);
Expand All @@ -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<IAuthFlow> subject = this.Subject(AuthMode.Broker);

subject.Should().ContainSingle().Which.Should().BeOfType<Broker>();
}

[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<IAuthFlow> subject = this.Subject(AuthMode.Broker);

subject.Should().ContainSingle().Which.Should().BeOfType<CachedAuth>();
}

[Test]
[Platform("Linux")]
public void DefaultModes_Linux()
{
// On Linux, default mode is Web only (broker is opt-in via --mode broker).
IEnumerable<IAuthFlow> 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);
Expand All @@ -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);
}
}
}
1 change: 1 addition & 0 deletions src/MSALWrapper.Test/AuthFlow/BrokerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal class BrokerTest : AuthFlowTestBase
this.mockPlatformUtils = new Mock<IPlatformUtils>(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);
Expand Down
11 changes: 9 additions & 2 deletions src/MSALWrapper/AuthFlow/AuthFlowFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static IEnumerable<IAuthFlow> 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));
Expand All @@ -62,7 +62,7 @@ public static IEnumerable<IAuthFlow> 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));
}
Expand All @@ -74,6 +74,13 @@ public static IEnumerable<IAuthFlow> 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())
Expand Down
15 changes: 11 additions & 4 deletions src/MSALWrapper/AuthFlow/Broker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ namespace Microsoft.Authentication.MSALWrapper.AuthFlow
using Microsoft.Identity.Client.Utils;

/// <summary>
/// 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).
/// </summary>
public class Broker : AuthFlowBase
{
Expand Down Expand Up @@ -130,9 +131,9 @@ private async Task<IAccount> 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;
}
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/MSALWrapper/AuthMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public enum AuthMode : short
Default = Broker | Web,
#else
/// <summary>
/// Broker auth mode (macOS Enterprise SSO Extension).
/// Broker auth mode (macOS Enterprise SSO Extension or Linux Microsoft Identity Broker).
/// </summary>
Broker = 1 << 2,

Expand All @@ -60,9 +60,10 @@ public enum AuthMode : short
All = Broker | Web | DeviceCode,

/// <summary>
/// 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.
/// </summary>
Default = Web,
#endif
Expand Down
5 changes: 3 additions & 2 deletions src/MSALWrapper/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ public static class Constants
public static readonly Uri AadRedirectUri = new Uri("http://localhost");

/// <summary>
/// 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.
/// </summary>
public static readonly Uri MacOSBrokerRedirectUri = new Uri("msauth.com.msauth.unsignedapp://auth");
public static readonly Uri UnsignedAppBrokerRedirectUri = new Uri("msauth.com.msauth.unsignedapp://auth");

/// <summary>
/// The name of an environment variable used to disable file cache configuration.
Expand Down
14 changes: 14 additions & 0 deletions src/MSALWrapper/IPlatformUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,19 @@ public interface IPlatformUtils
/// </summary>
/// <returns><see cref="bool"/> - true if macOS broker prerequisites are met.</returns>
bool IsMacOSBrokerAvailable();

/// <summary>
/// Check if running on Linux.
/// </summary>
/// <returns><see cref="bool"/> - true if running on Linux.</returns>
bool IsLinux();

/// <summary>
/// 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.
/// </summary>
/// <returns><see cref="bool"/> - true if Linux broker prerequisites are met.</returns>
bool IsLinuxBrokerAvailable();
}
}
45 changes: 45 additions & 0 deletions src/MSALWrapper/PlatformUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class PlatformUtils : IPlatformUtils
private Lazy<bool> isWindows10;
private Lazy<bool> isMacOS;
private Lazy<bool> isMacOSBrokerAvailable;
private Lazy<bool> isLinux;
private Lazy<bool> isLinuxBrokerAvailable;

/// <summary>
/// Initializes a new instance of the <see cref="PlatformUtils"/> class.
Expand All @@ -31,6 +33,8 @@ public PlatformUtils(ILogger logger)
this.isWindows10 = new Lazy<bool>(() => this.CheckWindows10());
this.isMacOS = new Lazy<bool>(() => this.CheckMacOS());
this.isMacOSBrokerAvailable = new Lazy<bool>(() => this.CheckMacOSBrokerAvailable());
this.isLinux = new Lazy<bool>(() => this.CheckLinux());
this.isLinuxBrokerAvailable = new Lazy<bool>(() => this.CheckLinuxBrokerAvailable());
}

/// <inheritdoc/>
Expand All @@ -57,6 +61,18 @@ public bool IsMacOSBrokerAvailable()
return this.isMacOSBrokerAvailable.Value;
}

/// <inheritdoc/>
public bool IsLinux()
{
return this.isLinux.Value;
}

/// <inheritdoc/>
public bool IsLinuxBrokerAvailable()
{
return this.isLinuxBrokerAvailable.Value;
}

private bool CheckMacOS()
{
this.logger.LogTrace($"IsMacOS: RuntimeInformation.IsOSPlatform(OSPlatform.OSX) = {RuntimeInformation.IsOSPlatform(OSPlatform.OSX)}");
Expand Down Expand Up @@ -146,6 +162,35 @@ private bool CheckMacOSBrokerAvailable()
}
}

/// <summary>
/// Install location of the Microsoft Identity Broker binary on Linux
/// (from the microsoft-identity-broker package).
/// </summary>
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)}");
Expand Down