1

I used to use Basic authentication when connecting to Exchange online, but after Microsoft disabled the basic authentication I have not had luck connecting to it. Having it use authentication popup would be ideal.

Code looked something like this:


            PSCredential credential = new PSCredential(username, new NetworkCredential("", password).SecurePassword);

            WSManConnectionInfo wsEOConnInfo = new WSManConnectionInfo((new Uri("https://outlook.office365.com/powershell-liveid/")),
            "http://schemas.microsoft.com/powershell/Microsoft.Exchange", credential);

            wsEOConnInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;

            try
            {
                using (Runspace runspace = RunspaceFactory.CreateRunspace(wsEOConnInfo))
                {

                    runspace.Open();

                    try
                    {
                        if (runspace.RunspaceStateInfo.State == RunspaceState.Opened)
                        {
                            PowerShell ps = PowerShell.Create();

                            var getLists = new PSCommand();
                            getLists.AddCommand("Get-DistributionGroup");
                            ps.Commands = getLists;
                            ps.Runspace = runspace;
                            var response = ps.Invoke();
                        }
                   }
                }
           }

I tried replacing the Basic authentication part with Powershell calls, that should bring up external authentication, but that doesn't seem to work with the C# sdk.

Connect-ExchangeOnline -UserPrincipalName "email@email.com"

this just creates error, and even tried to run Install-Module and Import-Module commands, but in the SDK Install-Module command is unknown and Import doesn't help

The error is:

System.Management.Automation.CommandNotFoundException: 'The term 'Get-DistributionGroup' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.'

Is there any way to get access to Exchange Online commands inside C# or do I need to rewrite it all in PowerShell for it to work.

