Forum Discussion

klightspeed's avatar
klightspeed
Copper Contributor
Jul 03, 2026

[Bug] ExchangeOnlineManagement uses incorrect TenantId in requests

An interaction between the ExchangeOnlineManagement powershell module and Microsoft.Identity.Client 4.83.0+ results in the REST API requests sent by the ExchangeOnlineManagement powershell module using realm.onmicrosoft.com in place of the expected Guid TenantId. This results in the dreaded Expired or Invalid pagination request after fetching one page.

As discussed in https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/6093, Microsoft.Identity.Client.AuthenticationResult.TenantId can no longer be trusted to contain either null or the expected Guid TenantId, but instead will generally contain the realm.onmicrosoft.com realm name as of Microsoft.Identity.Client 4.83.0

As version 3.10.0 of the ExchangeOnlineManagement powershell module now depends on Microsoft.Identity.Client 4.83.1, this version of the powershell module can no longer retrieve groups with more than 1000 members or enumerate the groups, contacts, recipients, etc. in a domain that has more than 1000 of each without running into the dreaded Expired or Invalid pagination request.

In theory the following Lib.Harmony patch encodes a potential fix for this issue (tested locally using ExchangeOnlineFetch 3.10.0 in a dotnet 10 program):

using System.Reflection.Emit;
using HarmonyLib;

[HarmonyPatch("Microsoft.Exchange.Management.AdminApiProvider.Authentication.TokenProviderUtils", "GetTokenInformation")]
static class Patch_TokenProviderUtils_GetTokenInformation
{
    private static Harmony? _harmony = null;

    public static void PatchOnce()
    {
        var asms = AppDomain.CurrentDomain.GetAssemblies();

        if (_harmony is null
            && asms.FirstOrDefault(e => e.GetName().Name == "Microsoft.Exchange.Management.AdminApiProvider") is { } asm
            && asm.GetType("Microsoft.Exchange.Management.AdminApiProvider.Authentication.TokenProviderUtils") is { } type
            && type.GetMethod("GetTokenInformation") is { } method)
        {
            var harmony = new Harmony("com.github.klightspeed.exchangeonlinemanagement.tenantidfix");
            harmony.Patch(
                method,
                transpiler: new HarmonyMethod(typeof(Patch_TokenProviderUtils_GetTokenInformation), nameof(Transpiler))
            );
            _harmony = harmony;
        }
    }

    static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
    {
        var matcher = new CodeMatcher(instructions);

        var get_TenantId = AccessTools.PropertyGetter("Microsoft.Identity.Client.AuthenticationResult:TenantId");
        var get_Organization = AccessTools.PropertyGetter("Microsoft.Exchange.Management.AdminApiProvider.Authentication.TokenProviderContext:Organization");

        // patch the following code snippet:
        //
        // if (IsCertificateBasedConnection(context))
        // {
        //    var upn = GetUPNForAppOnlyBasedConnection(context);
        //    var tenantId = JwtSecurityTokenUtils.GetTenantId(tokenAcquisitionResult.AccessToken);
        //    return TokenInformation.Create(
        //        upn,
        //        authorizationHeader,
        //        tokenAcquisitionResult.TenantId ?? tenantId ?? context.Organization,
        //        tokenAcquisitionResult.ExpiresOn
        //    );
        // }
        //
        // to
        //
        // if (IsCertificateBasedConnection(context))
        // {
        //    var upn = GetUPNForAppOnlyBasedConnection(context);
        //    var tenantId = JwtSecurityTokenUtils.GetTenantId(tokenAcquisitionResult.AccessToken);
        //    return TokenInformation.Create(
        //        upn,
        //        authorizationHeader,
        //        PatchedTenantId(tokenAcquisitionResult.TenantId, context) ?? tenantId ?? context.Organization,
        //        tokenAcquisitionResult.ExpiresOn
        //    );
        // }

        matcher.MatchStartForward(
            new CodeMatch(CodeInstruction.LoadArgument(0)),
            CodeMatch.Calls(get_TenantId),
            new CodeMatch(OpCodes.Dup),
            new CodeMatch(OpCodes.Brtrue_S),
            new CodeMatch(OpCodes.Pop)
        );

        if (matcher.IsValid)
        {
            matcher.Advance(2);
            matcher.Insert(
                CodeInstruction.LoadArgument(1),
                new CodeInstruction(OpCodes.Call, get_Organization),
                CodeInstruction.Call(() => PatchedTenantId(default, default!))
            );
        }

        return matcher.InstructionEnumeration();
    }

    static string? PatchedTenantId(string? tenantId, string? organization)
        => tenantId == organization ? null : tenantId;
}

 

2 Replies

  • Hi, this is a strong bug report, and I would definitely avoid using the Harmony patch as a production workaround even though it is useful proof of where the problem is.

     

    The safest short-term mitigation is probably to pin ExchangeOnlineManagement to the last version that did not pull in the affected MSAL behavior, if that works in your environment. I would also open or add to a GitHub/support issue with a minimal repro: module version, MSAL version, tenant type, connection method, command used, and the point where pagination fails.

     

    For large enumerations, you may be able to reduce impact by filtering into smaller result sets, but that is only a workaround. If the module is sending the realm name where the API expects the tenant GUID, the durable fix needs to come from the module owners, either by using the tenant value from the token claims or by not trusting `AuthenticationResult.TenantId` in that path.

    • klightspeed's avatar
      klightspeed
      Copper Contributor

      I would also open or add to a GitHub/support issue with a minimal repro: module version, MSAL version, tenant type, connection method, command used, and the point where pagination fails.

      It looks like Microsoft fixed it in 3.10.1-Preview1