Quick Connect (TL;DR)
If you have Exchange Administrator or Global Administrator rights, the full install-and-connect flow is two commands:
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com
A sign-in prompt opens for Microsoft 365 authentication, including any MFA challenge. When you are finished:
Disconnect-ExchangeOnline -Confirm:$false
You do not normally need Import-Module — PowerShell auto-loads the module when you call Connect-ExchangeOnline. Everything below covers the cases where one of these three commands does not behave.
Install-Module ExchangeOnlineManagement
This is the command that downloads the Exchange Online PowerShell module from the PowerShell Gallery:
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
-Scope CurrentUser installs into your own profile and does not require an elevated PowerShell session. Omit it and the module installs for all users, which does require a window opened with Run as administrator:
Install-Module -Name ExchangeOnlineManagement
What success looks like
A successful install prints nothing at all, or a progress bar followed by a return to the prompt. The first time you install anything from the gallery you also get a prompt about an untrusted repository — answer Y. To confirm the module actually landed:
Get-InstalledModule ExchangeOnlineManagement | Format-List Name,Version,InstalledLocation
The InstalledLocation value is the single most useful piece of output here. If it sits under \WindowsPowerShell\, it belongs to Windows PowerShell 5.1. If it sits under \PowerShell\, it belongs to PowerShell 7. That distinction causes more failed connections than anything else, and it has its own section below.
Prerequisites that actually block the install
-
Execution policy. If PowerShell refuses to run scripts you will see "Files cannot be loaded because running scripts is disabled on this system." Fix it once, from an elevated session:
Set-ExecutionPolicy RemoteSigned -
PowerShellGet and PackageManagement. REST-based connections on Windows require the PowerShellGet module, which in turn requires PackageManagement. Update them before installing the Exchange module for the first time, then close and reopen PowerShell. Preview builds of either module are a known source of connection failures:
Get-InstalledModule PackageManagement -AllVersions Get-InstalledModule PowerShellGet -AllVersions -
.NET Framework 4.7.2 is required in Windows PowerShell 5.1. Windows 11 and Windows Server 2019 and later already include a new enough version.
Common Install-Module ExchangeOnlineManagement errors
"No match was found for the specified search criteria and module name 'ExchangeOnlineManagement'" — the PowerShell Gallery is not registered as a repository. Re-register it:
Register-PSRepository -Default
"Unable to download from URI ..." or "PowerShellGetFormatVersion ... isn't supported by the current version of PowerShellGet" — your PowerShellGet is too old. Update PowerShellGet, close and reopen the PowerShell window, then retry the install.
TLS errors in Windows PowerShell 5.1 — the PowerShell Gallery requires TLS 1.2 or later, and older Windows builds do not negotiate it by default from the .NET Framework. Force it for the current session before installing:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
PowerShell 7 already uses TLS 1.2 or later, so this error is specific to 5.1.
ExchangeOnline Module Keeps Installing to PowerShell 5
This is the most common and most confusing failure in the whole workflow: you install the module, switch to PowerShell 7, and Connect-ExchangeOnline is not recognized. Or you install it repeatedly and it keeps ending up in a Windows PowerShell folder.
The cause
Install-Module has no concept of "install for PowerShell 7." It installs into the module path belonging to whichever PowerShell host you ran it from. Windows PowerShell 5.1 and PowerShell 7 are separate products with separate module directories:
| Scope | Windows PowerShell 5.1 | PowerShell 7 |
|---|---|---|
| CurrentUser | $HOME\Documents\WindowsPowerShell\Modules | $HOME\Documents\PowerShell\Modules |
| AllUsers | $env:ProgramFiles\WindowsPowerShell\Modules | $env:ProgramFiles\PowerShell\Modules |
| Shipped with the product | $env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules | $PSHOME\Modules |
So the module is not "escaping" to PowerShell 5. You are running Windows PowerShell 5.1 and have not noticed. That is easy to do, because almost every default entry point on Windows is still 5.1: the Start menu Windows PowerShell tile, the Windows PowerShell (Admin) entry on older builds, the ISE, powershell.exe in any script or shortcut, and the default integrated terminal in some VS Code configurations. PowerShell 7 is a separate install and its executable is pwsh.exe.
There is one extra wrinkle that makes the symptom look inconsistent. On Windows, PowerShell 7 inherits the machine-scoped module path, so it can usually see modules installed to the AllUsers Windows PowerShell folder (Program Files\WindowsPowerShell\Modules). It does not see the per-user Windows PowerShell folder (Documents\WindowsPowerShell\Modules). That is why an elevated AllUsers install sometimes appears to work in both hosts, while an -Scope CurrentUser install from 5.1 is invisible in PowerShell 7.
Diagnose it in three commands
# 1. Which PowerShell am I actually in? Desktop = 5.1, Core = PowerShell 7
$PSVersionTable.PSEdition
$PSVersionTable.PSVersion
# 2. Where did the module actually go?
Get-InstalledModule ExchangeOnlineManagement | Format-List Name,Version,InstalledLocation
# 3. Which folders does this host search?
$env:PSModulePath -split ';'
If step 2 reports a path that does not appear in step 3, you have found the problem: this host cannot see that copy of the module.
The fix
Open PowerShell 7 explicitly and install from there:
# From Start, Terminal, or Run — this is PowerShell 7, not powershell.exe
pwsh
# Confirm before installing
$PSVersionTable.PSEdition # must return: Core
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser
Get-InstalledModule ExchangeOnlineManagement | Format-List Name,Version,InstalledLocation
The reported location should now be under Documents\PowerShell\Modules. If you do not have PowerShell 7 at all, install it and then reopen your terminal:
winget install --id Microsoft.PowerShell
Two related traps worth knowing:
-
Module version versus PowerShell 7 version. Module 3.10.0 and later require PowerShell 7.6.0 or later, because of .NET 10 dependencies. Versions 3.5.0 through 3.9.2 require PowerShell 7.4.0 or later. If you are on PowerShell 7.4 or 7.5, install the newest module your host supports rather than the latest overall:
Install-Module -Name ExchangeOnlineManagement -RequiredVersion 3.9.2 -Scope CurrentUserWindows PowerShell 5.1 is unaffected by this — every module version works there.
-
OneDrive folder redirection. If Known Folder Move has redirected your Documents folder, the module installs into the OneDrive copy. Confirm where PowerShell thinks Documents is with
[Environment]::GetFolderPath('MyDocuments')before concluding the install failed.
If you genuinely want the module available to both hosts on a shared machine, install it once for all users from an elevated Windows PowerShell 5.1 session — PowerShell 7 will pick it up from Program Files\WindowsPowerShell\Modules — or simply install it separately in each host and keep both current.
Import-Module ExchangeOnlineManagement
Import-Module ExchangeOnlineManagement
This loads an already-installed module into the current session. It does not download anything; if the module is not installed, this command fails and Install-Module is what you need instead.
In most cases you can skip it. PowerShell auto-loads a module the first time you call one of its cmdlets, so Connect-ExchangeOnline pulls in the module by itself. Run Import-Module explicitly when you want to:
-
Surface load errors up front, before you are halfway through a script.
-
Pin a specific version when several are installed side by side:
Import-Module ExchangeOnlineManagement -RequiredVersion 3.9.2 -
Run under a host where auto-loading is disabled, such as some constrained runspaces and scheduled task configurations.
What success looks like
Nothing. A successful import is silent. To confirm the module is loaded in the current session and see which version won:
Get-Module ExchangeOnlineManagement
To list every version installed on the machine, whether loaded or not:
Get-Module ExchangeOnlineManagement -ListAvailable
When multiple versions are present on the path, PowerShell loads the highest version number by default — which is why a stale copy in another scope can quietly override the one you just installed.
Common Import-Module ExchangeOnlineManagement errors
"The specified module 'ExchangeOnlineManagement' was not loaded because no valid module file was found in any module directory" — the module is not installed for this host. Run the three diagnostic commands in the PowerShell 5 versus PowerShell 7 section before reinstalling; a reinstall from the wrong host will not help.
"Could not load file or assembly 'System.IdentityModel.Tokens.Jwt'" — the Exchange module is conflicting with another module already imported into the runspace. Open a fresh PowerShell window and import ExchangeOnlineManagement before anything else.
Import succeeds but cmdlets are missing — you have loaded an old version. Check Get-Module ExchangeOnlineManagement for the version number, then update and reopen the window.
Connect-ExchangeOnline
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com
This is the only supported way to open an Exchange Online PowerShell session. It uses modern authentication exclusively and is incapable of using basic authentication, so the same command covers accounts with and without MFA — there is no separate MFA parameter to add.
A sign-in window opens. In PowerShell 7, browser-based single sign-on is used by default, so the prompt appears in your default browser. In Windows PowerShell 5.1 it appears as a standalone dialog. Complete the password and any verification challenge, and control returns to your prompt.
If the sign-in stops with AADSTS50076: Due to a configuration change made by your administrator, a Conditional Access policy is demanding MFA that this flow cannot satisfy — typical on a headless machine, where -Device is the way to complete the challenge on a second device.
What success looks like
By default you get a banner, then the prompt returns with no error. The real confirmation is to ask for the session:
Get-ConnectionInformation
This returns the connection ID, the tenant, the user principal name, and the connection state. It exists because Get-PSSession does not report REST-based Exchange connections at all — if you are troubleshooting against an older guide that tells you to use Get-PSSession, that advice predates the current module. A quick functional test:
Get-AcceptedDomain
Useful Connect-ExchangeOnline parameters
| Parameter | What it does |
|---|---|
-UserPrincipalName | The admin account to sign in as, in user@domain.com form |
-ShowBanner:$false | Suppresses the startup banner |
-DelegatedOrganization | Connects to a customer tenant as a CSP, GDAP, or guest admin |
-ExchangeEnvironmentName | Selects a government or sovereign cloud |
-LoadCmdletHelp | Loads cmdlet help, which is no longer loaded by default in 3.7.0 and later |
-SkipLoadingFormatData | Avoids errors when connecting from inside a Windows service |
-DisableWAM | Disables Web Account Manager if you hit WAM-related sign-in errors (3.7.2 and later) |
-Credential | Non-MFA accounts only; pass a credential object instead of prompting |
Connect-ExchangeOnline in PowerShell 7 only
Two connection methods exist only in PowerShell 7. On a machine with no browser, authenticate from a second device:
Connect-ExchangeOnline -Device
The command prints a code, waits, and completes once you enter that code at https://microsoft.com/devicelogin on another device.
For accounts without MFA, prompt for credentials inside the PowerShell window rather than a popup:
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com -InlineCredential
Connect-ExchangeOnline to a customer tenant
For CSP, GDAP, and guest-admin scenarios:
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com -DelegatedOrganization customer.onmicrosoft.com
Connect-ExchangeOnline without a sign-in prompt
For scheduled tasks, runbooks, and CI pipelines, use app-only authentication. Never store an admin password in a script.
Connect-ExchangeOnline `
-CertificateThumbPrint "ABCD1234567890ABCD1234567890ABCD12345678" `
-AppId "12345678-1234-1234-1234-123456789012" `
-Organization "yourdomain.onmicrosoft.com"
-CertificateThumbPrint is supported on Windows only. On macOS and Linux, pass the certificate file directly:
Connect-ExchangeOnline `
-CertificateFilePath "/opt/automation/exo-cert.pfx" `
-CertificatePassword (ConvertTo-SecureString -String "<Password>" -AsPlainText -Force) `
-AppId "12345678-1234-1234-1234-123456789012" `
-Organization "yourdomain.onmicrosoft.com"
Setup, in order:
- Register an application in Microsoft Entra ID.
- Generate or obtain a certificate and upload the public key to the app registration.
- Install the private key on the machine that runs the script.
- Grant the app the Exchange.ManageAsApp API permission and consent to it.
- Assign the app an Exchange admin role in Entra ID.
Two failures map directly onto that list. A wrong -AppId, or the right one against the wrong tenant, returns AADSTS700016: Application not found in the directory — Entra is telling you the client ID does not exist in the tenant you named in -Organization. Skipping step 5 authenticates fine and then fails authorization with AADSTS50105: The signed in user is not assigned to a role, which is the same message users get when an app is set to require assignment.
On Azure VMs, Functions, and Automation accounts, a managed identity removes the certificate entirely:
# System-assigned
Connect-ExchangeOnline -ManagedIdentity -Organization "yourdomain.onmicrosoft.com"
# User-assigned
Connect-ExchangeOnline -ManagedIdentity -Organization "yourdomain.onmicrosoft.com" `
-ManagedIdentityAccountId <ManagedIdentityPrincipalIdGuid>
Connect-ExchangeOnline in government and sovereign clouds
Commercial Microsoft 365 and GCC both use the default environment, so no extra parameter is needed. Everything else requires -ExchangeEnvironmentName:
| Environment | -ExchangeEnvironmentName value |
|---|---|
| Microsoft 365 or GCC | not required (O365Default is the default) |
| Microsoft 365 GCC High | O365USGovGCCHigh |
| Microsoft 365 DoD | O365USGovDoD |
| Office 365 Germany | O365GermanyCloud |
| Office 365 operated by 21Vianet | O365China |
Connect-ExchangeOnline -UserPrincipalName admin@agency.gov -ExchangeEnvironmentName O365USGovGCCHigh
Certificate-based authentication works the same way in these clouds; add -ExchangeEnvironmentName to the app-only command. Register government app registrations in the Azure Government portal rather than the commercial one.
Disconnect-ExchangeOnline
Disconnect-ExchangeOnline -Confirm:$false
Always disconnect when a script or session finishes. Closing the PowerShell window without disconnecting leaves the session allocated until it expires, and concurrent sessions per user are limited — exhaust them and subsequent connections fail until they time out.
Common Connect-ExchangeOnline Errors
"The term 'Connect-ExchangeOnline' is not recognized"
The module is not visible to this PowerShell host. This is the PowerShell 5 versus PowerShell 7 problem in almost every case — work through the diagnosis commands above rather than blindly reinstalling.
Get-Module ExchangeOnlineManagement -ListAvailable
$env:PSModulePath -split ';'
"Files cannot be loaded because running scripts is disabled on this system"
The execution policy blocks the module. From an elevated session:
Set-ExecutionPolicy RemoteSigned
"The term 'Update-ModuleManifest' is not recognized"
PowerShellGet is missing or too old. REST-based connections depend on PowerShellGet and, by dependency, PackageManagement. Install or update both, then close and reopen the window before connecting.
Access denied, or the account cannot run Exchange cmdlets
Your account lacks Exchange administrative permissions. In the Microsoft 365 Admin Center, assign one of:
- Exchange Administrator (recommended for day-to-day work)
- Global Administrator
- Exchange Recipient Administrator (narrower scope)
Also confirm the account is enabled for PowerShell access at all — access can be disabled per user — and check whether a Conditional Access policy is blocking the sign-in based on location, device compliance, or client app.
The connection hangs, or no sign-in window ever appears
Work through these in order:
-
Special characters in the profile path. If the profile path of the signed-in Windows account contains a PowerShell special character such as
$, both connect and disconnect are likely to fail. There is no fix other than connecting from an account whose profile path is clean. -
Web Account Manager. From module 3.7.2 onward, WAM is used in the authentication flow and can fail on some builds. Retry with
-DisableWAM:Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com -DisableWAM -
Network path. Confirm outbound access to the Microsoft 365 endpoints. Note that TCP port 80 also needs to be open, not just 443 — restrictive egress policies that allow only 443 are a real cause of failures here:
Test-NetConnection outlook.office365.com -Port 443 Test-NetConnection login.microsoftonline.com -Port 443 -
No browser on the machine. Use device-code authentication from PowerShell 7 with
Connect-ExchangeOnline -Device. -
Federated identity provider not publicly reachable. A federated account cannot connect if your IdP or STS is not internet-facing. Use a cloud-only administrative account instead.
"Could not load file or assembly 'System.IdentityModel.Tokens.Jwt'"
Another module in the runspace conflicts with the Exchange module. Open a new PowerShell window and connect before importing anything else.
Guides that tell you to use -UseRPSSession, WinRM, or basic authentication
Ignore them. Remote PowerShell was retired in Exchange Online in October 2023, the module has used REST API connections since version 3.2.0, and the UseRpsSession parameter was deprecated in version 3.9.2. You do not need basic authentication enabled in WinRM, you do not need Enable-PSRemoting, and you should not modify TrustedHosts for this. Any guide still recommending those steps is describing a connection method that no longer exists.
Prerequisites Reference
| Requirement | Detail |
|---|---|
| Windows PowerShell 5.1 | Supported by every module version; requires .NET Framework 4.7.2 or later |
| PowerShell 7.6.0+ | Required for module 3.10.0 and later |
| PowerShell 7.4.0+ | Required for module 3.5.0 through 3.9.2 |
| Execution policy | RemoteSigned or looser |
| Supporting modules | Current PowerShellGet and PackageManagement (no preview builds) |
| Permissions | Exchange Administrator, Global Administrator, or Exchange Recipient Administrator |
| Network | Outbound HTTPS to Microsoft 365 endpoints, plus TCP port 80 |
PowerShell 7 is supported on Windows, macOS, and Linux. Check what you have:
$PSVersionTable.PSVersion
Useful Exchange Online PowerShell Commands
Once connected, these cover most day-to-day work.
Mailbox management
# List mailboxes
Get-Mailbox -ResultSize Unlimited
# Get one mailbox in full
Get-Mailbox -Identity user@domain.com | Format-List
# Create a shared mailbox
New-Mailbox -Shared -Name "Support Team" -DisplayName "Support Team" -Alias support
# Grant Full Access
Add-MailboxPermission -Identity sharedmailbox@domain.com -User admin@domain.com -AccessRights FullAccess
Mail flow rules
Get-TransportRule
New-TransportRule -Name "Email Disclaimer" `
-ApplyHtmlDisclaimerText "Confidential message" `
-ApplyHtmlDisclaimerLocation Append
Distribution groups
Get-DistributionGroup
New-DistributionGroup -Name "Marketing Team" -Members user1@domain.com,user2@domain.com
Microsoft 365 Admin PowerShell Toolkit
Production-ready scripts for Exchange Online, Teams, Intune, license reporting, Conditional Access export, and DNS verification.
M365 Admin Toolkit — 9 PowerShell scripts + quick reference guide
Best Practices
Security
- Use MFA-enabled admin accounts for interactive sessions.
- Use certificate or managed-identity authentication for automation — never a stored password.
- Disconnect sessions when finished.
- Review admin activity in the Entra ID audit logs.
Performance
- Prefer the
Get-EXO*cmdlets over their classic equivalents; they return categorised property sets instead of every property on the object. - Filter server-side with
-Filterrather than piping intoWhere-Object. - Use
-ResultSizedeliberately instead of pulling everything by reflex.
Maintenance
-
Keep the module current, matching the version to your PowerShell 7 release:
Update-Module -Name ExchangeOnlineManagement -
Update using the same scope you installed with, and reopen the window afterwards.
-
Test scripts against a lab tenant before production, and document certificate expiry dates for any automation.
Hybrid Exchange Scenarios
Organisations running on-premises Exchange alongside Exchange Online need both connections. The cloud connection is the module; the on-premises connection is still classic remote PowerShell against your own server, which is unaffected by the Exchange Online RPS retirement.
# Cloud
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
# On-premises, in the same window, with a prefix to avoid cmdlet collisions
$OnPremSession = New-PSSession -ConfigurationName Microsoft.Exchange `
-ConnectionUri http://exchange-server.contoso.local/PowerShell/ `
-Authentication Kerberos
Import-PSSession $OnPremSession -Prefix OnPrem -DisableNameChecking
With -Prefix OnPrem, Get-Mailbox targets Exchange Online and Get-OnPremMailbox targets your server.
Remote mailbox management
# Enable a remote mailbox for an existing on-premises user
Enable-OnPremRemoteMailbox -Identity "John.Doe" `
-RemoteRoutingAddress "john.doe@contoso.mail.onmicrosoft.com"
# Move a mailbox to the cloud
New-MoveRequest -Identity "user@contoso.com" -Remote `
-RemoteHostName "hybrid.contoso.com" `
-TargetDeliveryDomain "contoso.mail.onmicrosoft.com" `
-RemoteCredential $OnPremCred
Hybrid mail flow checks
Get-HybridConfiguration
Get-OrganizationRelationship | Format-List
Get-InboundConnector | Where-Object {$_.ConnectorType -eq "OnPremises"}
Get-OutboundConnector | Where-Object {$_.ConnectorType -eq "OnPremises"}
Bulk Operations and Throttling
Exchange Online throttles heavy automation. The exact limits are subject to change and are not fully published, so build scripts that degrade gracefully rather than scripts tuned to a specific number. Two limits are worth designing around: concurrent PowerShell connections per user are capped in the low single digits, and long-running sessions eventually expire and must be re-established.
Filter on the server
The single biggest performance win is not downloading data you are going to discard:
# Slow: retrieves every mailbox, then filters locally
Get-Mailbox -ResultSize Unlimited | Where-Object {$_.RecipientTypeDetails -eq "SharedMailbox"}
# Fast: the service does the filtering
Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails SharedMailbox
# Other server-side filters
Get-Mailbox -Filter "CustomAttribute1 -eq 'Sales'"
Get-Mailbox -Filter "WhenCreated -gt '2026-01-01'"
Retrieve only the properties you need
Get-EXOMailbox -ResultSize Unlimited -PropertySets Minimum
Get-EXOMailbox -ResultSize Unlimited -Properties DisplayName,PrimarySmtpAddress
Handle throttling instead of hoping to avoid it
$mailboxes = Get-EXOMailbox -ResultSize Unlimited -Properties PrimarySmtpAddress
$processed = 0
foreach ($mbx in $mailboxes) {
$processed++
Write-Progress -Activity "Processing mailboxes" `
-Status "$processed of $($mailboxes.Count)" `
-PercentComplete (($processed / $mailboxes.Count) * 100)
try {
Set-Mailbox -Identity $mbx.PrimarySmtpAddress -AuditEnabled $true -ErrorAction Stop
}
catch {
if ($_.Exception.Message -match 'throttl|exceeded') {
Write-Warning "Throttled — waiting 60 seconds before retrying."
Start-Sleep -Seconds 60
Set-Mailbox -Identity $mbx.PrimarySmtpAddress -AuditEnabled $true
}
else {
Write-Error "Failed for $($mbx.PrimarySmtpAddress): $_"
}
}
}
Keep long-running sessions alive
function Test-ExchangeConnection {
$connection = Get-ConnectionInformation -ErrorAction SilentlyContinue
return [bool]($connection | Where-Object { $_.State -eq 'Connected' })
}
$adminUPN = "admin@contoso.com"
foreach ($mbx in $mailboxes) {
if (-not (Test-ExchangeConnection)) {
Write-Warning "Connection lost — reconnecting."
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
Connect-ExchangeOnline -UserPrincipalName $adminUPN -ShowBanner:$false
}
# your operation here
}