Jan
  • 91
  • 1
  • 1
  • 8
  • @mklement0 does that mean, I can take the source code from the comment, run it before as .ps1 script and have it install the missing modules for the Install and rest of commands to work? Or is there someway I can link the C# code on windows to use the _PowerShell (Core) 7+ session_ version so it would work with if you have manually ran the commands before – Jan Aug 12 '23 at 06:10
  • The error is not on the local machine. It is on the Exchange Server. The installation needs to be run on the remote machine. See : https://learn.microsoft.com/en-us/powershell/module/exchange/get-distributiongroup?view=exchange-ps – jdweng Aug 12 '23 at 10:28
  • Sorry, @Jan - I was too narrowly focused on the module-availability angle and missed the remoting and authentication part. What authentication methods are still supported? If using a _local_ runspace with the `ExchangeOnlineManagement` module is an option for you _and_ that module has already been installed in a local, stand-alone PowerShell (Core) 7+ installation, your .NET (Core) PowerShell SDK projects should automatically see it. If not, then, yes, you could try to use the module-downloading code shown in [this answer](https://stackoverflow.com/a/76700639/45375). – mklement0 Aug 12 '23 at 12:39
  • However, I can't speak to how to automate the connection part; [`Connect-ExchangeOnline`](https://learn.microsoft.com/en-us/powershell/module/exchange/connect-exchangeonline?view=exchange-ps) does have a `-Credential` parameter, however. – mklement0 Aug 12 '23 at 12:40
  • @jdweng what do you mean under remote machine? Using the commands as .ps1 file, everything works. Issue seems to come from the PowerShell sdk that c# uses, that it can't locate the module. Even when using Connect-ExhangeOnline, what brings up the Login prompt when running from .ps1 file, doesn't happen in C# code – Jan Aug 12 '23 at 20:33
  • @mklement0 I think the issue is still more about the module-availability as when running the C# code in local fails, even though the ExchangeOnlineManagement module is installed, as I can open powershell, run the Connect-ExchangeOnline and be promped with popup. Ill give the module-donwloading code a try. That shoul be saved as .ps1 file and then imported in C# code as Script to execute it and make it available for the runspace? – Jan Aug 12 '23 at 20:36
  • You are connecting to a remote machine using : Connect-ExchangeOnline The command Get-DistributionGroup is run on remote machine. – jdweng Aug 13 '23 at 01:59
  • @Jan: The PowerShell SDK doesn't support interactive features, so an attempt to call `Get-Credential` would fail, for instance, so I assume that `Connect-ExchangeOnline` fails for that reason. If you know that a stand-alone PowerShell (Core) installation exists, you could try to call its CLI (`pwsh.exe`) via a child process instead , possibly just for the credential part. Alternatively, roll your own credentials prompt in C#. – mklement0 Aug 13 '23 at 12:09
  • @mklement0 does the pwsh.exe way support it returning values it get? If I run the Get-DistributionGroup, and expect it to return the list of them. – Jan Aug 13 '23 at 18:19
  • @Jan, I suggest using C# to implement a custom credential prompt - such as shown in [this answer](https://stackoverflow.com/a/3404464/45375) - then you can pass the resulting `SecureString` instance via `.AddParameter('Credential', ...)` to a `Connect-ExchangeOnline` PowerShell SDK call. – mklement0 Aug 14 '23 at 01:13
  • 1
    @mklement0 Thank you so much for the help, got somekind of flow working now. Had to use the script on [this page](https://stackoverflow.com/questions/76692674/error-install-module-is-not-recognised-when-linux-azure-app-service-tries-to/76700639#76700639). Run it in as .AddScript() and then I was able to run rest of the commands inside C# code without issue. I also added the authentication inside the powershell install script, so it creates popup for user to log in. Thank you again! – Jan Aug 14 '23 at 06:21

1 Answers1

1

To summarize the problem and provide a sample solution:

  • You were looking for an alternative to using a remote PowerShell runspace, because your authentication method (Basic) is no longer supported.

  • Trying to use a local runspace with a call to Connect-ExchangeOnline to establish the remote connection encountered two problems:

    • Your PowerShell SDK project didn't see the required ExchangeOnlineManagement module, and such projects do not come bundled with Install-Module for on-demand installation.

      • The workaround from this answer solved that problem: it shows how to download individual modules directly from the PowerShell Gallery.
    • Additionally, PowerShell SDK projects do not support PowerShell's interactive features, such as Get-Credential and Read-Host, thereby preventing prompting for credentials.

      • Workarounds can take advantage of the fact that using native .NET features for user interaction is not subject to this limitation, so you can create your own credentials prompt and pass the result to PowerShell later.

      • For instance, this answer - whose code is integrated below - shows how to present a console-based, masked password prompt that returns a SecureString instance; alternatively, you can create a GUI dialog.

Simple sample code that demonstrates console-based up-front prompting for a password, constructing a PSCredential instance, and passing it to PowerShell, written for the PowerShell (Core) SDK (Microsoft.PowerShell.SDK and .NET 7+:

using System;
using System.Management.Automation;
using System.Security;

// Prompt for a password in the console.
// Note: You're free to create a GUI dialog instead.
var username = "jdoe";
SecureString securePass;
do {
    Console.Write($@"Enter password for user '{username}' (Ctrl-C to abort): ");
    securePass = GetPassword(); // See function definition below.
} while (securePass.Length == 0);

// Construct a PSCredential instance to pass to PowerShell,
// typically via a -Credential parameter.
var cred = new PSCredential(username, securePass);

using (var ps = PowerShell.Create()) // Create a local runspace.
{
    
    // Pass the PSCredential instance to a sample command.
    // Here, we simply output it as-is.
    ps.AddCommand("Write-Output").AddArgument(cred);

    // Execute the sample command.
    foreach (var o in ps.Invoke())
    {
        // This should echo the full type name of PSCredential, i.e.
        // 'System.Management.Automation.PSCredential'
        Console.WriteLine("Received from PowerShell: {0}", o);
    }
    // Print errors, if any.
    foreach (var e in ps.Streams.Error)
    {
        Console.Error.WriteLine(e);
    }
}

// Helper function that prompts for a password using masked input,
// and returns a SecureString instance.
// Source: https://stackoverflow.com/a/3404464/45375
static SecureString GetPassword()
{
    var pwd = new SecureString();
    while (true)
    {
        ConsoleKeyInfo i = Console.ReadKey(true);
        if (i.Key == ConsoleKey.Enter)
        {
            Console.WriteLine();    
            break;
        }
        else if (i.Key == ConsoleKey.Backspace)
        {
            if (pwd.Length > 0)
            {
                pwd.RemoveAt(pwd.Length - 1);
                Console.Write("\b \b");
            }
        }
        else if (i.KeyChar != '\u0000' ) // KeyChar == '\u0000' if the key pressed does not correspond to a printable character, e.g. F1, Pause-Break, etc
        {
            pwd.AppendChar(i.KeyChar);
            Console.Write("*");
        }
    }
    return pwd;
}

mklement0
  • 382,024
  • 64
  • 607
  • 775