diff --git a/LibsAndSamples.sln b/LibsAndSamples.sln index cf52f4927f..34410d1a39 100644 --- a/LibsAndSamples.sln +++ b/LibsAndSamples.sln @@ -186,6 +186,8 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Identity.Test.E2E.MSI", "tests\Microsoft.Identity.Test.E2e\Microsoft.Identity.Test.E2E.MSI.csproj", "{97995B86-AA0F-3AF9-DA40-85A6263E4391}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacMauiAppWithBroker", "tests\devapps\MacMauiAppWithBroker\MacMauiAppWithBroker.csproj", "{AEF6BB00-931F-4638-955D-24D735625C34}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MacConsoleAppWithBroker", "tests\devapps\MacConsoleAppWithBroker\MacConsoleAppWithBroker.csproj", "{DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug + MobileApps|Any CPU = Debug + MobileApps|Any CPU @@ -1866,6 +1868,48 @@ Global {AEF6BB00-931F-4638-955D-24D735625C34}.Release|x64.Build.0 = Release|Any CPU {AEF6BB00-931F-4638-955D-24D735625C34}.Release|x86.ActiveCfg = Release|Any CPU {AEF6BB00-931F-4638-955D-24D735625C34}.Release|x86.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|Any CPU.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|Any CPU.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|ARM.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|ARM.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|ARM64.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|ARM64.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|iPhone.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|iPhone.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|iPhoneSimulator.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|x64.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|x64.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|x86.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug + MobileApps|x86.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|ARM.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|ARM.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|ARM64.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|iPhone.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|x64.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|x86.ActiveCfg = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Debug|x86.Build.0 = Debug|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|Any CPU.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|ARM.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|ARM.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|ARM64.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|ARM64.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|iPhone.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|iPhone.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x64.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x64.Build.0 = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.ActiveCfg = Release|Any CPU + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1921,6 +1965,7 @@ Global {DA9C3258-DEF6-7794-9762-20CF7B826839} = {BCAEE9AE-8D3E-4C77-A2E4-134E1552D5F8} {97995B86-AA0F-3AF9-DA40-85A6263E4391} = {9B0B5396-4D95-4C15-82ED-DC22B5A3123F} {AEF6BB00-931F-4638-955D-24D735625C34} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} + {DBD18BC8-72E4-47D4-BD79-8DEBD9F2C0D0} = {34BE693E-3496-45A4-B1D2-D3A0E068EEDB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {020399A9-DC27-4B82-9CAA-EF488665AC27} diff --git a/src/client/Microsoft.Identity.Client.Broker/RuntimeBroker.cs b/src/client/Microsoft.Identity.Client.Broker/RuntimeBroker.cs index 8696d5e100..4a306df1d0 100644 --- a/src/client/Microsoft.Identity.Client.Broker/RuntimeBroker.cs +++ b/src/client/Microsoft.Identity.Client.Broker/RuntimeBroker.cs @@ -186,16 +186,34 @@ public async Task AcquireTokenInteractiveAsync( { if (readAccountResult.IsSuccess) { - using (var result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync( - _parentHandle, - authParams, - authenticationRequestParameters.CorrelationId.ToString("D"), - readAccountResult.Account, - cancellationToken).ConfigureAwait(false)) + if (DesktopOsHelper.IsMac()) { + AuthResult result = null; + await MacMainThreadScheduler.Instance().RunOnMainThreadAsync(async () => + { + result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync( + _parentHandle, + authParams, + authenticationRequestParameters.CorrelationId.ToString("D"), + readAccountResult.Account, + cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); var errorMessage = "Could not acquire token interactively."; msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); } + else // Non macOS + { + using (var result = await s_lazyCore.Value.AcquireTokenInteractivelyAsync( + _parentHandle, + authParams, + authenticationRequestParameters.CorrelationId.ToString("D"), + readAccountResult.Account, + cancellationToken).ConfigureAwait(false)) + { + var errorMessage = "Could not acquire token interactively."; + msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); + } + } } else { @@ -238,16 +256,34 @@ private async Task SignInInteractivelyAsync( string loginHint = authenticationRequestParameters.LoginHint ?? authenticationRequestParameters?.Account?.Username; _logger?.Verbose(() => "[RuntimeBroker] AcquireTokenInteractive - login hint provided? " + !string.IsNullOrEmpty(loginHint)); - using (var result = await s_lazyCore.Value.SignInInteractivelyAsync( - _parentHandle, - authParams, - authenticationRequestParameters.CorrelationId.ToString("D"), - loginHint, - cancellationToken).ConfigureAwait(false)) + if (DesktopOsHelper.IsMac()) { + AuthResult result = null; + await MacMainThreadScheduler.Instance().RunOnMainThreadAsync(async () => + { + result = await s_lazyCore.Value.SignInInteractivelyAsync( + _parentHandle, + authParams, + authenticationRequestParameters.CorrelationId.ToString("D"), + loginHint, + cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); var errorMessage = "Could not sign in interactively."; msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); } + else // Non macOS + { + using (var result = await s_lazyCore.Value.SignInInteractivelyAsync( + _parentHandle, + authParams, + authenticationRequestParameters.CorrelationId.ToString("D"), + loginHint, + cancellationToken).ConfigureAwait(true)) + { + var errorMessage = "Could not sign in interactively."; + msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); + } + } } return msalTokenResponse; @@ -268,14 +304,31 @@ private async Task AcquireTokenInteractiveDefaultUserAsync( _brokerOptions, _logger)) { - using (NativeInterop.AuthResult result = await s_lazyCore.Value.SignInAsync( + if (DesktopOsHelper.IsMac()) + { + AuthResult result = null; + await MacMainThreadScheduler.Instance().RunOnMainThreadAsync(async () => + { + result = await s_lazyCore.Value.SignInAsync( + _parentHandle, + authParams, + authenticationRequestParameters.CorrelationId.ToString("D"), + cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + var errorMessage = "Could not sign in interactively with the default OS account."; + msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); + } + else // Non macOS + { + using (NativeInterop.AuthResult result = await s_lazyCore.Value.SignInAsync( _parentHandle, authParams, authenticationRequestParameters.CorrelationId.ToString("D"), cancellationToken).ConfigureAwait(false)) - { - var errorMessage = "Could not sign in interactively with the default OS account."; - msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); + { + var errorMessage = "Could not sign in interactively with the default OS account."; + msalTokenResponse = WamAdapters.HandleResponse(result, authenticationRequestParameters, _logger, errorMessage); + } } } diff --git a/src/client/Microsoft.Identity.Client/Instance/AuthorityManager.cs b/src/client/Microsoft.Identity.Client/Instance/AuthorityManager.cs index 6f5eedc09e..3ba667b956 100644 --- a/src/client/Microsoft.Identity.Client/Instance/AuthorityManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/AuthorityManager.cs @@ -6,6 +6,7 @@ using Microsoft.Identity.Client.Instance.Discovery; using Microsoft.Identity.Client.Instance.Validation; using Microsoft.Identity.Client.Internal; +using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Client.Utils; using static Microsoft.Identity.Client.AuthorityInfo; @@ -47,7 +48,15 @@ public async Task GetInstanceDiscoveryEntryAsync public async Task RunInstanceDiscoveryAndValidationAsync() { - if (!_instanceDiscoveryAndValidationExecuted) + if (DesktopOsHelper.IsMac() && _requestContext.ServiceBundle.Config.IsBrokerEnabled) + { + // On macOS, for broker flows we should avoid authority validation. Internally, we should avoid any network requests to prevent context switch. + // Interactive calls need to happen in the main thread, making network calls will switch to another thread and not easy to go back. + _metadata = _requestContext.ServiceBundle.InstanceDiscoveryManager.GetMetadataEntryAvoidNetwork( + _initialAuthority.AuthorityInfo, + _requestContext); + } + else if (!_instanceDiscoveryAndValidationExecuted) { // This will make a network call unless instance discovery is cached, but this OK // GetAccounts and AcquireTokenSilent do not need this diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/IInstanceDiscoveryManager.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/IInstanceDiscoveryManager.cs index a157a2cdf8..964a7bffc1 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/IInstanceDiscoveryManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/IInstanceDiscoveryManager.cs @@ -12,6 +12,9 @@ namespace Microsoft.Identity.Client.Instance.Discovery /// internal interface IInstanceDiscoveryManager { + InstanceDiscoveryMetadataEntry GetMetadataEntryAvoidNetwork( + AuthorityInfo authorityInfo, + RequestContext requestContext); Task GetMetadataEntryTryAvoidNetworkAsync( AuthorityInfo authorityinfo, IEnumerable existingEnvironmentsInCache, diff --git a/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs b/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs index 8dfd3603cd..a42d8c4103 100644 --- a/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs +++ b/src/client/Microsoft.Identity.Client/Instance/Discovery/InstanceDiscoveryManager.cs @@ -80,6 +80,25 @@ public InstanceDiscoveryManager( } } + public InstanceDiscoveryMetadataEntry GetMetadataEntryAvoidNetwork( + AuthorityInfo authorityInfo, + RequestContext requestContext) + { + string environment = authorityInfo.Host; + InstanceDiscoveryMetadataEntry entry = null; + if (requestContext.ServiceBundle.Config.IsInstanceDiscoveryEnabled) + { + entry = _networkCacheMetadataProvider.GetMetadata(environment, requestContext.Logger) ?? + _knownMetadataProvider.GetMetadata(environment, null, requestContext.Logger); + } + if (entry == null) + { + requestContext.Logger.Info(() => $"Skipping Instance discovery for {authorityInfo.AuthorityType} authority and use single authority."); + entry = CreateEntryForSingleAuthority(authorityInfo.CanonicalAuthority); + } + return entry; + } + public async Task GetMetadataEntryTryAvoidNetworkAsync( AuthorityInfo authorityInfo, IEnumerable existingEnvironmentsInCache, diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs index 095b092cf3..c0121cd651 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/Interactive/InteractiveRequest.cs @@ -9,7 +9,9 @@ using Microsoft.Identity.Client.Instance; using Microsoft.Identity.Client.Internal.Broker; using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.PlatformsCommon.Shared; using Microsoft.Identity.Client.UI; +using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.Internal.Requests { @@ -63,8 +65,45 @@ protected override async Task ExecuteAsync( { await ResolveAuthorityAsync().ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - MsalTokenResponse tokenResponse = await GetTokenResponseAsync(cancellationToken) - .ConfigureAwait(false); + MsalTokenResponse tokenResponse = null; + if (DesktopOsHelper.IsMac() && ServiceBundle.Config.IsBrokerEnabled) + { + var macMainThreadScheduler = MacMainThreadScheduler.Instance(); + if (!macMainThreadScheduler.IsCurrentlyOnMainThread()) + { + throw new MsalClientException( + MsalError.WamUiThread, + "Interactive requests with mac broker enabled must be executed on the main thread on macOS."); + } + bool messageLoopStarted = macMainThreadScheduler.IsRunning(); + var tcs = new TaskCompletionSource(); + _ = Task.Run(async () => + { + try + { + MsalTokenResponse response = await GetTokenResponseAsync(cancellationToken).ConfigureAwait(false); + tcs.SetResult(response); + } + catch (Exception ex) + { + _logger.Error($"Error in background GetTokenResponseAsync: {ex}"); + tcs.SetException(ex); + } + finally + { + if (!messageLoopStarted) + macMainThreadScheduler.Stop(); + } + }); + if (!messageLoopStarted) + macMainThreadScheduler.StartMessageLoop(); + tokenResponse = await tcs.Task.ConfigureAwait(false); + } + else + { + tokenResponse = await GetTokenResponseAsync(cancellationToken) + .ConfigureAwait(false); + } return await CacheTokenResponseAndCreateAuthenticationResultAsync(tokenResponse) .ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net462/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net472/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-android/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0-ios/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/net8.0/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2..1164619fd4 100644 --- a/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/client/Microsoft.Identity.Client/PublicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,7 @@ +Microsoft.Identity.Client.Utils.MacMainThreadScheduler +static Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Instance() -> Microsoft.Identity.Client.Utils.MacMainThreadScheduler +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsCurrentlyOnMainThread() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.IsRunning() -> bool +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.Stop() -> void +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.RunOnMainThreadAsync(System.Func asyncAction) -> System.Threading.Tasks.Task +Microsoft.Identity.Client.Utils.MacMainThreadScheduler.StartMessageLoop() -> void diff --git a/src/client/Microsoft.Identity.Client/Utils/MacMainThreadScheduler.cs b/src/client/Microsoft.Identity.Client/Utils/MacMainThreadScheduler.cs new file mode 100644 index 0000000000..6b8699ec5f --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Utils/MacMainThreadScheduler.cs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client.Utils +{ + internal struct MainThreadActionItem + { + public Action Action { get; } + public TaskCompletionSource Completion { get; } + public bool IsAsyncAction { get; } + + public MainThreadActionItem(Action action, TaskCompletionSource completion, bool isAsyncAction) + { + Action = action; + Completion = completion; + IsAsyncAction = isAsyncAction; + } + } + + /// + /// This class implements a main thread scheduler for macOS applications. It should be also working on other platforms, but it is primarily designed for macOS. + /// It is mainly designed for internal use to support the MSAL library in "switching to the main thread anytime". + /// However, external users can also call it if needed. + /// + public class MacMainThreadScheduler + { + private readonly ConcurrentQueue _mainThreadActions; + + private volatile bool _workerFinished; + private volatile bool _isRunning; + + // Configurable sleep time in milliseconds in the message loop. + private const int WorkerSleepInMilliseconds = 10; + + // Singleton mode + private static readonly Lazy _instance = new Lazy(() => new MacMainThreadScheduler()); + + /// + /// Gets the singleton instance of MacMainThreadScheduler + /// + public static MacMainThreadScheduler Instance() + { + return _instance.Value; + } + + /// + /// Private constructor for MacMainThreadScheduler (singleton pattern) + /// + private MacMainThreadScheduler() + { + _mainThreadActions = new ConcurrentQueue(); + _workerFinished = false; + _isRunning = false; + } + + /// + /// Check if the current thread is the main thread. + /// + public bool IsCurrentlyOnMainThread() + { + return Environment.CurrentManagedThreadId == 1; // Main thread id is always 1 on macOS. + } + + /// + /// Check if the message loop is currently running. + /// + public bool IsRunning() + { + return _isRunning; + } + + /// + /// Stop the main thread message loop + /// + public void Stop() + { + _workerFinished = true; + } + + /// + /// Run on the main thread asynchronously. + /// + /// action + /// FinishedTask + public Task RunOnMainThreadAsync(Func asyncAction) + { + if (asyncAction == null) + throw new ArgumentNullException(nameof(asyncAction)); + + var tcs = new TaskCompletionSource(); + Action wrapper = () => + { + try + { + asyncAction().ContinueWith(task => + { + if (task.IsFaulted && task.Exception != null) + { + tcs.TrySetException(task.Exception.InnerExceptions); + } + else + { + tcs.TrySetResult(true); + } + }, TaskScheduler.Default); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + }; + + _mainThreadActions.Enqueue(new MainThreadActionItem(wrapper, tcs, true)); + return tcs.Task; + } + + /// + /// Start the message loop on the main thread to process actions + /// + public void StartMessageLoop() + { + if (!IsCurrentlyOnMainThread()) + throw new InvalidOperationException("Message loop must be started on the main thread."); + + if (_isRunning) + throw new InvalidOperationException("StartMessageLoop already running."); + + _isRunning = true; + _workerFinished = false; + try + { + while (!_workerFinished) + { + while (_mainThreadActions.TryDequeue(out var actionItem)) + { + try + { + actionItem.Action(); + if (!actionItem.IsAsyncAction) + { + actionItem.Completion.TrySetResult(true); + } + } + catch (Exception ex) + { + actionItem.Completion.TrySetException(ex); + } + } + // Sleep for a short interval to avoid busy-waiting and reduce CPU usage while waiting for new actions in the queue. + Thread.Sleep(WorkerSleepInMilliseconds); + } + } + finally + { + _isRunning = false; + } + } + + } + +} diff --git a/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.cs b/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.cs new file mode 100644 index 0000000000..814c57079f --- /dev/null +++ b/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Broker; +using Microsoft.Identity.Client.Utils; + +class MacConsoleAppWithBroker +{ + private static MacMainThreadScheduler macMainThreadScheduler = MacMainThreadScheduler.Instance(); + + static void Main(string[] args) + { + _ = Task.Run(() => BackgroundWorker()); + + macMainThreadScheduler.StartMessageLoop(); + + Console.WriteLine("Background worker completed. Application exiting."); + } + + private static string TruncateToken(string token) + { + if (string.IsNullOrEmpty(token)) + return string.Empty; + + return token.Length <= 50 ? token : token.Substring(0, 50) + "..."; + } + + private static async Task SwitchToBackgroundThreadViaHttpRequest() + { + Console.WriteLine($"Current thread ID before HTTP request: {Environment.CurrentManagedThreadId}"); + using (HttpClient client = new HttpClient()) + { + try + { + // Make a simple HTTP request to switch context + HttpResponseMessage response = await client.GetAsync("https://httpbin.org/get").ConfigureAwait(false); + string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + Console.WriteLine("HTTP request completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"HTTP request failed: {ex.Message}"); + } + } + + Console.WriteLine($"Current thread ID (after HTTP request): {Environment.CurrentManagedThreadId}"); + } + + private static async Task BackgroundWorker() + { + try + { + PublicClientApplicationBuilder builder = PublicClientApplicationBuilder + .Create("04b07795-8ddb-461a-bbee-02f9e1bf7b46") // Azure CLI client id + .WithRedirectUri("msauth.com.msauth.unsignedapp://auth") // Unsigned app redirect, required by broker team. + .WithAuthority("https://login.microsoftonline.com/organizations"); + + builder = builder.WithLogging(SampleLogging); + + builder = builder.WithBroker(new BrokerOptions(BrokerOptions.OperatingSystems.OSX) + { + ListOperatingSystemAccounts = false, + MsaPassthrough = false, + Title = "MSAL Dev App .NET FX" + } + ); + + IPublicClientApplication pca = builder.Build(); + + AcquireTokenInteractiveParameterBuilder interactiveBuilder = pca.AcquireTokenInteractive(new string[] { "https://graph.microsoft.com/.default" }); + + AuthenticationResult? result = null; + + // Acquire token interactively on main thread + await macMainThreadScheduler.RunOnMainThreadAsync(async () => + { + try + { + // Execute the authentication request on the main thread + result = await interactiveBuilder.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + Console.WriteLine("Interactive authentication completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Interactive authentication error: {ex}"); + throw; + } + }).ConfigureAwait(false); + + Console.WriteLine($"Interactive call. Access token: {TruncateToken(result.AccessToken)}"); + Console.WriteLine($"Expires on: {result.ExpiresOn}"); + + // Make an HTTP request to switch to a background thread + await SwitchToBackgroundThreadViaHttpRequest().ConfigureAwait(false); + + IAccount account = result.Account; + AcquireTokenSilentParameterBuilder silentBuilder = pca.AcquireTokenSilent(new string[] { "https://graph.microsoft.com/.default" }, account); + + // Silent call on main thread + await macMainThreadScheduler.RunOnMainThreadAsync(async () => + { + try + { + // Execute the silent authentication request on the main thread + result = await silentBuilder.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + Console.WriteLine("Second interactive authentication completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Second interactive authentication error: {ex}"); + throw; + } + }).ConfigureAwait(false); + + Console.WriteLine($"Silent Call. Access token: {TruncateToken(result.AccessToken)}"); + Console.WriteLine($"Expires on: {result.ExpiresOn}"); + + // Make an HTTP request to switch to a background thread + await SwitchToBackgroundThreadViaHttpRequest().ConfigureAwait(false); + + interactiveBuilder.WithAccount(account); + // Second interactive call with account on main thread + await macMainThreadScheduler.RunOnMainThreadAsync(async () => + { + try + { + // Execute the authentication request on the main thread + result = await interactiveBuilder.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + Console.WriteLine("Second interactive authentication with account completed successfully."); + } + catch (Exception ex) + { + Console.WriteLine($"Second interactive authentication with account error: {ex}"); + throw; + } + }).ConfigureAwait(false); + + Console.WriteLine($"Second interactive call. Access token: {TruncateToken(result.AccessToken)}"); + Console.WriteLine($"Expires on: {result.ExpiresOn}"); + + // Make an HTTP request to switch to a background thread + await SwitchToBackgroundThreadViaHttpRequest().ConfigureAwait(false); + + try + { + // Execute the authentication request on the main thread + result = await interactiveBuilder.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + Console.WriteLine("Third interactive call should not succeed."); + } + catch (Exception ex) + { + Console.WriteLine($"Third interactive authentication error: {ex}"); + + Console.WriteLine($"\nNotice! The third interactive call fails with status ApiContractViolation is expected. The interactive call should happen in the main thread.\n"); + } + } + finally + { + // Signal that the worker has finished, regardless of success or failure + macMainThreadScheduler.Stop(); + } + } + + private static void SampleLogging(LogLevel level, string message, bool containsPii) + { + try + { + string homeDirectory = Environment.GetEnvironmentVariable("HOME") ?? "/tmp"; + string filePath = Path.Combine(homeDirectory, "msalnet.log"); + using (StreamWriter writer = new StreamWriter(filePath, append: true)) + { + writer.WriteLine($"{level} {message}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Failed to write log: {ex.Message}"); + } + } + +} diff --git a/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.csproj b/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.csproj new file mode 100644 index 0000000000..5892a40ce6 --- /dev/null +++ b/tests/devapps/MacConsoleAppWithBroker/MacConsoleAppWithBroker.csproj @@ -0,0 +1,26 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + +