diff --git a/src/Authentication/Authentication.Core/Common/GraphSession.cs b/src/Authentication/Authentication.Core/Common/GraphSession.cs index 9d9fc08b091..6b893d4d1c4 100644 --- a/src/Authentication/Authentication.Core/Common/GraphSession.cs +++ b/src/Authentication/Authentication.Core/Common/GraphSession.cs @@ -51,6 +51,11 @@ public class GraphSession : IGraphSession /// public IRequestContext RequestContext { get; set; } + /// + /// Stores the user's Graph options. + /// + public IGraphOption GraphOption { get; set; } + /// /// Represents a collection of Microsoft Graph PowerShell meta-info. /// diff --git a/src/Authentication/Authentication.Core/Interfaces/IGraphOptions.cs b/src/Authentication/Authentication.Core/Interfaces/IGraphOptions.cs new file mode 100644 index 00000000000..3dd2483694f --- /dev/null +++ b/src/Authentication/Authentication.Core/Interfaces/IGraphOptions.cs @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; +using System.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.Graph.PowerShell.Authentication +{ + public interface IGraphOption + { + bool EnableWAMForMSGraph { get; set; } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs b/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs index 801860ae38c..c7f8ba48c3d 100644 --- a/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs +++ b/src/Authentication/Authentication.Core/Interfaces/IGraphSession.cs @@ -11,5 +11,6 @@ public interface IGraphSession IAuthContext AuthContext { get; set; } IDataStore DataStore { get; set; } IRequestContext RequestContext { get; set; } + IGraphOption GraphOption { get; set; } } } \ No newline at end of file diff --git a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj index 83c7964fbcb..6c71886ae2f 100644 --- a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj +++ b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj @@ -1,6 +1,7 @@ + 9.0 netstandard2.0;net6.0;net472 Microsoft.Graph.PowerShell.Authentication.Core 2.0.0 @@ -12,6 +13,7 @@ + diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs index e88f519b9ec..ab51c98964f 100644 --- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs @@ -4,6 +4,7 @@ using Azure.Core; using Azure.Core.Diagnostics; using Azure.Identity; +using Azure.Identity.BrokeredAuthentication; using Microsoft.Graph.Authentication; using Microsoft.Graph.PowerShell.Authentication.Core.Extensions; using Microsoft.Identity.Client; @@ -41,10 +42,9 @@ public static async Task GetTokenCredentialAsync(IAuthContext a return await GetInteractiveBrowserCredentialAsync(authContext, cancellationToken).ConfigureAwait(false); return await GetDeviceCodeCredentialAsync(authContext, cancellationToken).ConfigureAwait(false); case AuthenticationType.AppOnly: - if (authContext.TokenCredentialType == TokenCredentialType.ClientCertificate) - return await GetClientCertificateCredentialAsync(authContext).ConfigureAwait(false); - else - return await GetClientSecretCredentialAsync(authContext).ConfigureAwait(false); + return authContext.TokenCredentialType == TokenCredentialType.ClientCertificate + ? await GetClientCertificateCredentialAsync(authContext).ConfigureAwait(false) + : await GetClientSecretCredentialAsync(authContext).ConfigureAwait(false); case AuthenticationType.ManagedIdentity: return await GetManagedIdentityCredentialAsync(authContext).ConfigureAwait(false); case AuthenticationType.EnvironmentVariable: @@ -81,6 +81,11 @@ private static bool IsAuthFlowNotSupported() && (string.IsNullOrEmpty(EnvironmentVariables.ClientSecret) && string.IsNullOrEmpty(EnvironmentVariables.ClientCertificatePath))); } + private static bool IsWamSupported() + { + return GraphSession.Instance.GraphOption.EnableWAMForMSGraph && SharedUtilities.IsWindowsPlatform(); + } + private static async Task GetClientSecretCredentialAsync(IAuthContext authContext) { if (authContext is null) @@ -108,19 +113,28 @@ private static async Task GetInteractiveBrowserCre { if (authContext is null) throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext); - - var interactiveOptions = new InteractiveBrowserCredentialOptions - { - ClientId = authContext.ClientId, - TenantId = authContext.TenantId, - AuthorityHost = new Uri(GetAuthorityUrl(authContext)), - TokenCachePersistenceOptions = GetTokenCachePersistenceOptions(authContext) - }; + var interactiveOptions = IsWamSupported() ? new InteractiveBrowserCredentialBrokerOptions(WindowHandleUtlities.GetConsoleOrTerminalWindow()) : new InteractiveBrowserCredentialOptions(); + interactiveOptions.ClientId = authContext.ClientId; + interactiveOptions.TenantId = authContext.TenantId ?? "common"; + interactiveOptions.AuthorityHost = new Uri(GetAuthorityUrl(authContext)); + interactiveOptions.TokenCachePersistenceOptions = GetTokenCachePersistenceOptions(authContext); if (!File.Exists(Constants.AuthRecordPath)) { + AuthenticationRecord authRecord; var interactiveBrowserCredential = new InteractiveBrowserCredential(interactiveOptions); - var authRecord = await interactiveBrowserCredential.AuthenticateAsync(new TokenRequestContext(authContext.Scopes), cancellationToken).ConfigureAwait(false); + if (IsWamSupported()) + { + authRecord = await Task.Run(() => + { + // Run the thread in MTA. + return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken); + }); + } + else + { + authRecord = await interactiveBrowserCredential.AuthenticateAsync(new TokenRequestContext(authContext.Scopes), cancellationToken).ConfigureAwait(false); + } await WriteAuthRecordAsync(authRecord).ConfigureAwait(false); return interactiveBrowserCredential; } @@ -174,10 +188,9 @@ private static async Task GetClientCertificateCrede private static TokenCachePersistenceOptions GetTokenCachePersistenceOptions(IAuthContext authContext) { - if (authContext.ContextScope == ContextScope.Process) - return GraphSession.Instance.InMemoryTokenCache.GetTokenCachePersistenceOptions(); - - return new TokenCachePersistenceOptions { Name = Constants.CacheName }; + return authContext.ContextScope == ContextScope.Process + ? GraphSession.Instance.InMemoryTokenCache.GetTokenCachePersistenceOptions() + : new TokenCachePersistenceOptions { Name = Constants.CacheName }; } /// @@ -291,10 +304,9 @@ private static string GetAuthorityUrl(IAuthContext authContext) if (authContext is null) throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext); string audience = authContext.TenantId ?? Constants.DefaultTenant; - if (GraphSession.Instance.Environment != null) - return $"{GraphSession.Instance.Environment.AzureADEndpoint}/{audience}"; - - return $"{Constants.DefaultAzureADEndpoint}/{audience}"; + return GraphSession.Instance.Environment != null + ? $"{GraphSession.Instance.Environment.AzureADEndpoint}/{audience}" + : $"{Constants.DefaultAzureADEndpoint}/{audience}"; } /// @@ -303,6 +315,7 @@ private static string GetAuthorityUrl(IAuthContext authContext) /// /// Current context /// A based on provided context + /// A based on provided context private static X509Certificate2 GetCertificate(IAuthContext authContext) { if (authContext is null) diff --git a/src/Authentication/Authentication.Core/Utilities/WindowHandleUtlities.cs b/src/Authentication/Authentication.Core/Utilities/WindowHandleUtlities.cs new file mode 100644 index 00000000000..6ce5bdfb3bd --- /dev/null +++ b/src/Authentication/Authentication.Core/Utilities/WindowHandleUtlities.cs @@ -0,0 +1,52 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using Microsoft.Identity.Client.Extensions.Msal; +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Graph.PowerShell.Authentication.Core.Utilities +{ + internal static class WindowHandleUtlities + { + enum GetAncestorFlags + { + GetParent = 1, + GetRoot = 2, + /// + /// Retrieves the owned root window by walking the chain of parent and owner windows returned by GetParent. + /// + GetRootOwner = 3 + } + + /// + /// Retrieves the handle to the ancestor of the specified window. + /// See https://learn.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-acquire-token-wam#console-applications. + /// + /// A handle to the window whose ancestor is to be retrieved. + /// If this parameter is the desktop window, the function returns NULL. + /// The ancestor to be retrieved. + /// The return value is the handle to the ancestor window. + [DllImport("user32.dll", ExactSpelling = true)] + static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags); + + [DllImport("kernel32.dll")] + static extern IntPtr GetConsoleWindow(); + + public static IntPtr GetConsoleOrTerminalWindow() + { + if (SharedUtilities.IsWindowsPlatform()) + { + IntPtr consoleHandle = GetConsoleWindow(); + IntPtr handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner); + return handle; + } + else + { + // can't call Windows native APIs + return (IntPtr)0; + } + } + } +} diff --git a/src/Authentication/Authentication/Cmdlets/SetMgGraphOption.cs b/src/Authentication/Authentication/Cmdlets/SetMgGraphOption.cs new file mode 100644 index 00000000000..022effd1dd7 --- /dev/null +++ b/src/Authentication/Authentication/Cmdlets/SetMgGraphOption.cs @@ -0,0 +1,44 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using Newtonsoft.Json; +using System.IO; +using System.Management.Automation; + +namespace Microsoft.Graph.PowerShell.Authentication.Cmdlets +{ + [Cmdlet(VerbsCommon.Set, "MgGraphOption", HelpUri = "")] + public class SetMgGraphOption : PSCmdlet + { + [Parameter] + public bool EnableLoginByWAM { get; set; } + + protected override void BeginProcessing() + { + base.BeginProcessing(); + } + + protected override void ProcessRecord() + { + base.ProcessRecord(); + if (this.IsParameterBound(nameof(EnableLoginByWAM))) + { + GraphSession.Instance.GraphOption.EnableWAMForMSGraph = EnableLoginByWAM; + var message = $"Signin by Web Account Manager (WAM) is {(EnableLoginByWAM ? "enabled" : "disabled")}."; + WriteObject(message); + } + File.WriteAllText(Constants.GraphOptionsFilePath, JsonConvert.SerializeObject(GraphSession.Instance.GraphOption, Formatting.Indented)); + } + + protected override void EndProcessing() + { + base.EndProcessing(); + } + + protected override void StopProcessing() + { + base.StopProcessing(); + } + } +} \ No newline at end of file diff --git a/src/Authentication/Authentication/Common/GraphSessionInitializer.cs b/src/Authentication/Authentication/Common/GraphSessionInitializer.cs index d0ec4984e30..42910b74087 100644 --- a/src/Authentication/Authentication/Common/GraphSessionInitializer.cs +++ b/src/Authentication/Authentication/Common/GraphSessionInitializer.cs @@ -5,7 +5,9 @@ using Microsoft.Graph.PowerShell.Authentication.Helpers; using Microsoft.Graph.PowerShell.Authentication.Interfaces; using Microsoft.Graph.PowerShell.Authentication.Models; +using Newtonsoft.Json; using System; +using System.IO; using System.Management.Automation; using RequestContext = Microsoft.Graph.PowerShell.Authentication.Models.RequestContext; @@ -26,10 +28,18 @@ public static void InitializeSession() /// internal static GraphSession CreateInstance(IDataStore dataStore = null) { + IGraphOption graphOptions = null; + if (File.Exists(Constants.GraphOptionsFilePath)) + { + // Deserialize the JSON into the GraphOption instance + graphOptions = JsonConvert.DeserializeObject(File.ReadAllText(Constants.GraphOptionsFilePath)); + } + return new GraphSession { DataStore = dataStore ?? new DiskDataStore(), - RequestContext = new RequestContext() + RequestContext = new RequestContext(), + GraphOption = graphOptions ?? new GraphOption() }; } /// diff --git a/src/Authentication/Authentication/Constants.cs b/src/Authentication/Authentication/Constants.cs index 906b7b6786b..6bc4d5ec1b3 100644 --- a/src/Authentication/Authentication/Constants.cs +++ b/src/Authentication/Authentication/Constants.cs @@ -24,6 +24,7 @@ public static class Constants internal const int MAX_NUMBER_OF_RETRY = 10; internal const int DEFAULT_RETRY_DELAY = 3; internal const int DEFAULT_MAX_RETRY = 3; + internal static readonly string GraphOptionsFilePath = Path.Combine(Core.Constants.GraphDirectoryPath, "mg.graphoptions.json"); public static class HelpMessages { diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec index cb498b25a94..d0da7f9ca91 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec @@ -1,7 +1,7 @@ - 2.0.0-rc3 + 2.0.0-rc4 Microsoft.Graph.Authentication Microsoft Graph PowerShell authentication module Microsoft @@ -27,6 +27,9 @@ + + + @@ -39,6 +42,9 @@ + + + diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 index b978cbf4402..b2284373da9 100644 --- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 +++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 @@ -75,7 +75,7 @@ FunctionsToExport = 'Find-MgGraphCommand', 'Find-MgGraphPermission' CmdletsToExport = 'Connect-MgGraph', 'Disconnect-MgGraph', 'Get-MgContext', 'Invoke-MgGraphRequest', 'Add-MgEnvironment', 'Get-MgEnvironment', 'Remove-MgEnvironment', 'Set-MgEnvironment', 'Get-MgRequestContext', - 'Set-MgRequestContext' + 'Set-MgRequestContext', 'Set-MgGraphOption' # Variables to export from this module VariablesToExport = '*' diff --git a/src/Authentication/Authentication/Models/GraphOption.cs b/src/Authentication/Authentication/Models/GraphOption.cs new file mode 100644 index 00000000000..d8c48d7f70a --- /dev/null +++ b/src/Authentication/Authentication/Models/GraphOption.cs @@ -0,0 +1,14 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System.IO; + +namespace Microsoft.Graph.PowerShell.Authentication +{ + internal class GraphOption : IGraphOption + { + public bool EnableWAMForMSGraph { get; set; } + } + +} \ No newline at end of file diff --git a/src/Authentication/Authentication/ModuleInitializer.cs b/src/Authentication/Authentication/ModuleInitializer.cs index 1f721f3a6f0..c04d4c4fe46 100644 --- a/src/Authentication/Authentication/ModuleInitializer.cs +++ b/src/Authentication/Authentication/ModuleInitializer.cs @@ -34,13 +34,29 @@ static ModuleInitializer() // Add shared dependencies. foreach (string filePath in Directory.EnumerateFiles(s_dependencyFolder, "*.dll")) { - s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + try + { + s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + } + catch (BadImageFormatException) + { + // Skip files without metadata. + continue; + } } // Add the dependencies for the current PowerShell edition. Can be either Desktop (PS 5.1) or Core (PS 7+). foreach (string filePath in Directory.EnumerateFiles(s_psEditionDependencyFolder, "*.dll")) { - s_psEditionDependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + try + { + s_psEditionDependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); + } + catch (BadImageFormatException) + { + // Skip files without metadata. + continue; + } } } @@ -70,7 +86,7 @@ public void OnRemove(PSModuleInfo psModuleInfo) private static bool IsAssemblyMatching(AssemblyName assemblyName, Assembly requestingAssembly) { return requestingAssembly != null - ? requestingAssembly.FullName.StartsWith("Microsoft") && IsAssemblyPresent(assemblyName) + ? (requestingAssembly.FullName.StartsWith("Microsoft") || requestingAssembly.FullName.StartsWith("Azure.Identity")) && IsAssemblyPresent(assemblyName) : IsAssemblyPresent(assemblyName); } @@ -82,10 +98,9 @@ private static bool IsAssemblyMatching(AssemblyName assemblyName, Assembly reque /// True if assembly is present in dependencies folder; otherwise False. private static bool IsAssemblyPresent(AssemblyName assemblyName) { - if (s_dependencies.Contains(assemblyName.FullName) || s_psEditionDependencies.Contains(assemblyName.FullName)) - return true; - else - return !string.IsNullOrEmpty(s_dependencies.SingleOrDefault((x) => x.StartsWith(assemblyName.Name))) || !string.IsNullOrEmpty(s_psEditionDependencies.SingleOrDefault((x) => x.StartsWith(assemblyName.Name))); + return s_dependencies.Contains(assemblyName.FullName) || s_psEditionDependencies.Contains(assemblyName.FullName) + ? true + : !string.IsNullOrEmpty(s_dependencies.SingleOrDefault((x) => x.StartsWith($"{assemblyName.Name},"))) || !string.IsNullOrEmpty(s_psEditionDependencies.SingleOrDefault((x) => x.StartsWith($"{assemblyName.Name},"))); } /// diff --git a/src/Authentication/Authentication/build-module.ps1 b/src/Authentication/Authentication/build-module.ps1 index dabfe050eed..6dfa62a37ea 100644 --- a/src/Authentication/Authentication/build-module.ps1 +++ b/src/Authentication/Authentication/build-module.ps1 @@ -104,20 +104,20 @@ $Deps = [System.Collections.Generic.HashSet[string]]::new() Get-ChildItem -Path "$coreSrc/bin/$Configuration/$netStandard/publish/" | Where-Object { $_.Extension -in $copyExtensions } | Where-Object { -not $CoreAssemblies.Contains($_.BaseName) } | -ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDeps } +ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDeps -Recurse } Get-ChildItem -Path "$coreSrc/bin/$Configuration/$netApp/publish/" | Where-Object { -not $CoreAssemblies.Contains($_.BaseName) } | -ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outCore } +ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outCore -Recurse } Get-ChildItem -Path "$coreSrc/bin/$Configuration/$netFx/publish/" | Where-Object { -not $CoreAssemblies.Contains($_.BaseName) } | -ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDesktop } +ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDesktop -Recurse } # Now copy each authentication asset, not taking any found in authentication.core. Get-ChildItem -Path "$cmdletsSrc/bin/$Configuration/$netStandard/publish/" | Where-Object { -not $Deps.Contains($_.Name) -and $_.Extension -in $copyExtensions } | -ForEach-Object { Copy-Item -Path $_.FullName -Destination $outDir } +ForEach-Object { Copy-Item -Path $_.FullName -Destination $outDir -Recurse } # Update module manifest with nested assemblies. $RequiredAssemblies = @( diff --git a/src/Authentication/Authentication/test/Microsoft.Graph.Authentication.Tests.ps1 b/src/Authentication/Authentication/test/Microsoft.Graph.Authentication.Tests.ps1 index 86e59ebc10f..6eb4488d04b 100644 --- a/src/Authentication/Authentication/test/Microsoft.Graph.Authentication.Tests.ps1 +++ b/src/Authentication/Authentication/test/Microsoft.Graph.Authentication.Tests.ps1 @@ -50,7 +50,8 @@ Describe "Microsoft.Graph.Authentication module" { "Find-MgGraphPermission", "Invoke-MgRestMethod", "Get-MgRequestContext", - "Set-MgRequestContext" + "Set-MgRequestContext", + "Set-MgGraphOption" ) $PSModuleInfo.ExportedCommands.Keys | Should -BeIn $ExpectedCommands