Using System.Management.Automation and an exported session, how do I specify credentials for an O365 call? - powershell

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.

I was able to accomplish this by editing the exported module.
First, export your O365 credentials:
Get-Credential | Export-Clixml -Path D:\Modules\O365Credential.xml
With that XML file now in hand, open the module for editing in your preferred IDE
Search for the function call PromptForCredential
Note that the call is a part of a larger call to the Set-PSImplicitRemotingSession cmdlet
Comment out that entire outer cmdlet call, e.g.:
Insert an uncommented copy of the call (optional: fix the broken indentation)
For the -Credential parameter, replace the value with $Credential
Above the new Set-PSImplicitRemotingSession call, add this line:
$Credential = Import-Clixml -Path D:\Modules\O365Credential.xml
Your finished code should look something like this:
Save the module
To test, open a new PowerShell prompt and import the updated module. Run Get-Mailbox. You should receive a list of the mailboxes hosted by your O365 tenant, without being prompted for credentials.
Note that the module uses and decrements a RunSpace connection against your quota. You'll want to close that connection gracefully when you're done with your session:
Get-PSSession | Where-Object { $_.ConfigurationName -eq 'Microsoft.Exchange' } | Remove-PSSession -ErrorAction SilentlyContinue
Now you can import and use the module under the System.Management.Automation namespace, as shown in the VB.NET code above.

Related

Passing active powershell Session to background jobs

I am writing a powershell script to manipulate Exchange Online mailboxes.
I want this script to run with background jobs in parallel, so I'm trying to use PoshRSJobs (https://github.com/proxb/PoshRSJob) to create the jobs.
My code is:
Connect-ExchangeOnline -Credentials ...
Start-RSJob -ModulesToImport ExchangeOnlineManagement -Throttle $ProcesosConcurrentes -InputObject $jobs -ScriptBlock {
./migra_buzon.ps1 ...
}
Where:
$jobs is an arraylist where I have the parameter of the mailboxes I want to operate with
migra_buzon.ps1 is another powershell scripts that operates over one specified mailbox
The problem I have when I run this way is that in the jobs I have the error:
The term 'Add-MailboxPermission' 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.
Although other commands like Get-EXOMailbox are working correctly.
Looking for help I found that the problem can be related with the session, so I changed my code to:
Connect-ExchangeOnline -Credentials ...
Start-RSJob -ModulesToImport ExchangeOnlineManagement -Throttle $ProcesosConcurrentes -InputObject $jobs -ScriptBlock {
$o365session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri "https://outlook.office365.com/powershell-liveid" -Credential $(Import-Clixml $Using:ExchangeCredentials) -Authentication "Basic" -AllowRedirection
Import-PSSession $o365Session -CommandName #('Add-MailboxPermission', 'Get-MailboxPermission')
./migra_buzon.ps1 ...
}
In this case, the problem I have is with the Exchange connection. After running a few jobs I'm getting the error:
[outlook.office365.com] Processing data from remote server outlook.office365.com failed with the following error message: Client did not get proper response from server. For more information, see the about_Remote_Troubleshooting Help topic.
Cannot validate argument on parameter 'Session'. The argument is null. Provide a valid value for the argument, and then try running the command again.
So my question is, what is the right way to run background jobs sharing the connection got in the main process?
Thanks
PS: I first tried to run jobs with Start-Job, but with this the problem is that each background job needs its own connection, so I got and maximum number of connections exceeded. And this is the reason I changed my code to Start-RSJob
It appears that you are hitting Exchange Online throttling limits.
If that indeed the case, you can try the following method.
How to relax PowerShell throttling
There is a relatively new customer facing way to increase or update PowerShell Throttling Policies.
Go to Microsoft 365 admin center.
Validate that you are logged in with the user that has the correct role assignment.
Click on the Need Help? Widget in the bottom right corner
Graphical user interface
Type Exchange PowerShell throttling in the search box and select “Temporarily update throttling policies for a migration”. Keep in mind that this is only applicable for 90 days. After 90 days, the throttles will return to back to the default values for that tenant.
MachSol offers Tenant management using a job engine, that allows you to do multiple operations using front-end and let the jobs handler take care of processing in background. You can give it a try:
https://www.machsol.com/machpanel-automation-for-microsoft-CSP-partners/

Import Module with an different user account

