Home > Software design >  Calling Exchange PowerShell in C#
Calling Exchange PowerShell in C#

Time:05-24

I have an application that allows users who would not otherwise have access to Exchange to perform some delegate functions. This all works fine for users who are on standard domain-joined machines. However we are getting an increasing number of devices built by MEM (Intune) that, while managed according to policies, aren't domain-members. This causes an issue when I attempt to create an Exchange PowerShell session in code because of the Kerberos authentication used. This is an example of some code from the application:

internal static bool UpdateMailbox(string identity, out string reply)
{
    reply = string.Empty;
    try
    {
        string server = DatabaseManager.GetConfigOption("ExchangePowerShellURI"); // http://exc16-01.domain.com/PowerShell
        string uname = DatabaseManager.GetConfigOption("ADUser");  // [email protected]
        SecureString password = GetPassword(); // makes a SecureString for the user password
        PSCredential creds = new PSCredential(uname, password);
        WSManConnectionInfo connectionInfo = new WSManConnectionInfo(new Uri(server), "http://schemas.microsoft.com/powershell/Microsoft.Exchange", creds);
        connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Kerberos;
        using (Runspace rs = RunspaceFactory.CreateRunspace(connectionInfo))
        {
            rs.Open();
            using (PowerShell ps = PowerShell.Create())
            {
                ps.Runspace = rs;
                
                // do stuff with PowerShell
                
                return true;
            }
        }
    }
    catch (Exception ex)
    {
        reply = ex.Message;
        return false;
    }
}

I have thought about using certificate-based authentication instead, but I don't know if this will work with Exchange PowerShell, or how to alter my code to use that instead of Kerberos. I know I could change the WinRM clients/servers to allow unencrypted traffic and use Basic authentication, but I would rather not do this unless there is no other choice.

I'd be grateful for any advice on how I could get this to work for the non-domain-joined devices, even if it turns out that Basic is the only option.

If its relevant to anything I'm asking, we are using Exchange 2016 on-premises.

CodePudding user response:

OK after a lot of trial and error I have a solution that is working. Sharing the info here in case its useful for anyone else, though this does seem to be a bit of a niche requirement.

First, you need to make sure that WinRM on the Exchange server is configured for HTTPS and the Certificate authentication is enabled on the service. Running the quick config will enable the HTTPS listener, and the option to enable certificate authentication can be enabled with the command below:

Set-Item -Path WSMan:\localhost\Service\Auth\Certificate -Value $true

You will need a certificate issued for the purposes of computer identification issued to the FQDN of the Exchange server for this. Your mileage may vary, but I found that I had to use a cert from our Enterprise CA as self-signed certificates didn't work for some reason.

Next, create a new local user account on the Exchange server and add it to the Remote Management users group.

Next, we need a certificate issued to a domain user. Again, for me, self-signed ones didn't work and I had to use one from the Enterprise CA. The full PFX (including private key) will need to be installed on all client machines that want to make use of the solution. Once you have a certificate, export just the public part of it as a CER file and install it on the Exchange server in the Trusted People container of the machine store.

After this, we need to bind the new local user to the certificate that has been imported which can be done with the following PowerShell command:

New-Item -Path WSMan:\localhost\ClientCertificate -Subject <upn of the user it was issued to> -URI * -Issuer <thumbprint of the CA cert> -Credential (Get-Credential)

For the Issuer thumbprint, I used the thumbprint of the certificate next up in the chain of the one I imported.

After this, its on to code:

internal static bool UpdateMailbox(string identity, out string reply)
{
    reply = string.Empty;
    try
    {
        string psURI = DatabaseManager.GetConfigOption("ExchangePowerShellURI"); // http://exc16-01.domain.com/PowerShell
        string server = new Uri(psURI).Host;
        string uname = DatabaseManager.GetConfigOption("ADUser");  // [email protected]
        SecureString password = GetPassword(); // makes a SecureString for the user password
        PSCredential creds = new PSCredential(uname, password); // PowerShell credential object for later use
        string thumbPrint = DatabaseManager.GetConfigOption("CertificateThumbprint"); // thumbprint of the cert to use
        WSManConnectionInfo connectionInfo = new WSManConnectionInfo(new Uri($"https://{server}"), "", creds);
        connectionInfo.CertificateThumbprint = thumbPrint;
        connectionInfo.Port = 5986;
        connectionInfo.MaximumConnectionRedirectionCount = 5;
        connectionInfo.SkipCACheck = true;
        connectionInfo.SkipCNCheck = true;
        using (Runspace rs = RunspaceFactory.CreateRunspace(connectionInfo))
        {
            rs.Open();
            using (PowerShell ps = PowerShell.Create())
            {
                ps.Runspace = rs;
                
                // Add PowerShell credential object into our session as a variable to use later
                PSCommand addCreds = new PSCommand();
                addCreds.AddCommand("Set-Variable");
                addCreds.AddParameter("Name", "cred");
                addCreds.AddParameter("Value", creds);
                ps.Commands = addCreds;
                ps.Invoke();
                
                // Create a new session of the Exchange Management Shell in our session called $ra. Kerberos is OK here as the Exchange server is pointing to itself
                PSCommand addSnapin = new PSCommand();
                string newSession = $"$ra = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri {psURI} -Authentication Kerberos -Credential $cred";
                addSnapin.AddScript(newSession);
                ps.Commands = addSnapin;
                ps.Invoke();
                
                // Invoke a command in our Exchange session from earlier
                PSCommand setMbx = new PSCommand();
                string strSetMbx = $"{{ Set-Mailbox -Identity {identity} -GrantSendOnBehalfTo [email protected] }}";
                setMbx.AddScript($"Invoke-Command -ScriptBlock {strSetMbx} -Session $ra");
                ps.Commands = setMbx;
                var result = ps.Invoke();
                
                // Do any further processing
                
                return true;
            }
        }
    }
    catch (Exception ex)
    {
        reply = ex.Message;
        return false;
    }
}

In short, what I'm doing above is creating a new PowerShell session as the local user on the Exchange server via certificate auth. Adding the credentials of a domain user with permissions in Exchange into the session as a variable, then using them to create an Exchange PowerShell session that I can invoke commands into.

I hope this helps anyone with a similar requirement. Its a bit more fiddly than simply opting for Basic, but it keeps things more secure at the cost of a bit more overhead on certificates, especially if they're only 12 month ones as ours are.

  • Related