Configure OAuth2 Client Credentials Flow (SMTP.SendAsApp) for SMTP AUTH in Exchange Online Using PowerShell

With Basic Authentication deprecated and Exchange Online's legacy ROPC flows retiring, enterprise applications, scanners, monitoring alerts, and PowerShell automation scripts need a secure, modern way to send authenticated SMTP messages (smtp.office365.com:587) without interactive user sign-ins.

Microsoft provides two primary App-Only options for sending email in Microsoft 365:

  1. Microsoft Graph Mail.Send Application Permission: Allows the app to send mail via HTTPS REST—but by default, an app granted Mail.Send can send as any mailbox in the entire tenant unless restricted by an Application Access Policy (now being replaced by Role-Based Access Control for Applications).
  2. Exchange Online SMTP.SendAsApp (OAuth 2.0 Client Credentials Flow for SMTP AUTH): A least-privilege design! Even after you grant Admin Consent for SMTP.SendAsApp in Microsoft Entra ID, the application cannot send email as anyone until an Exchange Administrator explicitly registers the Service Principal in Exchange Online (New-ServicePrincipal) and grants it FullAccess permission on the specific sender mailbox (Add-MailboxPermission). Furthermore, messages sent via SMTP.SendAsApp are automatically saved in the sender mailbox's Sent Items folder.

SMTP.SendAsApp Architecture & Security Model

Exchange Online SMTP.SendAsApp (OAuth 2.0 Client Credentials) Architecture
Step 1: Microsoft Entra ID
App Registration + Scope
  • API: Office 365 Exchange Online
  • App Permission: SMTP.SendAsApp
  • Admin Consent granted
Step 2: Exchange Online RBAC
Service Principal & Mailbox ACL
  • New-ServicePrincipal registers AppId & ObjectId
  • Add-MailboxPermission -AccessRights FullAccess scopes exact sender mailbox
Step 3: SMTP XOAUTH2
smtp.office365.com:587
  • Acquires token for https://outlook.office365.com/.default
  • Authenticates via SASL XOAUTH2
  • Saves copy in Sent Items

Comparison: SMTP.SendAsApp vs. Graph Mail.Send

FeatureExchange Online SMTP.SendAsAppMicrosoft Graph Mail.Send (App-Only)
ProtocolStandard SMTP TLS (smtp.office365.com:587 with XOAUTH2)HTTPS REST (POST /v1.0/users/{id}/sendMail)
Default Blast RadiusZero — Cannot send as any mailbox until granted FullAccess per mailboxTenant-Wide — Can send as any user unless scoped via EXO Application RBAC
Saved in Sent Items?Yes (Automatically saved by Exchange SMTP submission engine)Configurable (saveToSentItems: true)
Legacy App CompatibilityIdeal for SMTP-based apps, Ticketing systems, & PowerShellIdeal for cloud-native REST integrations