Are you able to import a module through PowerShell with a different user account? I am specifically attempting to import the ActiveDirectory module with a different account to the currently logged in one.
I don't want to go all out for the console though because I am attempting to use the current Outlook process to send an email after the part of the code is done, and if the entire console is elevated it will give a COM error (instance of PowerShell and Outlook are not elevated together).
The SMTP way of sending an email or through Send-Mail won't work as even though I can ping the SMTP server, I get the below error message, which from what I've read is because I am unable to communicate with the SMTP server appropriately?
Exception calling "Send" with "1" argument(s): "Failure sending mail."
At C:\Users\\Desktop\SCRIPT.ps1:64 char:9
+ $SMTP.Send($MSG)
+ ~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : SmtpException
You can't import a module with a different account as it doesn't work this way. You need to run the individual commands themselves with alternative credentials.
As you mentioned AD I've used Get-ADUser as an example but a lot of powershell commands have a Credential or PSCredential parameter of some kind, check the documentation to find out.
$Credentials = Get-Credential
Get-ADUser JohnSmith -Properties DistinguishedName -Credential $Credentials
This above example will prompt for credentials, but you can also save them in the script instead on entering them every time.
NOTE: Saving credentials in a file isn't secure so be careful what credentials you save and where you store them!
$Username = "DomainUserName"
$Password = "PlainPassword" | ConvertTo-SecureString -AsPlainText -Force
$Credentials = New-Object System.Management.Automation.PSCredential($Username ,$Password)
There are also other ways to save credentials, but that's too much to go into here.
The AD module for powershell is a wrapper around much of the .NET framework's System.DirectoryServices namespace of code.
.NET in turn is wrapped on top of the older COM ADSI component.
Because of this, it is possible to use windows cached credentials to handle the AD work without using the -Credential option.
If you cache a Windows Domain credential prior to running the script, the AD cmdlets will use those cached credentials to authenticate to the DC. Of course, there's no requirement to remove the cached credential...but realize it's static. if the password changes in the domain, you need to re-cache the cred.
The management of domain creds can be done by command line as well using the cmdkey.exe program that is present since Win7. Using this command line tool, you could set the windows credential just before you run your script, then remove the credential after.
Note that the use of the cached creds is based solely on the server name that the cmdlet will attempt to communicate. If you are not specifying a DC in your cmdlet calls, then it will use the %logonserver% environment variable.
The critical piece then is that the servername used by ADSI must match exactly in the credential cache. If the short name (server01) is used, then that must be in the cache. If the full dns name is used (server01.domain.com), then that must be in the cache. If you feel that your script may change to another server, then that server must be in the cache.

New-MailboxExportRequest don't work in remote PSsession

i often use the New-MailboxExportRequest 's command on an exchange server in powershell console, like this one :
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010;
New-MailboxExportRequest -Mailbox jadrego –filepath \\computer1\c$\test.pst -verbose
it works correctly. But if I run those commands in PS remote session like this one :
I use the same User (Domain Admin, Exchange Admin)
Invoke-Command -ComputerName vdiv03 -ScriptBlock {
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010;
New-MailboxExportRequest -Mailbox jadrego –filepath \\computer1\c$\test.pst
}
I obtain this error :
failed to comunicate with mailbox database
with -verbose
Loading the snapin like that isn't supported in Exchange 2010.
IMHO, you'd be much better off just leveraging the native remoting built into Exchange for management tasks.
$ExchangeServer = <exchange serer name>
$SessionParams =
#{
ConfigurationName = 'Microsoft.Exchange'
ConnectionURI = "http://$ExchangeServer/powershell/"
Authentication = 'Kerberos'
# Credential = $Creds
}
$Session = New-PSSession #SessionParams
Invoke-command -ScriptBlock {New-MailboxExportRequest -Mailbox jadrego –filepath \\computer1\c$\test.pst} -Session $Session
Remove-PSSession $Session
Set $ExchangeServer to the name of one of your Exchange 2010 servers. The account will need to be a member of the necessary RBAC role for the function you're performing, and you can uncomment the Credential parameter and provide alternate credentials for the session if you need to.
This will also elimnatat the need to have the management tools installed on the computer that's running the script, and the associated headaches of keeping it patched to the same level as what's on the servers.
If you're working interactively, or running a script that uses many Exchange cmdlets you can add the session creation to your profile, and do an Import-PSSession and you'll have proxy functions for the Exchange cmdlets available locally that you can use the same as if you'd loaded the snapin.
Import-PSSsession $Session
Some caveates to be aware of:
When you use implicit remoting like this, the account of the credentiaals used to establish the session will determine what capablilities you will have. What appear to be Exchange cmdlets added to the local session are actually proxy functions ( you can verify this using Get-Command). This set of proxy functions is created dynamically by Exchange when you initially establish the session and will be customized according to the RBAC roles the account making the connection belongs to. If it doesn't have permissions to perform given functions you will not get the proxy functions for those cmdlets, or functions may not have parameters for those functions.
The results you get back will not be the same as the same as the native objects returned if you used an EMS shell, or loaded the snapin. They will be deserialized objects, which means they may be missing methods and will lose some fidelity compared to the native objects. There will be very few instances where this will be an issue, or cannot by worked around.
Also be aware that when you use implicit remoting, updates are made under the authority of an Exchange system account, not your credentials. When you use the snapin, your account must have permission to update the Exchange properties stored in AD directly, and those changes will be logged in Windows audit logs (if enabled) as having been made by that account. When you use implicit remoting they will be recorded as being done by the Exchange service account. Exchange will record the details of the actual user account that made the request in it's admin audit log, and you can use Search-AdminAuditLog to find out when changes were made, and by who even if Windows audit logging is not enabled. If you use the snapin directly and do not have AD audit logging enabled you will lose that audit trail.

