I'm building a small console app that uses constructs in the System.Management.Automation
namespace to connect to ExchangeOnline and perform various tasks. The overhead time of creating and importing a new session with each run during my dev & test is prohibitive.
Thus, I've elected to save the session to disk using Export-PSSession
. This all works fine from a PowerShell prompt, like so:
Import-Module ExchangeOnline
Get-Mailbox
I'm prompted for my credentials, and off we go.
Unfortunately, the same can't be said for running the same sequence under Automation
:
System.Management.Automation.MethodInvocationException: Exception calling "GetSteppablePipeline" with "1" argument(s): "Exception calling "PromptForCredential" with "4" argument(s): "A command that prompts the user failed because the host program or the command type does not support user interaction. The host was attempting to request confirmation with the following message: Enter your credentials for https://outlook.office365.com/powershell-liveid/.""
How do I send my credentials to O365 when using System.Management.Automation
?
This Q&A almost answers it, but not quite.
Here's my code.
Implementation
Friend Class Monad
Implements IDisposable
Public Sub New()
Me.SessionState = InitialSessionState.CreateDefault
Me.Monad = PowerShell.Create
End Sub
Public Sub ImportModule(Modules As String())
If Me.RunSpace.IsNotNothing Then
Me.RunSpace.Dispose()
Me.RunSpace = Nothing
End If
Me.SessionState.ImportPSModule(Modules)
Me.RunSpace = RunspaceFactory.CreateRunspace(Me.SessionState)
Me.RunSpace.Open()
Me.Invoker = New RunspaceInvoke(Me.RunSpace)
End Sub
Public Function ExecuteScript(Script As String) As Collection(Of PSObject)
Dim oErrors As Collection(Of ErrorRecord)
ExecuteScript = Me.Invoker.Invoke(Script)
oErrors = Me.Monad.Streams.Error.ReadAll
If oErrors.Count > 0 Then
Throw New PowerShellException(oErrors)
End If
End Function
Protected Overridable Sub Dispose(IsDisposing As Boolean)
If Not Me.IsDisposed Then
If IsDisposing Then
If Me.RunSpace.IsNotNothing Then Me.RunSpace.Dispose()
If Me.Invoker.IsNotNothing Then Me.Invoker.Dispose()
If Me.Monad.IsNotNothing Then Me.Monad.Dispose()
Me.RunSpace = Nothing
Me.Invoker = Nothing
Me.Monad = Nothing
End If
End If
Me.IsDisposed = True
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Me.Dispose(True)
End Sub
Private ReadOnly SessionState As InitialSessionState
Private IsDisposed As Boolean
Private RunSpace As Runspace
Private Invoker As RunspaceInvoke
Private Monad As PowerShell
End Class
Call
Friend Function GetMailbox() As IEnumerable(Of PSObject)
Using oMonad As New Monad
oMonad.ImportModule({"ExchangeOnline"})
Return oMonad.ExecuteScript("Get-Mailbox")
End Using
End Function
The ExchangeOnline module has some issues with that. It wants to be able to display an interactive modern authentication dialog, and there's no reliable way to stop it. You can feed it credentials, but it will barf if it needs to display an interactive dialog (as it does for MFA).
For storing and retrieving the credentials, you can use ConvertFrom-SecureString and Export-Csv or Export-CliXml as in the answer from InteXX, but that will stop working if basic authentication is disabled for the account you're using, or after 13 October 2020 when basic auth is disabled in exchange online (see KB4521831 ref https://support.microsoft.com/en-us/help/4521831/exchange-online-deprecating-basic-auth). Until then, you can also use a module like VaultCredential to manage credentials (note that it won't work across accounts).
So the next question is probably how you get a token for modern auth and how you present it to Exchange Online to authenticate with powershell. That's not hard at all.
You can bang against the endpoints with .NET web calls without too much effort, but it's easier to use one of the officially sanctioned libraries like ADAL.PS (deprecated) and MSAL.PS. Go ahead and Install-Module -Name MSAL.PS from the powershell gallery. Once it's there (and run Get-Module -Refresh -ListAvailable to make sure it can autoload) you can run:
$Token = Get-MsalToken -ClientId "a0c73c16-a7e3-4564-9a95-2bdf47383716" -RedirectUri "urn:ietf:wg:oauth:2.0:oob" -Scopes "https://outlook.office365.com/AdminApi.AccessAsUser.All","https://outlook.office365.com/FfoPowerShell.AccessAsUser.All","https://outlook.office365.com/RemotePowerShell.AccessAsUser.All"
You can also add "-LoginHint " (i.e. probably your email address) to skip the initial locator prompt, and it might help to add "-TenantId '00000000-0000-0000-0000-000000000000'" (except use the actual GUID for your Azure AD tenant).
Once a token is acquired, MSAL will keep it in its cache and automatically refresh it for you. This might make non-interactive use a little more difficult. You can review preferred ways to deal with it (https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens), or work with the libraries or interfaces at a lower level (such as using AcquireTokenSilentAsync()).
To actually use the token against Exchange Online, you need to use basic auth. It will still work even after they deprecate it, but it won't validate your account's password, it will only do other stuff like accept encoded tokens. Basically you need to Get-PsSession -Credential $EncodedBasicCredential where $EncodedBasicCredential is constructed with your UPN as the username and the Base64-encoded authorization header value as the password. For example:
$EncodedBasicCredential = [System.Management.Automation.PSCredential]::new($Token.account.username,(ConvertTo-SecureString -AsPlainText -Force -String ($Token.CreateAuthorizationHeader())))
Note that $Token.CreateAuthorizationHeader() just takes the value of $Token.AccessToken and prepends it with "Bearer ". Now all you need to do is create a New-PsSession with the appropriate ConnectionUri, ConfigurationName, and your credential object:
$ExchangeOnlineSession = New-PSSession -Name ExchangeOnline -ConnectionUri "https://outlook.office365.com/PowerShell-LiveId?BasicAuthToOAuthConversion=true" -ConfigurationName Microsoft.Exchange -Credential $EncodedBasicCredential -Authentication Basic
And you can import that to the parent session as you like:
$ExchangeOnlineModule = Import-PSSession -Session ($ExchangeOnlineSession) -WarningAction Ignore
To answer your specific question (how you specify credentials), it all depends on how you access your identity authority. If you are using ADFS on premises and your script runs on premises, then you should be able to run the process as the desired identity and Get-MsalToken will automatically use integrated windows authentication against ADFS without prompting you. If you are using PTA or native auth directly against Azure AD, then you'll need to look at creating a client application and using a secret or a certificate to authenticate against Azure AD to get your token.
I think this will be reasonably easy to translate from powershell to C#, but I'm a scripting sys admin, not a coder.