Step 1: Register the Entra App & Grant SMTP.SendAsApp

  1. Sign in to the Microsoft Entra admin center (https://entra.microsoft.com) and go to Identity → Applications → App registrations → + New registration.
  2. Name the application (for example, LAB365-SMTP-Relay) and click Register.
  3. Go to API permissions → + Add a permission → APIs my organization uses.
  4. Search for Office 365 Exchange Online (AppId 00000002-0000-0ff1-ce00-000000000000) and select Application permissions.
  5. Expand SMTP, check SMTP.SendAsApp, and click Add permissions.
  6. Click Grant admin consent for [Your Tenant].
  7. Under Certificates & secrets, upload a certificate (recommended for production) or create a Client Secret.

Step 2: Register the Service Principal in Exchange Online & Grant Mailbox Permission

Important 2026 Update: The legacy Get-AzureADServicePrincipal cmdlet from the AzureAD module is retired. Always retrieve your Enterprise Application's AppId and Id (ObjectId) using the Microsoft Graph PowerShell SDK (Get-MgServicePrincipal)!

⚠️ Common Pitfall: In Microsoft Entra ID, the App Registration blade and the Enterprise Applications (Service Principal) blade have different Object IDs! When running New-ServicePrincipal in Exchange Online, you must pass the Id (ObjectId) from Enterprise Applications (Get-MgServicePrincipal), never the Object ID from the App Registration blade.

POWERSHELL
# 1. Connect to Microsoft Graph & Exchange Online
Connect-MgGraph -Scopes "Application.Read.All" -NoWelcome
Connect-ExchangeOnline -ShowBanner:$false

# 2. Fetch the Enterprise Application (Service Principal) details via Microsoft Graph
$AppDisplayName = "LAB365-SMTP-Relay"
$EntraSp = Get-MgServicePrincipal -Filter "displayName eq '$AppDisplayName'"

if (-not $EntraSp) {
    throw "Service Principal '$AppDisplayName' not found in Entra ID!"
}

Write-Host "Found Service Principal -> AppId: $($EntraSp.AppId) | Enterprise App ObjectId: $($EntraSp.Id)" -ForegroundColor Cyan

# 3. Register the Service Principal inside Exchange Online
New-ServicePrincipal -AppId $EntraSp.AppId `
    -ObjectId $EntraSp.Id `
    -DisplayName $EntraSp.DisplayName

# 4. Grant FullAccess permission on the sender mailbox to the Service Principal
$SenderMailbox = "[email protected]"
Add-MailboxPermission -Identity $SenderMailbox `
    -User $EntraSp.Id `
    -AccessRights FullAccess

# 5. Ensure Authenticated SMTP (SmtpClientAuthenticationDisabled) is enabled on the sender mailbox
Set-CASMailbox -Identity $SenderMailbox -SmtpClientAuthenticationDisabled:$false

Step 3: Send Email via OAuth 2.0 XOAUTH2 in PowerShell (Zero External Module Dependencies)

Once permissions propagate (typically 15–30 minutes in Exchange Online), you can acquire an OAuth 2.0 access token for the scope https://outlook.office365.com/.default and authenticate against smtp.office365.com:587 using the SASL XOAUTH2 mechanism.

Here is a complete, self-contained PowerShell function that acquires the OAuth 2.0 token (supporting either Certificate Auth or Client Secret), verifies that the token contains the SMTP.SendAsApp role claim, constructs the XOAUTH2 base64 payload, and sends the email over TLS 1.2/1.3:

POWERSHELL
function Send-ExoMailMessageOAuth2 {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$TenantId,

        [Parameter(Mandatory = $true)]
        [string]$ClientId,

        [Parameter(Mandatory = $true)]
        [string]$ClientSecret,

        [Parameter(Mandatory = $true)]
        [string]$From,

        [Parameter(Mandatory = $true)]
        [string]$To,

        [Parameter(Mandatory = $true)]
        [string]$Subject,

        [Parameter(Mandatory = $true)]
        [string]$Body
    )

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # 1. Request OAuth 2.0 Access Token for Exchange Online
    $TokenUri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    $TokenBody = @{
        client_id     = $ClientId
        client_secret = $ClientSecret
        scope         = "https://outlook.office365.com/.default"
        grant_type    = "client_credentials"
    }

    $TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenUri -Body $TokenBody -ContentType "application/x-www-form-urlencoded"
    $AccessToken   = $TokenResponse.access_token

    # 2. Verify JWT Token Contains 'SMTP.SendAsApp' Role Claim
    $PayloadPart = $AccessToken.Split('.')[1].Replace('', '+').Replace('_', '/')
    while ($PayloadPart.Length % 4 -ne 0) { $PayloadPart += "=" }
    $JwtClaims = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($PayloadPart)) | ConvertFrom-Json

    if ($JwtClaims.roles -notcontains "SMTP.SendAsApp") {
        throw "Access token does NOT contain 'SMTP.SendAsApp' in roles claim! Current roles: $($JwtClaims.roles -join ', ')"
    }
    Write-Host "[+] Verified JWT Role Claim: SMTP.SendAsApp (Token Audience: $($JwtClaims.aud))" -ForegroundColor Green

    # 3. Build SASL XOAUTH2 String: "user=" + User + "^Aauth=Bearer " + Token + "^A^A"
    $CtrlA = [char]1
    $XOAuth2Raw = "user=$From${CtrlA}auth=Bearer $AccessToken${CtrlA}${CtrlA}"
    $XOAuth2B64 = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($XOAuth2Raw))

    # 4. Connect to smtp.office365.com:587, Upgrade to STARTTLS, & Authenticate via XOAUTH2
    $TcpClient = New-Object System.Net.Sockets.TcpClient("smtp.office365.com", 587)
    $NetStream = $TcpClient.GetStream()
    $Reader    = New-Object System.IO.StreamReader($NetStream)
    $Writer    = New-Object System.IO.StreamWriter($NetStream)
    $Writer.AutoFlush = $true

    Write-Host "[*] Server Banner: $($Reader.ReadLine())" -ForegroundColor DarkGray
    $Writer.WriteLine("EHLO lab365.in")
    while (($Line = $Reader.ReadLine()) -and $Line.Substring(3, 1) -eq "-") {}

    $Writer.WriteLine("STARTTLS")
    $StartTlsResp = $Reader.ReadLine()
    if (-not $StartTlsResp.StartsWith("220")) {
        throw "Failed to initiate STARTTLS: $StartTlsResp"
    }

    $SslStream = New-Object System.Net.Security.SslStream($NetStream, $false)
    $SslStream.AuthenticateAsClient("smtp.office365.com")
    $SslReader = New-Object System.IO.StreamReader($SslStream)
    $SslWriter = New-Object System.IO.StreamWriter($SslStream)
    $SslWriter.AutoFlush = $true

    $SslWriter.WriteLine("EHLO lab365.in")
    while (($Line = $SslReader.ReadLine()) -and $Line.Substring(3, 1) -eq "-") {}

    $SslWriter.WriteLine("AUTH XOAUTH2 $XOAuth2B64")
    $AuthResp = $SslReader.ReadLine()
    if (-not $AuthResp.StartsWith("235")) {
        throw "SMTP XOAUTH2 Authentication Failed: $AuthResp"
    }
    Write-Host "[+] SMTP Authentication Successful: $AuthResp" -ForegroundColor Green

    # 5. Transmit Message
    $SslWriter.WriteLine("MAIL FROM:<$From>")
    [void]$SslReader.ReadLine()
    $SslWriter.WriteLine("RCPT TO:<$To>")
    [void]$SslReader.ReadLine()
    $SslWriter.WriteLine("DATA")
    [void]$SslReader.ReadLine()

    $SslWriter.WriteLine("From: $From")
    $SslWriter.WriteLine("To: $To")
    $SslWriter.WriteLine("Subject: $Subject")
    $SslWriter.WriteLine("Content-Type: text/html; charset=utf-8")
    $SslWriter.WriteLine("")
    $SslWriter.WriteLine($Body)
    $SslWriter.WriteLine(".")

    $DataResp = $SslReader.ReadLine()
    Write-Host "[+] Message Sent: $DataResp" -ForegroundColor Green

    $SslWriter.WriteLine("QUIT")
    $TcpClient.Close()
}

