Search code examples
c#powershelloauth-2.0office365powershell-remoting

Exchange Online Powershell BasicAuthToOAuth not working with WSManConnectionInfo


Our code currently uses WSManConnectionInfo class to connect to O365. We use basic auth and are trying to upgrade to modern auth. I turned off basic auth in my tenant. Following this guide here, https://www.michev.info/Blog/Post/2997/connecting-to-exchange-online-powershell-via-client-secret-fl..., I am able to connect successfully in PowerShell by getting an access token and using New-PSSession cmdlet. I use the following commands:

Add-Type -Path 'C:\Program Files\WindowsPowerShell\Modules\AzureAD\2.0.2.140\Microsoft.IdentityModel.Clients.ActiveDirectory.dll'
 
$authContext45 = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList " https://login.windows.net/mytenant.onmicrosoft.com"
$secret = Get-ChildItem cert://localmachine/my/thumbprint
$CAC = [Microsoft.IdentityModel.Clients.ActiveDirectory.ClientAssertionCertificate]::new(appId,$secret)
$authenticationResult = $authContext45.AcquireTokenAsync("https://outlook.office365.com",$CAC)

$token = $authenticationResult.Result.AccessToken
$Authorization = "Bearer {0}" -f $Token
$Password = ConvertTo-SecureString -AsPlainText $Authorization -Force
$Ctoken = New-Object System.Management.Automation.PSCredential -ArgumentList "OAuthUser@tenantGUID",$Password
 
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true -Credential $Ctoken -Authentication Basic -AllowRedirection -Verbose
Import-PSSession $Session

However, when I try to use C# WSManConnectionInfo to do the same thing, I get this strange error whenever I try to open the runspace:

System.Management.Automation.Remoting.PSRemotingTransportException HResult=0x80131501 Message=Connecting to remote server outlook.office365.com failed with the following error message : The WS-Management service cannot process the request. Cannot find the https://schemas.microsoft.com/powershell/Microsoft.Exchange session configuration in the WSMan: drive on the outlook.office365.com computer. For more information, see the about_Remote_Troubleshooting Help topic. Here is the code:

    public static Collection<PSObject> GetUsersUsingOAuthPublic()
    {
        var authContext = new AuthenticationContext("https://login.windows.net/mytenant.onmicrosoft.com");
        X509Store certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine);

        certStore.Open(OpenFlags.ReadOnly);
        X509Certificate2Collection certCollection = certStore.Certificates.Find(X509FindType.FindByThumbprint, certThumbprint, false);
        certStore.Close();
        var cac = new ClientAssertionCertificate(appId, certCollection[0]);
        var authResult = authContext.AcquireTokenAsync("https://outlook.office365.com", cac);

        var token = authResult.Result.AccessToken;
        string auth = string.Format("Bearer {0}", token);
        System.Security.SecureString password = new System.Security.SecureString();

        foreach (char c in auth)
        {
            password.AppendChar(c);
        }

        PSCredential psCredential = new PSCredential(string.Format("OAuthUser@{0}", tenantId), password);

        WSManConnectionInfo connectionInfo = new WSManConnectionInfo(
            new Uri("https://outlook.office365.com/powershell-liveid?BasicAuthToOAuthConversion=true"),
            "https://schemas.microsoft.com/powershell/Microsoft.Exchange",
            psCredential
            );
        connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
        connectionInfo.SkipCACheck = true;
        connectionInfo.SkipCNCheck = true;

        using (Runspace runspace = RunspaceFactory.CreateRunspace(connectionInfo))
        {
            return GetUserInformation(10, runspace);
        }
    }

I open the runspace like so:

    public static Collection<PSObject> GetUserInformation(int count, Runspace runspace)
    {
        using (PowerShell powershell = PowerShell.Create())
        {
            powershell.AddCommand("Get-Users");
            powershell.AddParameter("ResultSize", count);
            runspace.Open();
            powershell.Runspace = runspace;
            return powershell.Invoke();
        }
    }

The exception: Image of exception


Solution

  • We've been working on this issue as well and we were able to get it to work. In this test case we used a secret instead of a certificate for the accessToken, but that shouldn't matter.

    First difference is the uri should look like this:

    Uri psURI = new($"https://outlook.office365.com/powershell-liveid?BasicAuthToOAuthConversion=true&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40{msDomain}");
    

    The second change is the schema, with this change, you don't pass the full url for the schema, only the last part:

    WSManConnectionInfo connectionInfo = new(psURI, "Microsoft.Exchange", psCredential)
    {
        AuthenticationMechanism = AuthenticationMechanism.Basic,    
        SkipCACheck = true,
        SkipCNCheck = true
    };
    

    Instead of passing in "https://schemas.microsoft.com/powershell/Microsoft.Exchange" just pass in "Microsoft.Exchange"

    Here is the full code that allowed us to run Get-Mailbox against a tenant with basic auth disabled:

    string clientId = "";
    string tenantId = "";
    string secret = "";
    string upn = "[email protected]";
    string msDomain = "YOURDOMAIN.onmicrosoft.com";
    
    
    IConfidentialClientApplication thisApp = ConfidentialClientApplicationBuilder.Create(clientId)
        .WithClientSecret(secret)
        .WithAuthority($"https://login.windows.net/{msDomain}/")
        .Build();
    AuthenticationResult accessToken = await thisApp.AcquireTokenForClient(new[] { $"https://outlook.office365.com/.default" }).ExecuteAsync();
    
    //Bearer Header
    string auth = $"Bearer {accessToken.AccessToken}";
    SecureString password = GetSecureString(auth);
    
    PSCredential psCredential = new($"OAuthUser@{tenantId}", password);
    
    Uri psURI = new("https://outlook.office365.com/powershell-liveid?BasicAuthToOAuthConversion=true&email=SystemMailbox%7bbb558c35-97f1-4cb9-8ff7-d53741dc928c%7d%40{domain}");
    
    WSManConnectionInfo connectionInfo = new(psURI, "Microsoft.Exchange", psCredential)
    {
        AuthenticationMechanism = AuthenticationMechanism.Basic,    
        SkipCACheck = true,
        SkipCNCheck = true,
    };
    
    using Runspace runspace = RunspaceFactory.CreateRunspace(connectionInfo);
    runspace.Open();
    using PowerShell ps = PowerShell.Create();
    ps.Runspace = runspace;
    
    ps.AddCommand("Get-Mailbox");
    ps.AddParameter("Identity", upn);
    
    var results = await ps.InvokeAsync();