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