Securely accessing Exchange Online PowerShell with privileged access (Exchange Administrator) – while MFA is enforced through Conditional Access? It has been a real headache for many admins!
For scheduled tasks or Azure Automation, connecting to Exchange Online PowerShell is a must for any scripted solution!
But with new security measures like Conditional Access. And MFA enforcement coming into their rightful place in most organizations, many of these scripts have broken.
A quick fix is just to exclude the account. Or set up conditions in Conditional Access to allow a non MFA connection for unattended scripts.
But connecting without exclusions and keeping the enforcement in place – is something that has driven many admins to tears.
UPDATE: Here is a requested example of how to do this with Managed Identities, which is a much better way.
ExchangeOnlineScripts/ConnectEXOwithMSIRunbookExample.ps1 at main · mardahl/ExchangeOnlineScripts (github.com)
Service Principals with Certificates to the rescue!
Service Principals or App registrations in Azure AD are secure modern authentication entities. They can give applications access to Microsoft Online Services and more!
This article will try to sum up what you should do to get a working secure connection to Exchange Online PowerShell. But it is written with the expectation that you are very familiar with PowerShell and Azure AD App Registrations.
I used to have a hacky workaround for this with a script in the PowerShell Gallery. And it is still there, but not needed anymore!
Here is the link to the now useless PowerShell Gallery script: https://www.powershellgallery.com/packages/AzureAutomationAgainstExchangeOnlineWithMFAEnabledAccount/1.0.0
The official ExchangeOnlineManagement V2 module now supports the use of Certificate-Based Authentication with Service Principals!
How to – Azure Automation
If you created your Azure Automation account with a “RunAs” account, it would already have a Service Principal with a certificate (that expires every year btw!).
This article won’t deal with the configuration of Azure Automation, but we cover that in several other articles like this one: Configuring Azure Automation.
So with A RunAs account in Azure Automation, you would first need to install the ExchangeOnlineManagement PowerShell Module into your Azure Automation Account.
Then you can start a new Runbook that can do all sorts of Exchange Online Magic – with this bit of PowerShell code:
$connection = Get-AutomationConnection -Name AzureRunAsConnection $tenantName = "mydomain.onmicrosoft.com" Connect-ExchangeOnline -CertificateThumbprint $connection.CertificateThumbprint -AppId $connection.ApplicationID -Organization $tenantName
In the above code, you would replace “mydomain.onmicrosoft.com” with your actual tenant name (Tenant ID is not supported!).
But the above code would be worthless without having done the following steps.
Configure the Service Principals permissions for unattended Exchange Online admin access
There are two things that need to be done in order for your Service Principal to have the correct access to Exchange Online PowerShell. If you don’t grant these permissions you will get access denied when connecting with the certificate.
- It needs to be a member of the Exchange Administrators Role
- It needs to have Application permissions with full access to Exchange Online.
This bit of PowerShell should take care of the permissions – you can run it with the Azure Cloud Shell – like I did!
You will need to have the ID of the App Registration handy and replace it in the code below!
<# Script to add service principal to Azure AD Role By @michael_mardahl - msendpointmgr.com Credits for parts of API Permissions script go to adamtheautomator.com #> Connect-AzureAD $appId = "b4xxxxe8-xxee-4a22-axx8-6xxxxxxx0d50" $servicePrincipal = Get-AzureADServicePrincipal -Filter "AppID eq `'$appId'" #Adding Role membership $roleDefinition = Get-AzureADDirectoryRole | Where-Object {$_.DisplayName -eq 'Exchange Administrator'} Add-AzureADDirectoryRoleMember -ObjectId $roleDefinition.ObjectId -RefObjectId $servicePrincipal.ObjectId #Adding API Permissions $api = (Get-AzureADServicePrincipal -Filter "AppID eq '00000002-0000-0ff1-ce00-000000000000'") $permission = $api.AppRoles | Where-Object { $_.Value -eq 'Exchange.ManageAsApp' } $apiPermission = [Microsoft.Open.AzureAD.Model.RequiredResourceAccess]@{ ResourceAppId = $api.AppId ; ResourceAccess = [Microsoft.Open.AzureAD.Model.ResourceAccess]@{ Id = $permission.Id ; Type = "Role" } } $Application = Get-AzureADApplication | Where-Object {$_.AppId -eq $appId} $Application | Set-AzureADApplication -ReplyUrls 'http://localhost' $Application | Set-AzureADApplication -RequiredResourceAccess $apiPermission
After the code has been run, you must go to the App registration page in Azure AD and grant admin consent so you can successfully connect to Exchange Online.
That’s it for getting up and running with Azure Automation against Exchange Online!
How to – Scheduled Tasks
If you are still running some of these scripts from On-Prem, I beg you to consider using Azure Automation. It even has a free option that is usually good enough for most small to midsize companies.
If you must use the task scheduler – for whatever reason. You must create the App Registration manually in Azure AD, and assign the permissions to it as described in the above PowerShell script.
You would then need to upload a certificate to be used during authentication. Luckily this can be a self-signed certificate!
This script can create the certificate on your local machine and upload it to the App Registration. You would then have access to Exchange Online PowerShell from your local machine because you need the corresponding private key. The Private key will be placed on the computer generating the certificate (you can export this to another machine if you like, using certlm.msc).
Create a self-signed certificate for app registration authentication
<# Script to create self-signed 10 year valid cert and upload to App Registration By @Michael_Mardahl - MSEndpointMgr.com #> $appId = "b4xxxxe8-xxee-4a22-axx8-6xxxxxxx0d50" # App Id from Azure AD that needs certificate auth $PfxCertPath = '.\MyAppAuth.pfx' #Place to store temporary cert file $CertificatePassword = '1234SecurePassword' #A password you choose to save the cert with $certificateName = 'AZEXOAutomateCert' #A certificate name you choose $ErrorActionPreference = 'Stop' #Requires -Module Az if (-not (Get-AzSubscription)){ try { Connect-AzAccount } catch { $_; exit 1 } } try { #Creating secure password string $SecurePassword = ConvertTo-SecureString -String $CertificatePassword -AsPlainText -Force #Creating 10 year valid self-signed cert $NewCert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My ` -DnsName $certificateName ` -Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider' ` -KeyAlgorithm RSA ` -KeyLength 2048 ` -NotAfter (Get-Date).AddYears(10) #Exporting cert to file Export-PfxCertificate -FilePath $PfxCertPath ` -Password $SecurePassword ` -Cert $NewCert -Force } catch { $_ exit 1 } #Configure required flags on the certificate $flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable ` -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet ` -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet # Load the certificate into memory $PfxCert = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($PfxCertPath, $CertificatePassword, $flags) -ErrorAction Stop #Upload cert to Azure App Registration $binCert = $PfxCert.GetRawCertData() $certValue = [System.Convert]::ToBase64String($binCert) New-AzADAppCredential -ApplicationId $appId -CertValue $certValue -StartDate $PfxCert.NotBefore -EndDate $PfxCert.NotAfter
You will need the thumbprint of the certificate to add to the following script, which should be at the start of your Scheduled Task PowerShell script. The thumbprint can be viewed in many ways, but the easy way to make sure you have the right one is to just look in the App Registration in Azure, and click on the “Certificates and Secrets” menu item – it will be right there.
$certThumbprint = "XXXXXXXXXXXXXXXXXXXX" $appId = "XXXXXXXXXXXXXXX" $tenantName = "mydomain.onmicrosoft.com" #Requires -Module ExchangeOnlineManagement Connect-ExchangeOnline -CertificateThumbprint $certThumbprint -AppId $appId -Organization $tenantName
That’s it for tips on how to get this working with Scheduled tasks!
Final words
Microsoft has come along way with the new PowerShell modules. However, granularity on the backend is still missing, and the true graph experience for Exchange Automation is still not there yet… We can only dream for now!
Please don’t hesitate to follow and reach out on LinkedIn and Twitter to discuss this and more automation goodness.
Thanks for reading!
dont have role Exchange Service Administrator in my tenant. What role should i be using?
Hi Nima,
I know these names can be confusing because they are not always aligned throughout the services. So if you don’t see Exchange Service Administrator, you should see Exchange Administrators instead.
Hi Michael
Great article!
I have question about Exchange Hybrid.
Do we have any smarter option than to use the Task Scheduler approach?
I don’t see any, but maybe there is something I’m missing 🙂
I have begun using Azure Automation extensively, the free allowance is enough in many cases.
Extending it with a hybrid worker can make it fit into almost any scenario!
Great stuff Michael, helpful to have the steps written out for EXO. I’m so happy this functionality is finally available!
I could not agree more Jop, Thanks 🙂
Sweet, and if I understand correctly each cmdlet/module (Connect-exchangeonline, connect-msgraph, connect-azuread and so on) needs to have corresponding -CertificateThumbprint as an option for this to work? I mean, even though this works unattended for Exchange online today if I wanted to do the same with the Intune Powershell SDK or AzureAD the engineers at MS would need to incorporate support for certbased auth there aswell?
Yes, that is correctly understood 🙂
Azure AD: https://docs.microsoft.com/en-us/powershell/azure/active-directory/signing-in-service-principal?view=azureadps-2.0
MS-Graph: has had this forever; we use it in many of our scripts – most recently, we started using MSAL.PS PowerShell library, which can support this.
Enjoy your automation efforts 😀