# Example Usage:
# Send-ExoMailMessageOAuth2 `
#     -TenantId "ab85c7a1-366d-44ba-acc6-924fa0950045" `
#     -ClientId "c841ae33-4093-4385-8a90-6a530761f9b9" `
#     -ClientSecret "YOUR_CLIENT_SECRET" `
#     -From "[email protected]" `
#     -To "[email protected]" `
#     -Subject "Test OAuth2 SMTP.SendAsApp Relay" `
#     -Body "<h3>Sent via Exchange Online SMTP OAuth 2.0 Client Credentials Flow</h3>"

Troubleshooting Common 535 5.7.3 Authentication unsuccessful Errors

If AUTH XOAUTH2 returns 535 5.7.3 Authentication unsuccessful:

  1. Wrong ObjectId passed to New-ServicePrincipal: Verify that (Get-ServicePrincipal -Identity $AppDisplayName).ObjectId matches (Get-MgServicePrincipal -Filter "displayName eq '$AppDisplayName'").Id (the Enterprise Application Object ID, not the App Registration Object ID).
  2. SMTP AUTH Disabled on the Mailbox or Tenant: Run Get-CASMailbox -Identity $SenderMailbox | Select SmtpClientAuthenticationDisabled. Remember that $null inherits the tenant-level Get-TransportConfig setting, so explicitly set -SmtpClientAuthenticationDisabled:$false on the sender mailbox.
  3. Wrong OAuth Scope: Ensure your token request specifies https://outlook.office365.com/.default (not https://graph.microsoft.com/.default).