To run Exchange Online PowerShell scripts without a user account, register an application in Microsoft Entra ID, give it the Exchange.ManageAsApp application permission with admin consent, upload the public key of a CSP-based certificate, and assign it an Exchange-capable role. The script then connects with Connect-ExchangeOnline -AppId <id> -CertificateThumbprint <thumbprint> -Organization contoso.onmicrosoft.com (or -Certificate with a certificate object), with no password, no MFA prompt and no service account.
Who this is for and what you will have at the end
This guide is for administrators and automation engineers who still run scheduled Exchange scripts under a user account with a stored password, or whose jobs broke when MFA or Web Account Manager reached them. By the end you will have:
- An app registration with only the permission Exchange Online PowerShell needs.
- A certificate that Exchange app-only authentication accepts, with its private key kept where the script runs.
- A least-privilege role assignment, either a built-in Entra role or a scoped Exchange role group.
- Working connection commands for a Windows server, for any platform with a certificate object, and for an Azure Automation runbook.
- A list of the limitations and errors to expect.
How app-only authentication works
The Exchange Online PowerShell module requests an app-only token using the application ID, the tenant (the Organization parameter) and the certificate. The application has a directory role assigned to it, the role appears in the access token, and Exchange Online uses that role information to build the session's role-based access control. There is no user in the flow, so there is no password to store, no MFA prompt, and no Web Account Manager broker, which the module uses only for user sign-ins.
| Method | Where it runs | Secret to manage | Notes |
|---|---|---|---|
| Certificate thumbprint | Windows only | Private key in the user certificate store | Simplest for an on-premises server |
Certificate object (-Certificate) | Any platform | Certificate fetched at run time | Works with Azure Automation certificate assets |
Certificate file (-CertificateFilePath) | Any platform | .pfx file plus its password | Microsoft notes there is no automated and secure way to supply the password |
| Managed identity | Azure Automation, Azure VMs | None | No certificate at all |
Version requirements are modest: Exchange Online PowerShell app-only connections need module 2.0.4 or later, and Security & Compliance PowerShell needs 3.0.0 or later. In practice, use a current module that matches your PowerShell version; Connect-ExchangeOnline errors and fixes has the support matrix.
Prerequisites
- Permission to register applications in Microsoft Entra ID and to grant tenant-wide admin consent.
- Permission to assign Microsoft Entra roles, or Exchange Online permissions to manage role groups if you plan to use custom role groups.
- The ExchangeOnlineManagement module, plus PowerShellGet and PackageManagement, on the machine that runs the script.
- Your tenant's primary
.onmicrosoft.comdomain. App-only connections must use it as theOrganizationvalue. - Microsoft Graph PowerShell if you want to assign roles from the command line.
Step 1: Register the application
- In the Azure portal, search for App registrations and select New registration.
- Enter a descriptive Name, for example
ExO PowerShell CBA. - Under Supported account types, keep Accounts in this organizational directory only (single tenant). Choose multitenant only for partner scenarios where the same app manages Exchange Online in customer tenants.
- Leave Redirect URI empty unless you need it, and select Register.
- On the Overview page, copy the Application (client) ID. This is the
AppIdvalue for the connection.
Step 2: Grant Exchange.ManageAsApp
- In the app, open API permissions and select Add a permission.
- On the APIs my organization uses tab, search for and select Office 365 Exchange Online. For Security & Compliance PowerShell, select Microsoft Exchange Online Protection instead; if the app connects to both, add both.
- Select Application permissions, expand Exchange, select Exchange.ManageAsApp, and then Add permissions.
- Select Grant admin consent for your organization and confirm. The status changes to Granted.
- For the default Microsoft Graph > User.Read delegated permission, select ... > Revoke admin consent. The app doesn't need it.
Microsoft 365 GCC High and DoD organizations should add the permission by editing the app manifest instead. The requiredResourceAccess entry for Exchange Online PowerShell looks like this:
"requiredResourceAccess": [
{
"resourceAppId": "00000002-0000-0ff1-ce00-000000000000",
"resourceAccess": [
{
"id": "dc50a0fb-09a3-484d-be87-e023b12c6440",
"type": "Role"
}
]
},
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope"
}
]
}
],00000002-0000-0ff1-ce00-000000000000 is the Office 365 Exchange Online resource and dc50a0fb-09a3-484d-be87-e023b12c6440 is the Exchange.ManageAsApp role; both GUIDs are the same in every tenant. Grant admin consent after saving the manifest.
Step 3: Create a certificate Exchange will accept
Exchange app-only authentication doesn't support Cryptography Next Generation (CNG) certificates, and modern Windows creates CNG certificates by default. You need a certificate whose key comes from a CSP provider. A self-signed certificate is fine, as is one from an internal PKI such as AD CS or a commercial CA; the only other requirement is an exportable private key (.pfx) and a public certificate (.cer).
Microsoft's recommended method uses New-SelfSignedCertificate with -KeySpec KeyExchange, run in an elevated session:
$cert = New-SelfSignedCertificate -DnsName 'contoso.com' -CertStoreLocation 'cert:\CurrentUser\My' -NotAfter (Get-Date).AddYears(1) -KeySpec KeyExchange
# Private key, password protected, for the machine or service that runs the script
$cert | Export-PfxCertificate -FilePath .\exo-automation.pfx -Password (Get-Credential).Password
# Public key, for the app registration
$cert | Export-Certificate -FilePath .\exo-automation.cer
$cert.ThumbprintIf the certificate will be uploaded to Azure Automation, note that Automation requires the provider Microsoft Enhanced RSA and AES Cryptographic Provider. New-SelfSignedCertificate accepts that name in its -Provider parameter, so add -Provider 'Microsoft Enhanced RSA and AES Cryptographic Provider' to the command above when you create a certificate for Automation.
Record the expiry date. When the certificate expires, connections fail until you upload a new public key, so put the renewal in your change calendar.
Step 4: Attach the certificate to the app
- In the app registration, open Certificates & secrets.
- Select Upload certificate, browse to the
.cerfile, and select Add. - Confirm the certificate appears under Certificates with the thumbprint you recorded.
Only the public key goes to Entra ID. Anyone holding the private key can act as the app with all of its permissions, so protect the .pfx file and its password like a privileged credential and delete working copies once the certificate is installed.
Step 5: Give the app only the permissions it needs
Exchange.ManageAsApp only lets the app connect. What it can do after connecting depends on roles. You have three options.
Option 1: A built-in Microsoft Entra role
Supported roles for Exchange Online PowerShell are Compliance Administrator, Exchange Administrator, Exchange Recipient Administrator, Global Administrator, Global Reader, Helpdesk Administrator, Security Administrator and Security Reader. Exchange Administrator and Global Administrator cover every Exchange Online PowerShell task, including recipient management and anti-spam, anti-malware and anti-phishing settings; Security Administrator doesn't cover those tasks. For read-only reporting, Global Reader is usually enough.
Assign the role in Microsoft Entra roles and administrators by selecting the role name, then Add assignments, then the app. Or use Microsoft Graph PowerShell:
Connect-MgGraph -Scopes RoleManagement.ReadWrite.Directory,Application.Read.All
$sp = Get-MgServicePrincipal -Filter "DisplayName eq 'ExO PowerShell CBA'"
$roleId = (Get-MgRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq 'Global Reader'").Id
New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $sp.Id -RoleDefinitionId $roleId -DirectoryScopeId '/'Option 2: A custom Exchange role group
Use a custom role group when you need to restrict the cmdlets the app can run or limit which recipients it can change with a write scope. Create the role group in Exchange Online first, then link the app's service principal into Exchange and add it:
Connect-MgGraph -Scopes AppRoleAssignment.ReadWrite.All,Application.Read.All
$app = Get-MgServicePrincipal -Filter "DisplayName eq 'ExO PowerShell CBA'"
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
New-ServicePrincipal -AppId $app.AppId -ObjectId $app.Id -DisplayName 'SP for ExO PowerShell CBA'
$sp = Get-ServicePrincipal -Identity 'SP for ExO PowerShell CBA'
Add-RoleGroupMember -Identity 'Contoso View-Only Recipients' -Member $sp.IdentityYou must be connected to Exchange Online PowerShell when you run New-ServicePrincipal; the command needs the app ID and object ID from Entra ID.
Option 3: Combine both
Role-based access control combines permissions from all sources. You can assign a narrow Entra role such as Exchange Recipient Administrator and add a custom role group for the few extra cmdlets the script needs.
Step 6: Connect from the script
On Windows, with the certificate installed in the current user's store of the account that runs the task:
Connect-ExchangeOnline -CertificateThumbprint 'A1B2C3D4E5F60718293A4B5C6D7E8F9012345678' -AppId '36ee4c6c-0812-40a2-b820-b22ebd02bce3' -Organization 'contoso.onmicrosoft.com' -ShowBanner:$false
Get-AcceptedDomain | Format-Table Name
Disconnect-ExchangeOnline -Confirm:$false-CertificateThumbprint works only on Windows. On Linux or macOS, or whenever the certificate is loaded from somewhere else at run time, pass an X509Certificate2 object with -Certificate. For Security & Compliance PowerShell, use the same parameters with Connect-IPPSSession; note that Connect-IPPSSession isn't currently available in PowerShell 7 on macOS or Linux, so Security & Compliance scripts need to run on Windows. In GCC High or DoD, add -ExchangeEnvironmentName O365USGovGCCHigh or O365USGovDoD to Connect-ExchangeOnline.
In an Azure Automation runbook
Upload the .pfx to the Automation account under Shared Resources > Certificates > Add a certificate, or with New-AzAutomationCertificate. In the runbook, the internal Get-AutomationCertificate cmdlet returns an X509Certificate2 object that you pass straight to -Certificate:
$cert = Get-AutomationCertificate -Name 'ExoAutomation'
Connect-ExchangeOnline -Certificate $cert -AppId '36ee4c6c-0812-40a2-b820-b22ebd02bce3' -Organization 'contoso.onmicrosoft.com' -ShowBanner:$false
Get-EXOMailbox -ResultSize 10 | Select-Object DisplayName,PrimarySmtpAddress
Disconnect-ExchangeOnline -Confirm:$falseImport the ExchangeOnlineManagement module into the account or runtime environment that the runbook uses. Azure Automation lists module 3.0.0 and later as a known source of job errors unless the PowerShellGet and PackageManagement modules are also imported explicitly, so add those two at the same time. Azure Automation supports PowerShell 7.6, 7.4 and 5.1 runbooks, and the module has its own minimums: 3.10.0 and later need PowerShell 7.6, versions 3.5.0 to 3.9.2 need 7.4, and every version runs on 5.1. Pick the runtime and module pair together.
If the job only ever runs inside Azure, consider a managed identity instead of a certificate. The setup grants the same Exchange.ManageAsApp app role and an Entra role to the Automation account's identity, and the runbook connects with Connect-ExchangeOnline -ManagedIdentity -Organization contoso.onmicrosoft.com. If the Automation account is part of a larger Azure estate, the Azure cloud migration playbook covers landing zone design.
Verification
- Run
Get-ConnectionInformationafter connecting and confirm it returns a connection for your tenant. - Run a cmdlet the role allows, such as
Get-AcceptedDomain, and one it shouldn't, such as aSet-cmdlet when you assigned Global Reader. The second should be unavailable or denied. - In the Microsoft Entra sign-in logs, open the Service principal sign-ins log, filter on the app and confirm successful sign-ins.
- Remove any old service account the script used, or at least remove its Exchange roles, once the app-only job has run successfully.
Troubleshooting
AADSTS700016 application not found in the directory. The AppId is wrong or the request went to the wrong tenant. Check the client ID and that -Organization is your primary .onmicrosoft.com domain.
AADSTS700027 client assertion failed signature validation. Entra ID couldn't validate the signed request from the app. Confirm that the thumbprint or certificate object in the script is the one whose public key is uploaded to the app, re-upload the .cer if you regenerated the certificate, and check that it hasn't expired.
Connection fails with a certificate you just created on Windows 11 or Windows Server. The certificate is probably CNG. Recreate it with -KeySpec KeyExchange (and the Automation provider name if needed) and upload the new public key.
Connect-IPPSSession shows a sign-in prompt with app-only parameters. Microsoft's documented workaround is to run $Global:IsWindows = $true before Connect-IPPSSession.
Cmdlets are missing after a successful connection. RBAC for the session comes from the app's roles, so the role doesn't include them. Add the right Entra role or role group, then disconnect and connect again.
Microsoft 365 Group membership cmdlets fail. New-UnifiedGroup, Remove-UnifiedGroup, Add-UnifiedGroupLinks and Remove-UnifiedGroupLinks don't support app-only authentication. Use Microsoft Graph for those operations.
eDiscovery cmdlets fail in Security & Compliance PowerShell. App-only authentication remains unsupported for Purview eDiscovery cmdlets such as New-ComplianceSearch and Start-ComplianceSearch. Move those automations to Microsoft Graph where APIs exist. For existing automations that can't move yet, Microsoft describes module 3.10.1 or later with the -EnableSearchOnlySession switch on Connect-IPPSSession as a way to keep them working, but the configuration stays unsupported.
The term 'Update-ModuleManifest' is not recognized when connecting from an application that hosts the Windows PowerShell SDK. Add -SkipLoadingFormatData to the connect command.
Checklist
- Single-tenant app registered; client ID recorded.
Exchange.ManageAsAppapplication permission added from the right API, admin consent granted, unused Graph consent revoked.- CSP certificate created, public key uploaded, private key stored only where the job runs, expiry date in the calendar.
- Least-privilege role assigned: Entra role, scoped role group, or both.
- Script connects with
-AppId,-Organizationset to the.onmicrosoft.comdomain, and-CertificateThumbprintor-Certificate. - Module version matches the PowerShell or Automation runtime version.
- Service principal sign-ins monitored; legacy service account retired.
References
- App-only authentication in Exchange Online PowerShell and Security & Compliance PowerShell
- Use Azure managed identities to connect to Exchange Online PowerShell
- Connect-ExchangeOnline cmdlet reference
- About the Exchange Online PowerShell module
- New-SelfSignedCertificate
- Manage certificates in Azure Automation
- Azure Automation runbook types
- Microsoft Entra authentication and authorization error codes