Validate Service Account Powershell

I want to write a Powershell script that will validate a large number of service accounts that was provided to me by my AD team. Not that I don't trust them but I want to cycle thru each domain username and password to see if it logs in or fails. I am looking for some suggestions so far my attempts have failed (see post http://tjo.me/fKtvPM).
Thanks
P.S. I don't have access to AD so I have to try to login using the credentials to test.
This is really hacky (ugly for least-privileged model), but if you know that all of the service accounts have access to a particular program / file, you can try to start a process using their credentials.
$cred = get-credential # however you're getting the info from AD team, pass it hear to get-credential
start-process powershell -argumentlist "-command","exit" -cred (get-credential)
$? # if $true, process started (and exited) successfully, else failed (either bad creds or account can't access powershell.exe
Unfortunately, since you can't query AD directly, I think any solution is going to be a bit of a hack, since by definition you're going to have to simulate logging in as the user account.

Get current user's credentials object in Powershell without prompting

I have a Powershell script that is going to be run through an automation tool against multiple servers.
It works fine on Windows machines, as the remote calls use the tool's service account without any need for prompting or exposing any credentials in code.
This script also runs against Linux machines via SSH using the SharpSSH package. SharpSSH does not automatically use the Powershell user's credentials but requires either a username and password, an RSA key file, or a PSCredential object.
I can't prompt for credentials using Get-Credential, because it's being run through the automation tool. I don't want to expose the username and password in code or have an RSA key sitting out there. I would like to construct a PSCredential object from the current Powershell user (the service account).
Trying [System.Net.CredentialCache]::DefaultNetworkCredentials shows a blank, and [System.Security.Principal.WindowsIdentity]::GetCurrent() doesn't provide the object or information I need.
Does anyone have a method for creating a PSCredential object from the current user? Or maybe a completely different alternative for this problem?
Many thanks!
The Windows API will not expose the information you need, which is why Powershell can't get to them. Its an intentional feature of the security subsystem. The only way for this to work is for the Linux machines to trust the calling machine, such as joining them to an Active Directory (or any kerberos setup really).
Aside from that, you'd need to store and pass this information somehow.
You could store the RSA key in the user's keystore and extract it at runtime (using the .NET Crypto/Keystore libs), so you aren't storing the key around with the code. That way the key itself would be protected by the OS and available only when the calling user was authenticated. You'd have one more thing to install, but may be the only way to achieve what you are aiming for.
"Trying [System.Net.CredentialCache]::DefaultNetworkCredentials shows a blank, and [System.Security.Principal.WindowsIdentity]::GetCurrent() doesn't provide the object or information I need."
You already have your answer. I use this to pass the currently logged in user's credentials along in several scripts:
$Credentials = [System.Net.CredentialCache]::DefaultNetworkCredentials
$Username = $Credentials.UserName
$Password = $Credentials.Password
If you try to dump them to any kind of readable output, those values are empty when you dump them (for obvious security reasons), however they do work where you need a PSCredential object.
How about encrypting the password using the service account's encryption key?
A quick example:
Run PowerShell as the service account, run the following and save the output to a text file (or embed it in the scheduled task call):
$String = '<PASSWORD>'
ConvertFrom-SecureString -SecureString (ConvertTo-SecureString -String $String -AsPlainText -Force)
Use the following in your scheduled task in order to decrypt and utilize the password:
$EncryptedString = '<ENCRYPTED PASSWORD FROM ABOVE>'
[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((ConvertTo-SecureString -String $EncryptedString)))
That should do the trick. You cannot reuse the encrypted password on a different computer, though, or if you for whatever reason destroy you local key store :)
Since you can get the password in plaintext from a credential object, I doubt you can get this without prompting.