From 4b863fba75b33ebc5540c499d1d685a30ff6a33c Mon Sep 17 00:00:00 2001
From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com>
Date: Tue, 22 Sep 2026 14:48:49 +0200
Subject: [PATCH 1/2] Completely isolating Microsoft.Graph.Authentication to
never have dependency issues ever again
---
.../Authentication.Core/Constants.cs | 9 ++
.../HttpRequestMessageExtensions.cs | 6 +-
.../Http}/AuthenticationHandler.cs | 13 +-
.../Http/GraphHttpClientFactory.cs | 87 ++++++++++
.../GraphAssemblyLoadContext.cs | 147 +++++++++++++++++
.../GraphLoadContextInitializer.cs | 80 ++++++++++
...crosoft.Graph.Authentication.Loader.csproj | 23 +++
.../Authentication.Loader/README.md | 96 +++++++++++
.../AssemblyIsolationTests.cs | 73 +++++++++
...Microsoft.Graph.Authentication.Test.csproj | 2 +
src/Authentication/Authentication.sln | 55 +++++++
.../Handlers/RequestHeaderHandler.cs | 15 +-
.../Authentication/Helpers/HttpHelpers.cs | 67 +++-----
.../Microsoft.Graph.Authentication.nuspec | 12 +-
.../Microsoft.Graph.Authentication.psm1 | 32 ++++
.../Models/GraphCommandCache.cs | 20 +++
.../Authentication/ModuleInitializer.cs | 150 ++++++++++--------
.../Authentication/build-module.ps1 | 45 ++++--
.../custom/Find-MgGraphCommand.ps1 | 18 +--
.../custom/common/Permissions.ps1 | 10 +-
.../test/AssemblyIsolation.Tests.ps1 | 109 +++++++++++++
.../test/Disconnect-MgGraph.Tests.ps1 | 30 ++--
.../test/Find-MgGraphPermission.Tests.ps1 | 2 +-
.../Authentication/test/loadEnv.ps1 | 26 ++-
24 files changed, 952 insertions(+), 175 deletions(-)
rename src/Authentication/{Authentication => Authentication.Core}/Extensions/HttpRequestMessageExtensions.cs (95%)
rename src/Authentication/{Authentication/Handlers => Authentication.Core/Http}/AuthenticationHandler.cs (92%)
create mode 100644 src/Authentication/Authentication.Core/Http/GraphHttpClientFactory.cs
create mode 100644 src/Authentication/Authentication.Loader/GraphAssemblyLoadContext.cs
create mode 100644 src/Authentication/Authentication.Loader/GraphLoadContextInitializer.cs
create mode 100644 src/Authentication/Authentication.Loader/Microsoft.Graph.Authentication.Loader.csproj
create mode 100644 src/Authentication/Authentication.Loader/README.md
create mode 100644 src/Authentication/Authentication.Test/AssemblyIsolationTests.cs
create mode 100644 src/Authentication/Authentication/Models/GraphCommandCache.cs
create mode 100644 src/Authentication/Authentication/test/AssemblyIsolation.Tests.ps1
diff --git a/src/Authentication/Authentication.Core/Constants.cs b/src/Authentication/Authentication.Core/Constants.cs
index 70134875e70..b0eebbda025 100644
--- a/src/Authentication/Authentication.Core/Constants.cs
+++ b/src/Authentication/Authentication.Core/Constants.cs
@@ -22,6 +22,15 @@ public static class Constants
internal const string AuthRecordName = "mg.authrecord.json";
internal const int MaxAuthRetry = 2;
internal static readonly string AuthRecordPath = Path.Combine(GraphDirectoryPath, AuthRecordName);
+
+ ///
+ /// Header names mirrored from Microsoft.Graph.Core's CoreConstants so callers do not need a reference to that assembly.
+ ///
+ public static class Headers
+ {
+ public const string SdkVersionHeaderName = "SdkVersion";
+ public const string ClientRequestId = "client-request-id";
+ }
}
internal static class EnvironmentVariables
diff --git a/src/Authentication/Authentication/Extensions/HttpRequestMessageExtensions.cs b/src/Authentication/Authentication.Core/Extensions/HttpRequestMessageExtensions.cs
similarity index 95%
rename from src/Authentication/Authentication/Extensions/HttpRequestMessageExtensions.cs
rename to src/Authentication/Authentication.Core/Extensions/HttpRequestMessageExtensions.cs
index d969c0cd3c5..38839276a7b 100644
--- a/src/Authentication/Authentication/Extensions/HttpRequestMessageExtensions.cs
+++ b/src/Authentication/Authentication.Core/Extensions/HttpRequestMessageExtensions.cs
@@ -1,4 +1,4 @@
-// ------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
@@ -6,7 +6,7 @@
using System.Net.Http;
using System.Threading.Tasks;
-namespace Microsoft.Graph.PowerShell.Authentication.Extensions
+namespace Microsoft.Graph.PowerShell.Authentication.Core.Extensions
{
internal static class HttpRequestMessageExtensions
{
@@ -67,4 +67,4 @@ internal static bool IsBuffered(this HttpRequestMessage httpRequestMessage)
return true;
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs b/src/Authentication/Authentication.Core/Http/AuthenticationHandler.cs
similarity index 92%
rename from src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
rename to src/Authentication/Authentication.Core/Http/AuthenticationHandler.cs
index e57d74186b3..9f25a15c177 100644
--- a/src/Authentication/Authentication/Handlers/AuthenticationHandler.cs
+++ b/src/Authentication/Authentication.Core/Http/AuthenticationHandler.cs
@@ -1,10 +1,9 @@
-// ------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
-
using Microsoft.Graph.Authentication;
-using Microsoft.Graph.PowerShell.Authentication.Extensions;
+using Microsoft.Graph.PowerShell.Authentication.Core.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -15,8 +14,12 @@
using System.Threading;
using System.Threading.Tasks;
-namespace Microsoft.Graph.PowerShell.Authentication.Handlers
+namespace Microsoft.Graph.PowerShell.Authentication.Core.Http
{
+ ///
+ /// A that authenticates outgoing requests and retries once on 401 with CAE claims.
+ /// This type lives in Authentication.Core so that Microsoft.Graph.Core / Azure.Identity types never leak into the cmdlet assembly.
+ ///
internal class AuthenticationHandler : DelegatingHandler
{
private const string ClaimsKey = "claims";
@@ -129,4 +132,4 @@ private static async Task DrainAsync(HttpResponseMessage response)
response.Dispose();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Authentication/Authentication.Core/Http/GraphHttpClientFactory.cs b/src/Authentication/Authentication.Core/Http/GraphHttpClientFactory.cs
new file mode 100644
index 00000000000..9ae1caca24b
--- /dev/null
+++ b/src/Authentication/Authentication.Core/Http/GraphHttpClientFactory.cs
@@ -0,0 +1,87 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using Microsoft.Graph.PowerShell.Authentication.Core.Interfaces;
+using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
+using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
+using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+
+namespace Microsoft.Graph.PowerShell.Authentication.Core.Http
+{
+ ///
+ /// Builds the Microsoft Graph pipeline.
+ /// The public surface of this type intentionally only exposes BCL types (, )
+ /// and types owned by this assembly, so that callers living in the default AssemblyLoadContext never bind to
+ /// Microsoft.Graph.Core, Microsoft.Kiota.* or Azure.* directly.
+ ///
+ public static class GraphHttpClientFactory
+ {
+ ///
+ /// Creates a pre-configured Microsoft Graph for the provided .
+ ///
+ /// The authentication context used to acquire tokens.
+ /// Retry/timeout settings.
+ ///
+ /// Optional handlers supplied by the caller. They are inserted after authentication and before the retry/redirect
+ /// handlers, in the order provided.
+ ///
+ ///
+ /// Optional handlers supplied by the caller that are appended after the retry/redirect handlers, in the order provided.
+ ///
+ ///
+ /// When true a with auto-redirect disabled and GZip/Deflate decompression is used
+ /// as the final handler (required on .NET Framework / Windows PowerShell).
+ ///
+ /// A configured .
+ public static HttpClient Create(
+ IAuthContext authContext,
+ IRequestContext requestContext,
+ IEnumerable customHandlers = null,
+ IEnumerable trailingHandlers = null,
+ bool useLegacyClientHandler = false)
+ {
+ if (authContext is null)
+ throw new AuthenticationException(ErrorConstants.Message.MissingAuthContext);
+ if (requestContext is null)
+ throw new AuthenticationException(string.Format(CultureInfo.InvariantCulture, ErrorConstants.Message.MissingSessionProperty, nameof(requestContext)));
+
+ var authProvider = AuthenticationHelpers.GetAuthenticationProviderAsync(authContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ var delegatingHandlers = new List
+ {
+ new AuthenticationHandler(authProvider)
+ };
+
+ if (customHandlers != null)
+ delegatingHandlers.AddRange(customHandlers);
+
+ delegatingHandlers.Add(new RetryHandler(new RetryHandlerOption
+ {
+ Delay = requestContext.RetryDelay,
+ MaxRetry = requestContext.MaxRetry,
+ RetriesTimeLimit = requestContext.RetriesTimeLimit
+ }));
+ delegatingHandlers.Add(new RedirectHandler());
+
+ if (trailingHandlers != null)
+ delegatingHandlers.AddRange(trailingHandlers);
+
+ HttpClient httpClient = useLegacyClientHandler
+ ? GraphClientFactory.Create(delegatingHandlers, finalHandler: new HttpClientHandler
+ {
+ AllowAutoRedirect = false,
+ AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
+ })
+ : GraphClientFactory.Create(delegatingHandlers);
+
+ httpClient.Timeout = requestContext.ClientTimeout;
+ return httpClient;
+ }
+ }
+}
diff --git a/src/Authentication/Authentication.Loader/GraphAssemblyLoadContext.cs b/src/Authentication/Authentication.Loader/GraphAssemblyLoadContext.cs
new file mode 100644
index 00000000000..047356f79e4
--- /dev/null
+++ b/src/Authentication/Authentication.Loader/GraphAssemblyLoadContext.cs
@@ -0,0 +1,147 @@
+// ------------------------------------------------------------------------------
+// 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.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
+
+namespace Microsoft.Graph.PowerShell.Authentication.Loader
+{
+ ///
+ /// A private that hosts Microsoft.Graph.Authentication.Core and all of its
+ /// third-party dependencies (Azure.Identity, Microsoft.Identity.Client, Microsoft.Kiota.*, Microsoft.Graph.Core, ...)
+ /// so that they never collide with copies loaded by other PowerShell modules in the default context.
+ ///
+ public sealed class GraphAssemblyLoadContext : AssemblyLoadContext
+ {
+ ///
+ /// Assemblies that must always be resolved from the default load context because their types are shared
+ /// with the cmdlet assembly, the PowerShell engine or the generated service modules.
+ ///
+ private static readonly HashSet s_sharedAssemblyNames = new HashSet(StringComparer.OrdinalIgnoreCase)
+ {
+ "Newtonsoft.Json",
+ "System.Management.Automation",
+ "Microsoft.PowerShell.Commands.Utility",
+ "Microsoft.PowerShell.Commands.Management",
+ "Microsoft.PowerShell.Security",
+ "Microsoft.PowerShell.ConsoleHost",
+ "Microsoft.Graph.Authentication",
+ "Microsoft.Graph.Authentication.Loader",
+ };
+
+ ///
+ /// Simple names of the assemblies that make up the shared framework (Trusted Platform Assemblies). These must always
+ /// come from the runtime, never from the module's Dependencies folder: loading e.g. the netstandard build of
+ /// System.Memory.dll into this context would create a second, incompatible ReadOnlyMemory<T> type.
+ ///
+ private static readonly HashSet s_trustedPlatformAssemblies = GetTrustedPlatformAssemblies();
+
+ private readonly string _dependencyFolder;
+ private readonly string _psEditionDependencyFolder;
+ private readonly string _nativeFolder;
+
+ public GraphAssemblyLoadContext(string dependencyFolder, string psEditionDependencyFolder)
+ : base(name: "Microsoft.Graph.Authentication", isCollectible: false)
+ {
+ _dependencyFolder = dependencyFolder ?? throw new ArgumentNullException(nameof(dependencyFolder));
+ _psEditionDependencyFolder = psEditionDependencyFolder ?? throw new ArgumentNullException(nameof(psEditionDependencyFolder));
+ _nativeFolder = Path.Combine(_dependencyFolder, "runtimes", GetRuntimeIdentifier(), "native");
+ }
+
+ ///
+ /// Gets the folder containing shared managed dependencies.
+ ///
+ public string DependencyFolder => _dependencyFolder;
+
+ ///
+ /// Gets the folder containing PowerShell edition specific managed dependencies.
+ ///
+ public string PSEditionDependencyFolder => _psEditionDependencyFolder;
+
+ ///
+ protected override Assembly Load(AssemblyName assemblyName)
+ {
+ if (s_sharedAssemblyNames.Contains(assemblyName.Name) || s_trustedPlatformAssemblies.Contains(assemblyName.Name))
+ {
+ // Defer to the default context so type identity is preserved across the boundary.
+ return null;
+ }
+
+ string path = ResolveManagedPath(assemblyName.Name);
+ return path != null ? LoadFromAssemblyPath(path) : null;
+ }
+
+ ///
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ foreach (string candidate in GetNativeCandidates(unmanagedDllName))
+ {
+ if (File.Exists(candidate))
+ {
+ return LoadUnmanagedDllFromPath(candidate);
+ }
+ }
+ return IntPtr.Zero;
+ }
+
+ ///
+ /// Attempts to resolve a managed assembly file for the given simple name from the module's dependency folders.
+ ///
+ /// Simple assembly name (without extension).
+ /// The full path when found; otherwise null.
+ public string ResolveManagedPath(string simpleName)
+ {
+ string fileName = simpleName + ".dll";
+
+ string path = Path.Combine(_psEditionDependencyFolder, fileName);
+ if (File.Exists(path))
+ return path;
+
+ path = Path.Combine(_dependencyFolder, fileName);
+ return File.Exists(path) ? path : null;
+ }
+
+ private IEnumerable GetNativeCandidates(string unmanagedDllName)
+ {
+ string fileName = unmanagedDllName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ? unmanagedDllName : unmanagedDllName + ".dll";
+ yield return Path.Combine(_nativeFolder, fileName);
+ yield return Path.Combine(_psEditionDependencyFolder, "runtimes", GetRuntimeIdentifier(), "native", fileName);
+ yield return Path.Combine(_psEditionDependencyFolder, fileName);
+ yield return Path.Combine(_dependencyFolder, fileName);
+ }
+
+ private static HashSet GetTrustedPlatformAssemblies()
+ {
+ var result = new HashSet(StringComparer.OrdinalIgnoreCase);
+ if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
+ {
+ foreach (string path in tpa.Split(Path.PathSeparator))
+ {
+ if (!string.IsNullOrEmpty(path))
+ result.Add(Path.GetFileNameWithoutExtension(path));
+ }
+ }
+ return result;
+ }
+
+ private static string GetRuntimeIdentifier()
+ {
+ string os = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "win"
+ : RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "osx"
+ : "linux";
+ string arch = RuntimeInformation.ProcessArchitecture switch
+ {
+ Architecture.X86 => "x86",
+ Architecture.Arm64 => "arm64",
+ Architecture.Arm => "arm",
+ _ => "x64",
+ };
+ return $"{os}-{arch}";
+ }
+ }
+}
diff --git a/src/Authentication/Authentication.Loader/GraphLoadContextInitializer.cs b/src/Authentication/Authentication.Loader/GraphLoadContextInitializer.cs
new file mode 100644
index 00000000000..918648b4074
--- /dev/null
+++ b/src/Authentication/Authentication.Loader/GraphLoadContextInitializer.cs
@@ -0,0 +1,80 @@
+// ------------------------------------------------------------------------------
+// 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.Reflection;
+using System.Runtime.Loader;
+
+namespace Microsoft.Graph.PowerShell.Authentication.Loader
+{
+ ///
+ /// Entry point used by the cmdlet assembly (via reflection, since it targets netstandard2.0) to set up the
+ /// isolated and hook the default context so that requests for
+ /// Microsoft.Graph.Authentication.Core are redirected into it.
+ ///
+ public static class GraphLoadContextInitializer
+ {
+ ///
+ /// Simple name of the engine assembly that is redirected into the isolated context.
+ ///
+ public const string CoreAssemblyName = "Microsoft.Graph.Authentication.Core";
+
+ private static readonly object s_lock = new object();
+ private static GraphAssemblyLoadContext s_context;
+
+ ///
+ /// The isolated context; null until has been called.
+ ///
+ public static GraphAssemblyLoadContext Context => s_context;
+
+ ///
+ /// Creates the isolated context and registers the default-context redirect. Safe to call multiple times.
+ ///
+ /// Path to the module's Dependencies folder.
+ /// Path to the module's Dependencies/Core folder.
+ public static void Initialize(string dependencyFolder, string psEditionDependencyFolder)
+ {
+ if (s_context != null)
+ return;
+
+ lock (s_lock)
+ {
+ if (s_context != null)
+ return;
+
+ s_context = new GraphAssemblyLoadContext(dependencyFolder, psEditionDependencyFolder);
+ AssemblyLoadContext.Default.Resolving += OnDefaultResolving;
+ }
+ }
+
+ ///
+ /// Unregisters the default-context redirect. The isolated context itself is not collectible and remains alive
+ /// for the lifetime of the process, which matches PowerShell's own behaviour for binary modules.
+ ///
+ public static void Shutdown()
+ {
+ lock (s_lock)
+ {
+ if (s_context == null)
+ return;
+ AssemblyLoadContext.Default.Resolving -= OnDefaultResolving;
+ }
+ }
+
+ private static Assembly OnDefaultResolving(AssemblyLoadContext defaultContext, AssemblyName assemblyName)
+ {
+ // Only the engine assembly is bridged into the default context. Everything else that Core needs is
+ // resolved by GraphAssemblyLoadContext.Load and stays invisible to other modules.
+ if (!string.Equals(assemblyName.Name, CoreAssemblyName, StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ var context = s_context;
+ if (context == null)
+ return null;
+
+ string path = context.ResolveManagedPath(assemblyName.Name);
+ return path != null ? context.LoadFromAssemblyPath(path) : null;
+ }
+ }
+}
diff --git a/src/Authentication/Authentication.Loader/Microsoft.Graph.Authentication.Loader.csproj b/src/Authentication/Authentication.Loader/Microsoft.Graph.Authentication.Loader.csproj
new file mode 100644
index 00000000000..a4b3419f9e8
--- /dev/null
+++ b/src/Authentication/Authentication.Loader/Microsoft.Graph.Authentication.Loader.csproj
@@ -0,0 +1,23 @@
+
+
+
+ 9.0
+ net6.0
+ Library
+ Microsoft.Graph.Authentication.Loader
+ Microsoft.Graph.PowerShell.Authentication.Loader
+ true
+ © Microsoft Corporation. All rights reserved.
+ 2.38.1
+
+ true
+
+
+ true
+ true
+
+
+ $(DefineConstants);SIGNED_BUILD
+
+
+
diff --git a/src/Authentication/Authentication.Loader/README.md b/src/Authentication/Authentication.Loader/README.md
new file mode 100644
index 00000000000..1fa5b15c772
--- /dev/null
+++ b/src/Authentication/Authentication.Loader/README.md
@@ -0,0 +1,96 @@
+# Microsoft.Graph.Authentication.Loader
+
+A tiny, dependency-free assembly that gives the `Microsoft.Graph.Authentication` PowerShell module its own
+`AssemblyLoadContext` on PowerShell 7+.
+
+## Why it exists
+
+`Microsoft.Graph.Authentication` depends on a number of assemblies that other PowerShell modules also ship
+(`Microsoft.Identity.Client`, `Azure.Identity`, `Azure.Core`, `Microsoft.Kiota.*`, `System.Text.Json`, ...). Historically
+those assemblies were loaded into the *default* load context. Whichever module was imported first "won", and the second
+module frequently failed with `FileLoadException` / `MissingMethodException` because a different version of the same
+assembly was already loaded.
+
+The loader fixes this by loading `Microsoft.Graph.Authentication.Core` (the engine) and all of its third-party
+dependencies into a **private** `AssemblyLoadContext`. Other modules never see them, and the module never sees theirs.
+
+## What lives where
+
+```
+Microsoft.Graph.Authentication/ (module root – default load context)
+├── Microsoft.Graph.Authentication.psd1 manifest (no NestedModules / RequiredAssemblies!)
+├── Microsoft.Graph.Authentication.psm1 root module – bootstraps the loader, then imports the dll
+├── Microsoft.Graph.Authentication.dll cmdlets only; may NOT reference isolated assemblies
+├── Newtonsoft.Json.dll shared with the generated service modules, stays in Default
+└── Dependencies/
+ ├── *.dll netstandard2.0 dependencies (Azure.Identity, MSAL, Kiota, ...)
+ ├── Core/ PowerShell 7+ only – loaded into the private context
+ │ ├── Microsoft.Graph.Authentication.Loader.dll ← this project
+ │ ├── Microsoft.Graph.Authentication.Core.dll
+ │ └── *.dll net6.0-specific dependencies
+ └── Desktop/ Windows PowerShell 5.1 only (net472 builds, no load contexts)
+```
+
+## How it works
+
+1. **`Microsoft.Graph.Authentication.psm1`** runs first (it is the `RootModule`). On PowerShell 7+ it does
+ `Assembly.LoadFrom("Dependencies/Core/Microsoft.Graph.Authentication.Loader.dll")` and calls
+ `GraphLoadContextInitializer.Initialize(dependencyFolder, coreDependencyFolder)`.
+2. **`GraphLoadContextInitializer`** creates a single `GraphAssemblyLoadContext` and subscribes to
+ `AssemblyLoadContext.Default.Resolving`. Whenever the default context fails to find
+ `Microsoft.Graph.Authentication.Core`, the handler returns the copy loaded in the private context.
+3. The psm1 then `Import-Module`s `Microsoft.Graph.Authentication.dll`. That assembly is loaded into the default
+ context by PowerShell, but its reference to `Microsoft.Graph.Authentication.Core` is satisfied by step 2 and
+ therefore resolves into the private context.
+4. **`GraphAssemblyLoadContext.Load`** is called for every assembly `Core` (or anything else in the context) needs:
+ - assemblies in `s_sharedAssemblyNames` (e.g. `Newtonsoft.Json`, `System.Management.Automation`) and every
+ **Trusted Platform Assembly** (the shared framework, e.g. `System.Memory`, `System.Text.Json`) return `null`,
+ which defers to the default context so type identity is preserved across the boundary;
+ - everything else is probed in `Dependencies/Core` first, then `Dependencies`, and loaded privately.
+5. **`GraphAssemblyLoadContext.LoadUnmanagedDll`** resolves native libraries (e.g. `msalruntime`) from the
+ `runtimes//native` folders next to the managed dependencies.
+6. On `Remove-Module`, `ModuleInitializer.OnRemove` calls `GraphLoadContextInitializer.Shutdown()` which
+ unsubscribes the `Resolving` handler. The context itself is non-collectible; assemblies stay loaded for the
+ lifetime of the process, as is normal for PowerShell binary modules.
+
+### Why the framework assemblies must be deferred
+
+`Dependencies/` contains netstandard2.0 facades such as `System.Memory.dll`. If the private context loaded those,
+`ReadOnlyMemory` inside the context would be a *different type* from the one in the runtime. Overrides of
+framework methods (e.g. `UnsafeTokenCacheOptions.RefreshCacheAsync`) would then fail with
+`TypeLoadException: ... does not have an implementation`. Checking `AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")`
+makes sure anything shipped by the runtime is always taken from the runtime.
+
+## Rules for the rest of the module
+
+- **No `NestedModules` / `RequiredAssemblies` in the manifest.** PowerShell processes those *before* the `RootModule`,
+ which would load the cmdlet dll (and try to bind `Core`) before the loader is initialised. `build-module.ps1` fails
+ the build if either is present.
+- **`Microsoft.Graph.Authentication.dll` must not reference isolated assemblies.** Types from `Azure.*`, `Microsoft.Identity.*`,
+ `Microsoft.Graph.Core`, `Microsoft.Kiota.*`, `System.Text.Json`, ... must stay behind APIs in `Core`. The xUnit test
+ `AssemblyIsolationTests` in `Authentication.Test` guards this.
+- **Script cmdlets cannot use type literals for `Core` types.** `[Microsoft.Graph.PowerShell.Authentication.GraphSession]`
+ is not resolvable from PowerShell because it lives in the private context. Use a type in the cmdlet assembly (e.g.
+ `GraphCommandCache`) or match on type name / resolve reflectively via the loaded assembly.
+- **This project must stay dependency-free** and target the lowest runtime the module supports on PowerShell 7 (`net6.0`).
+ It is loaded into the default context, so any package it referenced would be back in the shared space.
+
+## Windows PowerShell 5.1
+
+.NET Framework has no `AssemblyLoadContext`. The loader is not used there; instead the psm1 pre-loads
+`Dependencies/Desktop` and `Dependencies` with `Assembly.LoadFrom`, and `ModuleInitializer` keeps an
+`AppDomain.AssemblyResolve` handler as a fallback. Conflicts with other modules remain possible on 5.1, exactly as before.
+
+## Verifying isolation
+
+```powershell
+Import-Module ./artifacts/Microsoft.Graph.Authentication.psd1
+Connect-MgGraph -AccessToken (ConvertTo-SecureString -AsPlainText -Force '')
+[AppDomain]::CurrentDomain.GetAssemblies() |
+ Where-Object { $_.GetName().Name -match 'Identity|Azure|Kiota|Graph' } |
+ ForEach-Object { '{0,-45} {1}' -f $_.GetName().Name, [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext($_).Name }
+```
+
+Everything except `Microsoft.Graph.Authentication` and `Microsoft.Graph.Authentication.Loader` should report
+`Microsoft.Graph.Authentication` as its load context. `test/AssemblyIsolation.Tests.ps1` automates this, including a
+scenario where a conflicting `Microsoft.Identity.Client` is pre-loaded before importing the module.
diff --git a/src/Authentication/Authentication.Test/AssemblyIsolationTests.cs b/src/Authentication/Authentication.Test/AssemblyIsolationTests.cs
new file mode 100644
index 00000000000..247ec45b3ed
--- /dev/null
+++ b/src/Authentication/Authentication.Test/AssemblyIsolationTests.cs
@@ -0,0 +1,73 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using Microsoft.Graph.PowerShell.Authentication;
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.Graph.Authentication.Test
+{
+ ///
+ /// Guards the AssemblyLoadContext isolation boundary: the cmdlet assembly (Microsoft.Graph.Authentication.dll) is loaded
+ /// into the default load context by PowerShell, so it must never reference any of the assemblies that are isolated inside
+ /// the module's private load context. If this test fails, a type from an isolated dependency leaked into the cmdlet assembly
+ /// and must be moved behind an API in Microsoft.Graph.Authentication.Core.
+ ///
+ public class AssemblyIsolationTests
+ {
+ private static readonly string[] IsolatedAssemblyPrefixes =
+ {
+ "Azure.",
+ "Microsoft.Identity.",
+ "Microsoft.IdentityModel.",
+ "Microsoft.Graph.Core",
+ "Microsoft.Kiota.",
+ "System.Text.Json",
+ "System.ClientModel",
+ "System.Memory.Data",
+ };
+
+ [Fact]
+ public void CmdletAssemblyMustNotReferenceIsolatedDependencies()
+ {
+ var cmdletAssembly = typeof(ModuleInitializer).Assembly;
+
+ var leaked = cmdletAssembly.GetReferencedAssemblies()
+ .Select(a => a.Name)
+ .Where(name => IsolatedAssemblyPrefixes.Any(prefix => name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
+ .ToArray();
+
+ Assert.True(leaked.Length == 0,
+ $"Microsoft.Graph.Authentication.dll references isolated assemblies: {string.Join(", ", leaked)}. " +
+ "Move the offending code into Microsoft.Graph.Authentication.Core.");
+ }
+
+ [Fact]
+ public void CmdletAssemblyOnlyReferencesAllowedAssemblies()
+ {
+ var cmdletAssembly = typeof(ModuleInitializer).Assembly;
+
+ var allowed = new[]
+ {
+ "netstandard",
+ "mscorlib",
+ "System",
+ "System.Core",
+ "System.Management.Automation",
+ "Microsoft.Win32.Registry",
+ "Newtonsoft.Json",
+ "Microsoft.Graph.Authentication.Core",
+ };
+
+ var unexpected = cmdletAssembly.GetReferencedAssemblies()
+ .Select(a => a.Name)
+ .Where(name => !allowed.Contains(name, StringComparer.OrdinalIgnoreCase) && !name.StartsWith("System.", StringComparison.OrdinalIgnoreCase))
+ .ToArray();
+
+ Assert.True(unexpected.Length == 0,
+ $"Microsoft.Graph.Authentication.dll has unexpected references: {string.Join(", ", unexpected)}.");
+ }
+ }
+}
diff --git a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
index a95c90647b8..04b151eef23 100644
--- a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
+++ b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
@@ -26,5 +26,7 @@
+
+
\ No newline at end of file
diff --git a/src/Authentication/Authentication.sln b/src/Authentication/Authentication.sln
index 1e4002e7a0b..332afc7484b 100644
--- a/src/Authentication/Authentication.sln
+++ b/src/Authentication/Authentication.sln
@@ -11,32 +11,87 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Graph.Authenticat
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiInfoGenerator", "..\..\tools\OpenApiInfoGenerator\OpenApiInfoGenerator\OpenApiInfoGenerator.csproj", "{6437E29A-E398-48EE-B7BE-27A8C922F13E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Authentication.Loader", "Authentication.Loader", "{2C8F0C02-C7D9-E2CF-5DAF-C2631839B130}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Graph.Authentication.Loader", "Authentication.Loader\Microsoft.Graph.Authentication.Loader.csproj", "{47FC04F9-4C82-4058-A477-31CD115E320B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|x64.Build.0 = Debug|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Debug|x86.Build.0 = Debug|Any CPU
{44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|x64.ActiveCfg = Release|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|x64.Build.0 = Release|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|x86.ActiveCfg = Release|Any CPU
+ {44FF315A-27B2-4401-81A9-1912E6511EE6}.Release|x86.Build.0 = Release|Any CPU
{416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|x64.Build.0 = Debug|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Debug|x86.Build.0 = Debug|Any CPU
{416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|Any CPU.ActiveCfg = Release|Any CPU
{416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|Any CPU.Build.0 = Release|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|x64.ActiveCfg = Release|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|x64.Build.0 = Release|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|x86.ActiveCfg = Release|Any CPU
+ {416590B4-3A91-4B0D-9B40-3F69438B6D85}.Release|x86.Build.0 = Release|Any CPU
{50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|x64.Build.0 = Debug|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Debug|x86.Build.0 = Debug|Any CPU
{50050576-74B8-4507-B1FE-C47740BB3B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50050576-74B8-4507-B1FE-C47740BB3B71}.Release|Any CPU.Build.0 = Release|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Release|x64.ActiveCfg = Release|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Release|x64.Build.0 = Release|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Release|x86.ActiveCfg = Release|Any CPU
+ {50050576-74B8-4507-B1FE-C47740BB3B71}.Release|x86.Build.0 = Release|Any CPU
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|x64.Build.0 = Debug|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Debug|x86.Build.0 = Debug|Any CPU
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|x64.ActiveCfg = Release|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|x64.Build.0 = Release|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|x86.ActiveCfg = Release|Any CPU
+ {6437E29A-E398-48EE-B7BE-27A8C922F13E}.Release|x86.Build.0 = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|x64.Build.0 = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Debug|x86.Build.0 = Debug|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|x64.ActiveCfg = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|x64.Build.0 = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|x86.ActiveCfg = Release|Any CPU
+ {47FC04F9-4C82-4058-A477-31CD115E320B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {47FC04F9-4C82-4058-A477-31CD115E320B} = {2C8F0C02-C7D9-E2CF-5DAF-C2631839B130}
+ EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9871048-63A2-4F7A-9964-910674BD72A3}
EndGlobalSection
diff --git a/src/Authentication/Authentication/Handlers/RequestHeaderHandler.cs b/src/Authentication/Authentication/Handlers/RequestHeaderHandler.cs
index 7fcdbe2869a..e6ab9f41057 100644
--- a/src/Authentication/Authentication/Handlers/RequestHeaderHandler.cs
+++ b/src/Authentication/Authentication/Handlers/RequestHeaderHandler.cs
@@ -9,6 +9,7 @@
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
+using Headers = Microsoft.Graph.PowerShell.Authentication.Core.Constants.Headers;
namespace Microsoft.Graph.PowerShell.Authentication.Handlers
{
@@ -27,20 +28,20 @@ protected override Task SendAsync(HttpRequestMessage reques
{
string psSdkVersionHeader = string.Format(request.RequestUri.AbsolutePath.StartsWith("/beta") ? Constants.PSSDKHeaderValueBeta
: Constants.PSSDKHeaderValueV1, _assemblyInfo.Version.Major, _assemblyInfo.Version.Minor, _assemblyInfo.Version.Build);
- if (request.Headers.TryGetValues(CoreConstants.Headers.SdkVersionHeaderName, out IEnumerable previousSDKHeaders))
+ if (request.Headers.TryGetValues(Headers.SdkVersionHeaderName, out IEnumerable previousSDKHeaders))
{
var dotNetSdkHeader = previousSDKHeaders.Where(h => h.StartsWith(Constants.DotNetSDKHeaderValue, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
- request.Headers.Remove(CoreConstants.Headers.SdkVersionHeaderName);
- request.Headers.Add(CoreConstants.Headers.SdkVersionHeaderName, new[] { psSdkVersionHeader, dotNetSdkHeader });
+ request.Headers.Remove(Headers.SdkVersionHeaderName);
+ request.Headers.Add(Headers.SdkVersionHeaderName, new[] { psSdkVersionHeader, dotNetSdkHeader });
}
else
{
- request.Headers.Add(CoreConstants.Headers.SdkVersionHeaderName, psSdkVersionHeader);
+ request.Headers.Add(Headers.SdkVersionHeaderName, psSdkVersionHeader);
}
- if (request.Headers.Contains(CoreConstants.Headers.ClientRequestId))
- request.Headers.Remove(CoreConstants.Headers.ClientRequestId);
- request.Headers.Add(CoreConstants.Headers.ClientRequestId, Guid.NewGuid().ToString());
+ if (request.Headers.Contains(Headers.ClientRequestId))
+ request.Headers.Remove(Headers.ClientRequestId);
+ request.Headers.Add(Headers.ClientRequestId, Guid.NewGuid().ToString());
return base.SendAsync(request, cancellationToken);
}
diff --git a/src/Authentication/Authentication/Helpers/HttpHelpers.cs b/src/Authentication/Authentication/Helpers/HttpHelpers.cs
index e54612ba0b7..4d37a7b8ebd 100644
--- a/src/Authentication/Authentication/Helpers/HttpHelpers.cs
+++ b/src/Authentication/Authentication/Helpers/HttpHelpers.cs
@@ -1,15 +1,8 @@
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
// ------------------------------------------------------------------------------
-using Microsoft.Graph.Authentication;
-using Microsoft.Graph.PowerShell.Authentication.Core.Interfaces;
-using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
+using Microsoft.Graph.PowerShell.Authentication.Core.Http;
using Microsoft.Graph.PowerShell.Authentication.Handlers;
-using Microsoft.Kiota.Http.HttpClientLibrary.Middleware;
-using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Net;
using System.Net.Http;
namespace Microsoft.Graph.PowerShell.Authentication.Helpers
@@ -17,12 +10,16 @@ namespace Microsoft.Graph.PowerShell.Authentication.Helpers
///
/// A HTTP helper class.
///
+ ///
+ /// This type is compiled into the cmdlet assembly which lives in the default AssemblyLoadContext.
+ /// It must therefore only deal in BCL types and types owned by Microsoft.Graph.Authentication.Core;
+ /// all Microsoft.Graph.Core / Kiota / Azure.Identity wiring happens inside .
+ ///
public static class HttpHelpers
{
///
/// Creates a pre-configured Microsoft Graph .
///
- ///
///
public static HttpClient GetGraphHttpClient()
{
@@ -31,47 +28,23 @@ public static HttpClient GetGraphHttpClient()
var requestUserAgent = new RequestUserAgent(GraphSession.Instance.AuthContext?.PSHostVersion, null);
- AzureIdentityAccessTokenProvider authProvider = AuthenticationHelpers.GetAuthenticationProviderAsync(GraphSession.Instance.AuthContext).ConfigureAwait(false).GetAwaiter().GetResult();
- var newHttpClient = GetGraphHttpClient(authProvider, GraphSession.Instance.RequestContext);
+ var newHttpClient = GraphHttpClientFactory.Create(
+ GraphSession.Instance.AuthContext,
+ GraphSession.Instance.RequestContext,
+ customHandlers: new DelegatingHandler[]
+ {
+ new NationalCloudHandler(),
+ new ODataQueryOptionsHandler(),
+ new HttpVersionHandler()
+ },
+ trailingHandlers: new DelegatingHandler[]
+ {
+ new RequestHeaderHandler() // Should always be last.
+ },
+ useLegacyClientHandler: !RuntimeUtils.IsPsCore());
newHttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(requestUserAgent.UserAgent);
GraphSession.Instance.GraphHttpClient = newHttpClient;
return newHttpClient;
}
-
- ///
- /// Creates a pre-configured Microsoft Graph .
- /// with an
- ///
- /// Custom AuthProvider
- ///
- private static HttpClient GetGraphHttpClient(AzureIdentityAccessTokenProvider authProvider, IRequestContext requestContext)
- {
- if (requestContext is null)
- throw new AuthenticationException(string.Format(CultureInfo.InvariantCulture, Core.ErrorConstants.Message.MissingSessionProperty, nameof(requestContext)));
-
- IList delegatingHandlers = new List {
- new AuthenticationHandler(authProvider),
- new NationalCloudHandler(),
- new ODataQueryOptionsHandler(),
- new HttpVersionHandler(),
- new RetryHandler(new RetryHandlerOption{
- Delay = requestContext.RetryDelay,
- MaxRetry = requestContext.MaxRetry,
- RetriesTimeLimit= requestContext.RetriesTimeLimit
- }),
- new RedirectHandler(),
- new RequestHeaderHandler() // Should always be last.
- };
-
- HttpClient httpClient = RuntimeUtils.IsPsCore()
- ? GraphClientFactory.Create(delegatingHandlers)
- : GraphClientFactory.Create(delegatingHandlers, finalHandler: new HttpClientHandler
- {
- AllowAutoRedirect = false,
- AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
- });
- httpClient.Timeout = requestContext.ClientTimeout;
- return httpClient;
- }
}
}
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
index 9f481d84af0..606840c1cc7 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
@@ -21,7 +21,8 @@
-
+
+
@@ -52,18 +53,19 @@
-
+
+
+
-
-
+
+
-
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psm1 b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psm1
index 792636e9907..239a5f88844 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psm1
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psm1
@@ -1,4 +1,36 @@
+# On PowerShell 7+ set up the isolated AssemblyLoadContext *before* the module dll is loaded, so that
+# Microsoft.Graph.Authentication.Core and its dependencies (Azure.Identity, MSAL, Kiota, ...) never bind into the
+# default load context and cannot conflict with other modules. Windows PowerShell 5.1 has no load contexts, so there the
+# dependencies are pre-loaded with Assembly.LoadFrom (the ModuleInitializer AssemblyResolve handler remains as a fallback).
+$DependencyFolder = Join-Path $PSScriptRoot 'Dependencies'
+if ($PSEdition -eq 'Core') {
+ $CoreDependencyFolder = Join-Path $DependencyFolder 'Core'
+ $LoaderPath = Join-Path $CoreDependencyFolder 'Microsoft.Graph.Authentication.Loader.dll'
+ if (Test-Path -LiteralPath $LoaderPath) {
+ $null = [System.Reflection.Assembly]::LoadFrom($LoaderPath)
+ [Microsoft.Graph.PowerShell.Authentication.Loader.GraphLoadContextInitializer]::Initialize($DependencyFolder, $CoreDependencyFolder)
+ }
+}
+else {
+ $DesktopDependencyFolder = Join-Path $DependencyFolder 'Desktop'
+ # Desktop first: it contains the net472-specific builds which must take precedence over the netstandard ones.
+ $Loaded = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
+ foreach ($Folder in @($DesktopDependencyFolder, $DependencyFolder)) {
+ if (-not (Test-Path -LiteralPath $Folder)) { continue }
+ foreach ($AssemblyFile in Get-ChildItem -LiteralPath $Folder -Filter '*.dll' -File) {
+ if (-not $Loaded.Add($AssemblyFile.Name)) { continue }
+ try {
+ $null = [System.Reflection.Assembly]::LoadFrom($AssemblyFile.FullName)
+ }
+ catch {
+ # Native or otherwise unloadable files are skipped; AssemblyResolve will handle anything still missing.
+ Write-Debug "Skipped pre-loading '$($AssemblyFile.FullName)': $($_.Exception.Message)"
+ }
+ }
+ }
+}
+
# Load the module dll
$ModulePath = (Join-Path $PSScriptRoot 'Microsoft.Graph.Authentication.dll')
$null = Import-Module -Name $ModulePath
diff --git a/src/Authentication/Authentication/Models/GraphCommandCache.cs b/src/Authentication/Authentication/Models/GraphCommandCache.cs
new file mode 100644
index 00000000000..f9a54c30167
--- /dev/null
+++ b/src/Authentication/Authentication/Models/GraphCommandCache.cs
@@ -0,0 +1,20 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System.Collections;
+
+namespace Microsoft.Graph.PowerShell.Authentication.Models
+{
+ ///
+ /// Session-scoped cache of the command metadata used by Find-MgGraphCommand. Lives in the cmdlet assembly (default
+ /// AssemblyLoadContext) so that it stays visible to the module's script-based cmdlets; GraphSession itself lives in the
+ /// isolated load context and cannot be referenced from PowerShell type literals.
+ ///
+ public static class GraphCommandCache
+ {
+ public static Hashtable[] MgCommandMetadata { get; set; }
+
+ public static Hashtable[] MgLegacyCommandMapping { get; set; }
+ }
+}
diff --git a/src/Authentication/Authentication/ModuleInitializer.cs b/src/Authentication/Authentication/ModuleInitializer.cs
index e0d597687a5..66b32889721 100644
--- a/src/Authentication/Authentication/ModuleInitializer.cs
+++ b/src/Authentication/Authentication/ModuleInitializer.cs
@@ -13,65 +13,114 @@
namespace Microsoft.Graph.PowerShell.Authentication
{
///
- /// This class is used to load the dependencies of the module into the current AppDomain.
+ /// Bootstraps dependency loading for the module.
+ ///
+ /// -
+ /// On PowerShell 7+ (.NET) the Microsoft.Graph.Authentication.Loader assembly is loaded from Dependencies/Core
+ /// and asked to create a private AssemblyLoadContext. Only Microsoft.Graph.Authentication.Core is bridged into
+ /// the default context; Azure.Identity, MSAL, Kiota, Microsoft.Graph.Core, etc. stay invisible to other modules.
+ ///
+ /// -
+ /// On Windows PowerShell 5.1 (.NET Framework) there is no AssemblyLoadContext, so the historical
+ /// + approach is kept.
+ ///
+ ///
+ /// IMPORTANT: this type must not reference any type from Microsoft.Graph.Authentication.Core (directly or via
+ /// field/parameter/local types) because its static constructor runs before the load-context redirect is in place.
///
public class ModuleInitializer : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
+ private const string LoaderAssemblyFileName = "Microsoft.Graph.Authentication.Loader.dll";
+ private const string LoaderInitializerTypeName = "Microsoft.Graph.PowerShell.Authentication.Loader.GraphLoadContextInitializer";
+
private static readonly string s_dependencyFolder;
private static readonly string s_psEditionDependencyFolder;
private static readonly HashSet s_dependencies;
private static readonly HashSet s_psEditionDependencies;
- private static readonly AssemblyLoadContextProxy s_proxy;
+ private static readonly bool s_isPsCore;
+ private static readonly Type s_loaderInitializer;
static ModuleInitializer()
{
+ s_isPsCore = RuntimeUtils.IsPsCore();
s_dependencyFolder = Path.Combine(Path.GetDirectoryName(typeof(ModuleInitializer).Assembly.Location), "Dependencies");
- s_psEditionDependencyFolder = Path.Combine(s_dependencyFolder, RuntimeUtils.IsPsCore() ? "Core" : "Desktop");
+ s_psEditionDependencyFolder = Path.Combine(s_dependencyFolder, s_isPsCore ? "Core" : "Desktop");
s_dependencies = new HashSet(StringComparer.Ordinal);
s_psEditionDependencies = new HashSet(StringComparer.Ordinal);
- s_proxy = AssemblyLoadContextProxy.CreateLoadContext("msgraph-load-context");
- // Add shared dependencies.
- foreach (string filePath in Directory.EnumerateFiles(s_dependencyFolder, "*.dll"))
+ if (s_isPsCore)
{
- try
- {
- s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName);
- }
- catch (BadImageFormatException)
- {
- // Skip files without metadata.
- continue;
- }
+ // Register the isolated load context as early as possible: before any method that references
+ // Microsoft.Graph.Authentication.Core is JIT-compiled.
+ s_loaderInitializer = InitializeIsolatedLoadContext();
+ return;
}
- // 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"))
- {
- try
- {
- s_psEditionDependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName);
- }
- catch (BadImageFormatException)
- {
- // Skip files without metadata.
- continue;
- }
- }
+ // .NET Framework: index the dependency folders for the AssemblyResolve handler.
+ IndexDependencyFolder(s_dependencyFolder, s_dependencies);
+ IndexDependencyFolder(s_psEditionDependencyFolder, s_psEditionDependencies);
}
///
public void OnImport()
{
+ if (s_isPsCore)
+ return;
+
AppDomain.CurrentDomain.AssemblyResolve += ResolvingHandler;
}
///
public void OnRemove(PSModuleInfo psModuleInfo)
{
+ if (s_isPsCore)
+ {
+ s_loaderInitializer?.GetMethod("Shutdown", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null);
+ return;
+ }
+
AppDomain.CurrentDomain.AssemblyResolve -= ResolvingHandler;
}
+ ///
+ /// Loads the Loader assembly into the default context and calls GraphLoadContextInitializer.Initialize.
+ ///
+ /// The initializer type, or null when the loader could not be found.
+ private static Type InitializeIsolatedLoadContext()
+ {
+ string loaderPath = Path.Combine(s_psEditionDependencyFolder, LoaderAssemblyFileName);
+ if (!File.Exists(loaderPath))
+ {
+ throw new FileNotFoundException(
+ $"The Microsoft.Graph.Authentication module is incomplete: '{loaderPath}' was not found.", loaderPath);
+ }
+
+ Assembly loader = Assembly.LoadFrom(loaderPath);
+ Type initializer = loader.GetType(LoaderInitializerTypeName, throwOnError: true);
+ MethodInfo initialize = initializer.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(string), typeof(string) }, null);
+ initialize.Invoke(null, new object[] { s_dependencyFolder, s_psEditionDependencyFolder });
+ return initializer;
+ }
+
+ private static void IndexDependencyFolder(string folder, HashSet target)
+ {
+ if (!Directory.Exists(folder))
+ return;
+
+ foreach (string filePath in Directory.EnumerateFiles(folder, "*.dll"))
+ {
+ try
+ {
+ target.Add(AssemblyName.GetAssemblyName(filePath).FullName);
+ }
+ catch (BadImageFormatException)
+ {
+ // Skip files without metadata.
+ continue;
+ }
+ }
+ }
+
///
/// Checks to see if the requested assembly matches the assemblies in our dependencies folder.
/// The requesting assembly is always available in .NET, but could be null in .NET Framework.
@@ -125,7 +174,7 @@ private static string GetRequiredAssemblyPath(AssemblyName assemblyName)
///
- /// Resolves the assembly reference from the dependencies folder.
+ /// Resolves the assembly reference from the dependencies folder (.NET Framework only).
///
/// The source of the event.
/// The event data.
@@ -137,49 +186,12 @@ internal static Assembly ResolvingHandler(object sender, ResolveEventArgs args)
string filePath = GetRequiredAssemblyPath(assemblyName);
if (!string.IsNullOrEmpty(filePath))
{
- // - In .NET, load the assembly into the custom assembly load context.
- // - In .NET Framework, assembly conflict is not a problem, so we load the assembly
- // by 'Assembly.LoadFrom', the same as what powershell.exe would do.
- return s_proxy != null
- ? s_proxy.LoadFromAssemblyPath(filePath)
- : Assembly.LoadFrom(filePath);
+ // In .NET Framework, assembly conflict is not a problem, so we load the assembly
+ // by 'Assembly.LoadFrom', the same as what powershell.exe would do.
+ return Assembly.LoadFrom(filePath);
}
}
return null;
}
}
-
- ///
- /// An encapsulation of reflection API calls to create a custom AssemblyLoadContext. type is not available when targeting netstandard2.0 .NET Framework.
- ///
- internal class AssemblyLoadContextProxy
- {
- private readonly object _customContext;
- private readonly MethodInfo _loadFromAssemblyPath;
-
- private AssemblyLoadContextProxy(Type alc, string loadContextName)
- {
- var ctor = alc.GetConstructor(new[] { typeof(string), typeof(bool) });
- _loadFromAssemblyPath = alc.GetMethod("LoadFromAssemblyPath", new[] { typeof(string) });
- _customContext = ctor.Invoke(new object[] { loadContextName, false });
- }
-
- internal Assembly LoadFromAssemblyPath(string assemblyPath)
- {
- return (Assembly)_loadFromAssemblyPath.Invoke(_customContext, new[] { assemblyPath });
- }
-
- internal static AssemblyLoadContextProxy CreateLoadContext(string name)
- {
- if (string.IsNullOrEmpty(name))
- {
- throw new ArgumentNullException(nameof(name));
- }
-
- var alc = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyLoadContext");
- return alc != null
- ? new AssemblyLoadContextProxy(alc, name)
- : null;
- }
- }
}
diff --git a/src/Authentication/Authentication/build-module.ps1 b/src/Authentication/Authentication/build-module.ps1
index 44d580f4009..91443ce37ba 100644
--- a/src/Authentication/Authentication/build-module.ps1
+++ b/src/Authentication/Authentication/build-module.ps1
@@ -14,6 +14,7 @@ $copyExtensions = @('.dll', '.pdb')
# Source code locations
$coreSrc = Join-Path $PSScriptRoot "../$ModuleName.Core"
+$loaderSrc = Join-Path $PSScriptRoot "../$ModuleName.Loader"
$cmdletsSrc = Join-Path $PSScriptRoot "../$ModuleName"
# Generated output locations
@@ -54,6 +55,7 @@ if (-not $Isolated) {
# Clean build folders.
Write-Host -ForegroundColor Green 'Cleaning build folders...'
$null = Remove-Item -Path "$coreSrc/bin", "$coreSrc/obj" -Recurse -ErrorAction Ignore
+$null = Remove-Item -Path "$loaderSrc/bin", "$loaderSrc/obj" -Recurse -ErrorAction Ignore
$null = Remove-Item -Path "$cmdletsSrc/bin", "$cmdletsSrc/obj" -Recurse -ErrorAction Ignore
if ((Test-Path "$cmdletsSrc/bin") -or (Test-Path "$cmdletsSrc/obj")) {
@@ -69,6 +71,11 @@ dotnet publish -c $Configuration -f $netApp --verbosity quiet /nologo
dotnet publish -c $Configuration -f $netFx --verbosity quiet /nologo
Pop-Location
+# Build the AssemblyLoadContext loader (PowerShell 7+ only).
+Push-Location $loaderSrc
+dotnet publish -c $Configuration -f $netApp --verbosity quiet /nologo
+Pop-Location
+
# Build authentication.
Push-Location $cmdletsSrc
dotnet publish -c $Configuration --verbosity quiet /nologo
@@ -163,34 +170,52 @@ Copy-Item -Path "$cmdletsSrc/$ModulePrefix.$ModuleName-Help.xml" -Recurse -Desti
# Copy custom commands.
Copy-Item -Path "$cmdletsSrc/custom" -Recurse -Destination $outDir
-# Core assemblies to include with cmdlets (Let PowerShell load them).
+# Assemblies that are shared with the cmdlet assembly / generated service modules and therefore must live in the
+# module root (default AssemblyLoadContext). Everything else is isolated under Dependencies.
+$SharedAssemblies = @('Newtonsoft.Json')
+
+# The engine assembly. On PowerShell 7+ it is loaded into the isolated AssemblyLoadContext from Dependencies/Core,
+# on Windows PowerShell 5.1 from Dependencies/Desktop via AssemblyResolve. It must NOT be in the module root, otherwise
+# PowerShell would load it (and all of its dependencies) into the default context.
$CoreAssemblies = @('Microsoft.Graph.Authentication.Core')
# Copy each authentication.core asset to out directory and remember it.
$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) } |
+Where-Object { -not $CoreAssemblies.Contains($_.BaseName) -and -not $SharedAssemblies.Contains($_.BaseName) } |
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) } |
+Where-Object { -not $SharedAssemblies.Contains($_.BaseName) } |
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) } |
+Where-Object { -not $SharedAssemblies.Contains($_.BaseName) } |
ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDesktop -Recurse }
+# Copy the loader next to the PowerShell 7+ dependencies.
+Get-ChildItem -Path "$loaderSrc/bin/$Configuration/$netApp/publish/" |
+Where-Object { $_.BaseName -eq 'Microsoft.Graph.Authentication.Loader' -and $_.Extension -in $copyExtensions } |
+ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outCore }
+
+# Shared assemblies go to the module root.
+Get-ChildItem -Path "$coreSrc/bin/$Configuration/$netStandard/publish/" |
+Where-Object { $SharedAssemblies.Contains($_.BaseName) -and $_.Extension -in $copyExtensions } |
+ForEach-Object { [void]$Deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDir }
+
# 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 -Recurse }
-# Update module manifest with nested assemblies.
-$RequiredAssemblies = @(
- 'Microsoft.Graph.Authentication.dll',
- 'Microsoft.Graph.Authentication.Core.dll'
-)
-Update-ModuleManifest -Path (Join-Path $outDir "$ModulePrefix.$ModuleName.psd1") -NestedModules $RequiredAssemblies
+# NOTE: The cmdlet assembly is intentionally NOT declared in NestedModules/RequiredAssemblies. PowerShell processes those
+# before the RootModule (.psm1) runs, which would load Microsoft.Graph.Authentication.dll (and resolve its reference to
+# Microsoft.Graph.Authentication.Core) before the isolated AssemblyLoadContext hook is registered. The .psm1 sets up the
+# load context first and then imports the dll itself.
+$Manifest = Import-PowerShellDataFile (Join-Path $outDir "$ModulePrefix.$ModuleName.psd1")
+if ($Manifest.ContainsKey('NestedModules') -or $Manifest.ContainsKey('RequiredAssemblies')) {
+ Write-Error 'The module manifest must not declare NestedModules or RequiredAssemblies; the .psm1 loads the assemblies after initializing the AssemblyLoadContext.'
+}
Write-Host -ForegroundColor Green '-------------Done-------------'
diff --git a/src/Authentication/Authentication/custom/Find-MgGraphCommand.ps1 b/src/Authentication/Authentication/custom/Find-MgGraphCommand.ps1
index 43f53d14456..f4186f9af89 100644
--- a/src/Authentication/Authentication/custom/Find-MgGraphCommand.ps1
+++ b/src/Authentication/Authentication/custom/Find-MgGraphCommand.ps1
@@ -1,4 +1,4 @@
-# ------------------------------------------------------------------------------
+# ------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
# ------------------------------------------------------------------------------
Set-StrictMode -Version 2
@@ -33,12 +33,11 @@ Function Find-MgGraphCommand {
. "$PSScriptRoot/common/GraphUri.ps1" | Out-Null
# Read content of metadata file and cache in session object.
- if ($null -ne [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance -and
- $null -ne [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgCommandMetadata) {
+ if ($null -ne [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgCommandMetadata) {
Write-Debug "Reading MgCommandMetadata from session object."
}
else {
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgCommandMetadata = GraphCommand_ReadGraphCommandMetadata
+ [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgCommandMetadata = GraphCommand_ReadGraphCommandMetadata
}
function ResolveCommand {
@@ -50,16 +49,15 @@ Function Find-MgGraphCommand {
Write-Debug "Received Command: $Command"
# Read content of mapping file and cache in session object.
- if ($null -ne [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance -and
- $null -ne [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgLegacyCommandMapping) {
+ if ($null -ne [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgLegacyCommandMapping) {
Write-Debug "Reading MgLegacyCommandMapping from session object."
}
else {
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgLegacyCommandMapping = GraphCommand_ReadLegacyGraphCommandMapping
+ [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgLegacyCommandMapping = GraphCommand_ReadLegacyGraphCommandMapping
}
# Resolve legacy commands.
- [array]$ResolvedCommands = [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgLegacyCommandMapping | Where-Object LegacyMapping -Contains $Command
+ [array]$ResolvedCommands = [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgLegacyCommandMapping | Where-Object LegacyMapping -Contains $Command
if ($ResolvedCommands) {
$ResolvedCommands = $ResolvedCommands.Command
}
@@ -80,7 +78,7 @@ Function Find-MgGraphCommand {
$Result = @()
Write-Debug "Matching Command: $c"
Write-Debug "Matching ApiVersion: $ApiVersion"
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgCommandMetadata | ForEach-Object {
+ [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgCommandMetadata | ForEach-Object {
if ($_.ApiVersion -match $ApiVersion -and
$_.Command -match "^$c$" -or $_.CommandAlias -match "^$c$") {
$Result += [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommand]$_
@@ -141,7 +139,7 @@ Function Find-MgGraphCommand {
Write-Debug "Matching URI: $ResourceSegmentRegex"
Write-Debug "Matching Method: $Method"
Write-Debug "Matching ApiVersion: $ApiVersion"
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.MgCommandMetadata | ForEach-Object {
+ [Microsoft.Graph.PowerShell.Authentication.Models.GraphCommandCache]::MgCommandMetadata | ForEach-Object {
if ($_.Method -match $Method -and
$_.ApiVersion -match $ApiVersion -and
$_.Uri -match $ResourceSegmentRegex) {
diff --git a/src/Authentication/Authentication/custom/common/Permissions.ps1 b/src/Authentication/Authentication/custom/common/Permissions.ps1
index 963d9181e13..0de380547fb 100644
--- a/src/Authentication/Authentication/custom/common/Permissions.ps1
+++ b/src/Authentication/Authentication/custom/common/Permissions.ps1
@@ -54,8 +54,14 @@ function Permissions_GetPermissionsData([bool] $online) {
$_permissions.isFromInvokeMgGraphRequest = $true
}
}
- catch [System.Management.Automation.ValidationMetadataException], [System.Net.Http.HttpRequestException], [Microsoft.Graph.PowerShell.AuthenticationException] {
- if ( $online ) {
+ catch {
+ # [Microsoft.Graph.PowerShell.AuthenticationException] lives in the isolated AssemblyLoadContext and cannot be
+ # used as a type literal here, so match on the type name instead.
+ $exceptionType = $_.Exception.GetType()
+ $isExpected = $exceptionType.FullName -eq 'Microsoft.Graph.PowerShell.AuthenticationException' -or
+ $_.Exception -is [System.Management.Automation.ValidationMetadataException] -or
+ $_.Exception -is [System.Net.Http.HttpRequestException]
+ if ( -not $isExpected -or $online ) {
throw
}
# We can't get the data from MS Graph, so just use a local static (possibly stale) copy
diff --git a/src/Authentication/Authentication/test/AssemblyIsolation.Tests.ps1 b/src/Authentication/Authentication/test/AssemblyIsolation.Tests.ps1
new file mode 100644
index 00000000000..50340cf6015
--- /dev/null
+++ b/src/Authentication/Authentication/test/AssemblyIsolation.Tests.ps1
@@ -0,0 +1,109 @@
+# ------------------------------------------------------------------------------
+# Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+# ------------------------------------------------------------------------------
+[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification='Just an example for testing purposes.')]
+param()
+
+# These tests verify that, on PowerShell 7+, Microsoft.Graph.Authentication.Core and all of its third-party dependencies
+# (Azure.Identity, Microsoft.Identity.Client, Microsoft.Kiota.*, Microsoft.Graph.Core, ...) are loaded into the module's private
+# AssemblyLoadContext and never into the default context, so they can no longer conflict with other modules.
+# Each scenario runs in a fresh pwsh process so the load contexts are not polluted by previous tests.
+
+BeforeAll {
+ $ModuleName = "Microsoft.Graph.Authentication"
+ $ModulePath = (Resolve-Path (Join-Path $PSScriptRoot "..\artifacts\$ModuleName.psd1")).Path
+ $DummyToken = "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiVGVzdCIsIklzc3VlciI6Iklzc3VlciIsIlVzZXJuYW1lIjoiVGVzdCIsImV4cCI6MTkxMTIzMDM1NiwiaWF0IjoxNjc4NDg4ODE2fQ.yjRvogDyxlQrrQV3EaEsZJKhpYuNzaCyrh5Ip9WvdjU"
+
+ function Invoke-InFreshPwsh {
+ param([Parameter(Mandatory)][string]$Script)
+ $pwsh = (Get-Process -Id $PID).Path
+ $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Script))
+ $output = & $pwsh -NoProfile -NonInteractive -NoLogo -EncodedCommand $encoded 2>&1
+ if ($LASTEXITCODE -ne 0) {
+ throw "Child pwsh failed ($LASTEXITCODE):`n$($output -join "`n")"
+ }
+ return ($output | Where-Object { $_ -is [string] }) -join "`n" | ConvertFrom-Json
+ }
+
+ # Emits, as JSON, for each assembly of interest the name of the AssemblyLoadContext it was loaded into.
+ $ProbeScript = @"
+`$ErrorActionPreference = 'Stop'
+Import-Module '$ModulePath' -Force
+Connect-MgGraph -AccessToken (ConvertTo-SecureString -AsPlainText -String '$DummyToken' -Force) | Out-Null
+`$null = Get-MgContext
+# Force the HTTP pipeline (and therefore Kiota / Graph.Core) to be materialised.
+`$null = [Microsoft.Graph.PowerShell.Authentication.Helpers.HttpHelpers]::GetGraphHttpClient()
+`$names = 'Microsoft.Graph.Authentication','Microsoft.Graph.Authentication.Core','Microsoft.Graph.Authentication.Loader','Microsoft.Identity.Client','Azure.Identity','Azure.Core','Microsoft.Graph.Core','Microsoft.Kiota.Abstractions','Newtonsoft.Json'
+`$result = @{}
+foreach (`$asm in [AppDomain]::CurrentDomain.GetAssemblies()) {
+ `$n = `$asm.GetName().Name
+ if (`$names -contains `$n) {
+ `$alc = [System.Runtime.Loader.AssemblyLoadContext]::GetLoadContext(`$asm)
+ `$key = if (`$result.ContainsKey(`$n)) { "`$n#2" } else { `$n }
+ `$result[`$key] = `$alc.Name
+ }
+}
+`$result | ConvertTo-Json -Compress
+"@
+}
+
+Describe 'AssemblyLoadContext isolation' -Skip:($PSEdition -ne 'Core') {
+
+ It 'Should not ship Microsoft.Graph.Authentication.Core.dll in the module root' {
+ $root = Split-Path $ModulePath
+ Test-Path (Join-Path $root 'Microsoft.Graph.Authentication.Core.dll') | Should -BeFalse
+ Test-Path (Join-Path $root 'Dependencies\Core\Microsoft.Graph.Authentication.Core.dll') | Should -BeTrue
+ Test-Path (Join-Path $root 'Dependencies\Core\Microsoft.Graph.Authentication.Loader.dll') | Should -BeTrue
+ Test-Path (Join-Path $root 'Newtonsoft.Json.dll') | Should -BeTrue
+ }
+
+ It 'Should not declare NestedModules or RequiredAssemblies (the psm1 loads the dll after initializing the load context)' {
+ $manifest = Import-PowerShellDataFile $ModulePath
+ $manifest.ContainsKey('NestedModules') | Should -BeFalse
+ $manifest.ContainsKey('RequiredAssemblies') | Should -BeFalse
+ }
+
+ It 'Should load Core and its dependencies into the private load context' {
+ $contexts = Invoke-InFreshPwsh -Script $ProbeScript
+
+ $contexts.'Microsoft.Graph.Authentication' | Should -Be 'Default'
+ $contexts.'Microsoft.Graph.Authentication.Loader' | Should -Be 'Default'
+ $contexts.'Newtonsoft.Json' | Should -Be 'Default'
+
+ $contexts.'Microsoft.Graph.Authentication.Core' | Should -Be 'Microsoft.Graph.Authentication'
+ $contexts.'Microsoft.Identity.Client' | Should -Be 'Microsoft.Graph.Authentication'
+ $contexts.'Azure.Identity' | Should -Be 'Microsoft.Graph.Authentication'
+ $contexts.'Azure.Core' | Should -Be 'Microsoft.Graph.Authentication'
+ $contexts.'Microsoft.Graph.Core' | Should -Be 'Microsoft.Graph.Authentication'
+ $contexts.'Microsoft.Kiota.Abstractions' | Should -Be 'Microsoft.Graph.Authentication'
+ }
+
+ It 'Should keep working when a conflicting Microsoft.Identity.Client is already loaded in the default context' {
+ # Simulate another module (e.g. Az.Accounts / ExchangeOnlineManagement) that has already loaded a *different*
+ # Microsoft.Identity.Client into the default context by loading a stub assembly with that name first.
+ $stubSource = @'
+using System.Reflection;
+[assembly: AssemblyVersion("1.0.0.0")]
+namespace Microsoft.Identity.Client { public static class Stub { } }
+'@
+ $stubDir = Join-Path ([IO.Path]::GetTempPath()) ("mg-alc-" + [Guid]::NewGuid().ToString('N'))
+ New-Item -ItemType Directory -Path $stubDir | Out-Null
+ $stubPath = Join-Path $stubDir 'Microsoft.Identity.Client.dll'
+ Add-Type -TypeDefinition $stubSource -OutputAssembly $stubPath -OutputType Library | Out-Null
+
+ $script = @"
+`$ErrorActionPreference = 'Stop'
+`$null = [System.Reflection.Assembly]::LoadFrom('$stubPath')
+$ProbeScript
+"@
+ $contexts = Invoke-InFreshPwsh -Script $script
+
+ # Two copies of MSAL must coexist: the stub in Default, the real one in our context.
+ $msal = @($contexts.PSObject.Properties | Where-Object { $_.Name -like 'Microsoft.Identity.Client*' })
+ $msal.Count | Should -Be 2
+ ($msal.Value | Sort-Object) | Should -Be @('Default', 'Microsoft.Graph.Authentication')
+ $contexts.'Microsoft.Graph.Authentication.Core' | Should -Be 'Microsoft.Graph.Authentication'
+
+ Remove-Item $stubDir -Recurse -Force -ErrorAction Ignore
+ }
+}
diff --git a/src/Authentication/Authentication/test/Disconnect-MgGraph.Tests.ps1 b/src/Authentication/Authentication/test/Disconnect-MgGraph.Tests.ps1
index 5eb69807e8e..87bd3879f65 100644
--- a/src/Authentication/Authentication/test/Disconnect-MgGraph.Tests.ps1
+++ b/src/Authentication/Authentication/test/Disconnect-MgGraph.Tests.ps1
@@ -5,15 +5,25 @@
BeforeAll {
$ModuleName = "Microsoft.Graph.Authentication"
$ModulePath = Join-Path $PSScriptRoot "..\artifacts\$ModuleName.psd1"
- Import-Module $ModulePath -Force -ErrorAction SilentlyContinue
- $RandomId = (New-Guid).Guid
+ Import-Module $ModulePath -Force -ErrorAction SilentlyContinue
+ $RandomId = (New-Guid).Guid
- $MockAuthContext = New-Object Microsoft.Graph.PowerShell.Authentication.AuthContext -Property @{
- ClientId = $RandomId
- TenantId = $RandomId
- AuthType = [Microsoft.Graph.PowerShell.Authentication.AuthenticationType]::UserProvidedAccessToken
- TokenCredentialType = [Microsoft.Graph.PowerShell.Authentication.TokenCredentialType]::UserProvidedAccessToken
- }
+ # Microsoft.Graph.Authentication.Core is loaded into a private AssemblyLoadContext on PowerShell 7+, so its types
+ # cannot be referenced with type literals. Resolve them from the already-loaded assembly instead.
+ function Get-MgCoreType {
+ param([Parameter(Mandatory)][string]$TypeName)
+ $asm = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Microsoft.Graph.Authentication.Core' } | Select-Object -First 1
+ return $asm.GetType($TypeName, $true)
+ }
+ function Get-MgGraphSessionInstance {
+ return (Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.GraphSession').GetProperty('Instance').GetValue($null)
+ }
+
+ $MockAuthContext = [Activator]::CreateInstance((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.AuthContext'))
+ $MockAuthContext.ClientId = $RandomId
+ $MockAuthContext.TenantId = $RandomId
+ $MockAuthContext.AuthType = [Enum]::Parse((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.AuthenticationType'), 'UserProvidedAccessToken')
+ $MockAuthContext.TokenCredentialType = [Enum]::Parse((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.TokenCredentialType'), 'UserProvidedAccessToken')
}
Describe 'Disconnect-MgGraph' {
it 'Should have one ParameterSets' {
@@ -25,7 +35,7 @@ Describe 'Disconnect-MgGraph' {
It 'Should remove current AuthContext' {
{
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.AuthContext = $MockAuthContext
+ (Get-MgGraphSessionInstance).AuthContext = $MockAuthContext
$AuthContextBeforeDisconnect = Get-MgContext
Disconnect-MgGraph
@@ -34,7 +44,7 @@ Describe 'Disconnect-MgGraph' {
$AuthContextBeforeDisconnect.AuthType | Should -Be $MockAuthContext.AuthType
$AuthContextBeforeDisconnect.TokenCredentialType | Should -Be $MockAuthContext.TokenCredentialType
Get-MgContext | Should -BeNullOrEmpty
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.AuthContext | Should -BeNullOrEmpty
+ (Get-MgGraphSessionInstance).AuthContext | Should -BeNullOrEmpty
} | Should -Not -Throw
}
}
diff --git a/src/Authentication/Authentication/test/Find-MgGraphPermission.Tests.ps1 b/src/Authentication/Authentication/test/Find-MgGraphPermission.Tests.ps1
index 77ec125a9d7..0badeab9cb2 100644
--- a/src/Authentication/Authentication/test/Find-MgGraphPermission.Tests.ps1
+++ b/src/Authentication/Authentication/test/Find-MgGraphPermission.Tests.ps1
@@ -227,7 +227,7 @@ Describe "The Find-MgGraphPermission Command" {
BeforeEach {
_Permissions_Initialize
Mock Invoke-MgGraphRequest {
- Throw [Microsoft.Graph.PowerShell.AuthenticationException]::new('mock connection error message')
+ Throw [Activator]::CreateInstance((Get-MgCoreType 'Microsoft.Graph.PowerShell.AuthenticationException'), @('mock connection error message', $null))
}
}
diff --git a/src/Authentication/Authentication/test/loadEnv.ps1 b/src/Authentication/Authentication/test/loadEnv.ps1
index c5ed68dee1e..f9a760eddf3 100644
--- a/src/Authentication/Authentication/test/loadEnv.ps1
+++ b/src/Authentication/Authentication/test/loadEnv.ps1
@@ -21,14 +21,28 @@ if (Test-Path -Path (Join-Path $PSScriptRoot $envFile)) {
} else {
$envFilePath = Join-Path $PSScriptRoot '..\$envFile'
}
+# Microsoft.Graph.Authentication.Core is loaded into a private AssemblyLoadContext on PowerShell 7+, so its types cannot be
+# referenced with type literals like [Microsoft.Graph.PowerShell.Authentication.GraphSession]. Resolve them from the
+# already-loaded assembly instead.
+function Get-MgCoreType {
+ param([Parameter(Mandatory)][string]$TypeName)
+ $asm = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'Microsoft.Graph.Authentication.Core' } | Select-Object -First 1
+ if ($null -eq $asm) { throw 'Microsoft.Graph.Authentication.Core is not loaded. Import the module first.' }
+ return $asm.GetType($TypeName, $true)
+}
+
+function Get-MgGraphSessionInstance {
+ return (Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.GraphSession').GetProperty('Instance').GetValue($null)
+}
+
$env = @{}
if (Test-Path -Path $envFilePath) {
# Load dummy auth configuration. This is used to run Pester tests.
$env = Get-Content (Join-Path $PSScriptRoot $envFile) | ConvertFrom-Json -AsHashTable
- [Microsoft.Graph.PowerShell.Authentication.GraphSession]::Instance.AuthContext = New-Object Microsoft.Graph.PowerShell.Authentication.AuthContext -Property @{
- ClientId = $env.ClientId
- TenantId = $env.TenantId
- AuthType = [Microsoft.Graph.PowerShell.Authentication.AuthenticationType]::UserProvidedAccessToken
- TokenCredentialType = [Microsoft.Graph.PowerShell.Authentication.TokenCredentialType]::UserProvidedAccessToken
- }
+ $authContext = [Activator]::CreateInstance((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.AuthContext'))
+ $authContext.ClientId = $env.ClientId
+ $authContext.TenantId = $env.TenantId
+ $authContext.AuthType = [Enum]::Parse((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.AuthenticationType'), 'UserProvidedAccessToken')
+ $authContext.TokenCredentialType = [Enum]::Parse((Get-MgCoreType 'Microsoft.Graph.PowerShell.Authentication.TokenCredentialType'), 'UserProvidedAccessToken')
+ (Get-MgGraphSessionInstance).AuthContext = $authContext
}
\ No newline at end of file
From 8688dac3c9e05405abb133de0d66378c828e9d64 Mon Sep 17 00:00:00 2001
From: Microsoft Graph DevX Tooling
Date: Tue, 22 Sep 2026 15:09:01 -0700
Subject: [PATCH 2/2] test(auth): update identity broker version
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6ab1df75-2b8c-40d4-958b-6c66b03916b3
---
config/ModuleMetadata.json | 6 +++---
.../Microsoft.Graph.Authentication.Core.csproj | 10 +++++-----
.../Microsoft.Graph.Authentication.Test.csproj | 2 +-
.../Microsoft.Graph.Authentication-help.xml | 2 +-
.../Microsoft.Graph.Authentication.csproj | 2 +-
.../Microsoft.Graph.Authentication.dll-Help.xml | 4 ++--
.../Microsoft.Graph.Authentication.nuspec | 2 +-
.../Authentication/Microsoft.Graph.Authentication.psd1 | 4 ++--
.../docs/Microsoft.Graph.Authentication.md | 2 +-
src/Authentication/docs/Set-MgEnvironment.md | 2 --
10 files changed, 17 insertions(+), 19 deletions(-)
diff --git a/config/ModuleMetadata.json b/config/ModuleMetadata.json
index e95cfaad371..436c32ebfd7 100644
--- a/config/ModuleMetadata.json
+++ b/config/ModuleMetadata.json
@@ -27,15 +27,15 @@
"versions": {
"authentication": {
"prerelease": "",
- "version": "2.40.0"
+ "version": "2.40.1"
},
"beta": {
"prerelease": "",
- "version": "2.40.0"
+ "version": "2.40.1"
},
"v1.0": {
"prerelease": "",
- "version": "2.40.0"
+ "version": "2.40.1"
}
}
}
diff --git a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
index 264e81288d0..a5cb9d1e597 100644
--- a/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
+++ b/src/Authentication/Authentication.Core/Microsoft.Graph.Authentication.Core.csproj
@@ -4,7 +4,7 @@
9.0
netstandard2.0;net6.0;net472
Microsoft.Graph.PowerShell.Authentication.Core
- 2.38.1
+ 2.40.1
true
@@ -19,11 +19,11 @@
-
+
-
+
-
+
@@ -32,4 +32,4 @@
-
+
\ No newline at end of file
diff --git a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
index 04b151eef23..0c2039b63cc 100644
--- a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
+++ b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj
@@ -2,7 +2,7 @@
net8.0;net472
false
- 2.38.1
+ 2.40.1
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication-help.xml b/src/Authentication/Authentication/Microsoft.Graph.Authentication-help.xml
index 65bedc3ea34..e05ebc523a3 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication-help.xml
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication-help.xml
@@ -419,7 +419,7 @@ Get-MgUserActivityHistoryItem CrossDeviceExperiences GET /users
All
- To return all possible permissions rather than just those that match the SearchString parameter, specify the All parameter. The All parameter may also be used with the PermissionType to enumerate all application permissions or all delegated permissions.
+ To return all possible permissions rather than just those that match the SearchString parameter, specify the All parameter. The All parameter may also be used with the PermissionType to enumerate all applicaition permissions or all delegated permissions.
SwitchParameter
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj
index 6d5e8c5a4b5..bc4f38c64ba 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.csproj
@@ -10,7 +10,7 @@
Microsoft.Graph.Authentication.nuspec
© Microsoft Corporation. All rights reserved.
- 2.38.1
+ 2.40.1
true
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.dll-Help.xml b/src/Authentication/Authentication/Microsoft.Graph.Authentication.dll-Help.xml
index e4fdd92455e..6173e46513c 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.dll-Help.xml
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.dll-Help.xml
@@ -2629,7 +2629,7 @@ Canary https://login.microsoftonline.com https://microsoftgraph.com User-de
DisableLoginByWAM
- Disables sign in via Web Account Manager (WAM). Note: This only takes effect when using a custom ClientId. When using the default ClientId, WAM remains enabled regardless of this setting.
+ {{ Fill DisableLoginByWAM Description }}
Boolean
@@ -2656,7 +2656,7 @@ Canary https://login.microsoftonline.com https://microsoftgraph.com User-de
DisableLoginByWAM
- Disables sign in via Web Account Manager (WAM). Note: This only takes effect when using a custom ClientId. When using the default ClientId, WAM remains enabled regardless of this setting.
+ {{ Fill DisableLoginByWAM Description }}
Boolean
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
index 606840c1cc7..46123d5e3bd 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.nuspec
@@ -1,7 +1,7 @@
- 2.38.1
+ 2.40.1
Microsoft.Graph.Authentication
Microsoft Graph PowerShell authentication module
Microsoft
diff --git a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1 b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1
index b935343b604..cee73da237e 100644
--- a/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1
+++ b/src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1
@@ -3,7 +3,7 @@
#
# Generated by: Microsoft
#
-# Generated on: 7/16/2026
+# Generated on: 9/22/2026
#
@{
@@ -12,7 +12,7 @@
RootModule = './Microsoft.Graph.Authentication.psm1'
# Version number of this module.
-ModuleVersion = '2.38.1'
+ModuleVersion = '2.40.1'
# Supported PSEditions
CompatiblePSEditions = 'Core', 'Desktop'
diff --git a/src/Authentication/docs/Microsoft.Graph.Authentication.md b/src/Authentication/docs/Microsoft.Graph.Authentication.md
index 1c7747b5f6f..1ee6f68088e 100644
--- a/src/Authentication/docs/Microsoft.Graph.Authentication.md
+++ b/src/Authentication/docs/Microsoft.Graph.Authentication.md
@@ -1,6 +1,6 @@
---
Module Name: Microsoft.Graph.Authentication
-Module Guid: 883916f2-9184-46ee-b1f8-b6a2fb784cee
+Module Guid: c83afdbe-f069-4558-9535-5c7667b92146
Download Help Link: https://learn.microsoft.com/powershell/module/Microsoft.Graph.Authentication
Help Version: 1.0.0.0
Locale: en-US
diff --git a/src/Authentication/docs/Set-MgEnvironment.md b/src/Authentication/docs/Set-MgEnvironment.md
index 7404de1a830..0ad0dbaa3f1 100644
--- a/src/Authentication/docs/Set-MgEnvironment.md
+++ b/src/Authentication/docs/Set-MgEnvironment.md
@@ -128,11 +128,9 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
## INPUTS
### System.String
-
## OUTPUTS
### Microsoft.Graph.PowerShell.Authentication.Models.GraphEnvironment
-
## NOTES
## RELATED LINKS