Search code examples
powershellemailoauth-2.0azure-active-directoryexchange-online

OAuth2 authenticated e-mails from PowerShell to Exchange Online


OAuth2 for the 100th time, sorry, but I'm on the verge of despair here. I need to send authenticated e-mails from a PowerShell script to my own Exchange Online Mailbox (I'm also the tenant admin, if this matters). So I registered an App in AzureAD.

Display name: MyApp
Supported account types: My organization only
App permissions:
Microsoft Graph\User.Read (Delegated, Granted for MyDomain)
Office 365 Exchange Online\IMAP.AccessAsApp (Application, Granted for MyDomain)

Probably the Microsoft Graph API isn't needed here. The app has a secret with an expiration period of 730 days.

I created a new Exchange Service Principal and granted FullAccess to my mailbox:

$app = Get-AzureADServicePrincipal -SearchString MyApp
New-ServicePrincipal -AppId $app.AppId -ObjectId $app.ObjectId -DisplayName "MyServicePrincipalName"
Add-MailboxPermission -Identity $azureUserName -User $app.ObjectId -AccessRights FullAccess

Here's the result of Get-MailboxPermission -Identity $azureUserName | fl:

IsOwner         : False
AccessRights    : {FullAccess, ReadPermission}
Deny            : False
InheritanceType : All
User            : NT AUTHORITY\SELF
UserSid         : S-X-Y-Z
Identity        : admin
IsInherited     : False
IsValid         : True
ObjectState     : Unchanged

IsOwner         : False
AccessRights    : {FullAccess}
Deny            : False
InheritanceType : All
User            : MyServicePrincipalName
UserSid         : S-X-Y-Z-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxxxx-xxxxxxxx
Identity        : admin
IsInherited     : False
IsValid         : True
ObjectState     : Unchanged

With this configured, I tried to add an e-mail to my inbox. Here's the script I used:

Import-Module AzureAD
Import-Module ExchangeOnlineManagement

$tenantID = "asdf1234"
$appID = "asdf1234"
$secretValue = "asdf1234"
$userName = "myazureuser@mydomain.onmicrosoft.com"
$senderAddress = $userName
$recipientAddress = $userName

$scope = "https://outlook.office365.com/.default"
$tokenURL = "https://login.microsoftonline.com/$tenantID/oauth2/v2.0/token"

# Define the OAuth 2.0 token request body
$tokenRequestBody = @{
    client_id     = $appID
    scope         = $scope
    client_secret = $secretValue
    grant_type    = "client_credentials"
} 

# Request an access token
$response = Invoke-RestMethod -Uri $tokenURL -Method POST -ContentType "application/x-www-form-urlencoded" -Body $tokenRequestBody

# Access the access token
$accessToken = $response.access_token

# Connect to Office 365 IMAP service
$server = "outlook.office365.com"
$port = 993

$tcpClient = [System.Net.Sockets.TcpClient]::new($server, $port)
$sslStream = [System.Net.Security.SslStream]::new($tcpClient.GetStream())
$sslStream.AuthenticateAsClient($server, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false)
$reader = [System.IO.StreamReader]::new($sslStream)
$writer = [System.IO.StreamWriter]::new($sslStream)

# Read server response
$reader.ReadLine()

# Compose the email
$emailSubject = "Test e-mail"
$emailBody = "This is a test e-mail"
$email = "To: $recipientAddress`nFrom: $senderAddress`nSubject: $emailSubject`n`n$emailBody"

# build XOAUTH2 login string with accesstoken and username
$accessString ="user=" + $userName + "$([char]0x01)auth=Bearer " + $accessToken + "$([char]0x01)$([char]0x01)"
$Bytes = [System.Text.Encoding]::ASCII.GetBytes($accessString)
$loginString = [Convert]::ToBase64String($Bytes)

# Authenticate using XOAUTH2
$command = "A01 AUTHENTICATE XOAUTH2 $loginString"
$writer.WriteLine($command)

# Capture the response
$ResponseStr = $reader.ReadLine()

# Check if the response indicates success
if ($ResponseStr -like "*OK AUTHENTICATE completed.") {
    Write-Host "Authentication successful."
} else {
    Write-Host "Authentication failed: $ResponseStr"
}

# Send an email
$command = "A01 APPEND INBOX (\Seen) $(" + $email.Length + ")"
$writer.WriteLine($command)
$writer.WriteLine($email)
$writer.WriteLine()

# Logout and clean up sessions
$writer.WriteLine("A01 Logout")
$writer.Close()
$reader.Close()
$sslStream.Close()
$tcpClient.Close()

I get an access token, the $response looks like this:

token_type expires_in ext_expires_in access_token
Bearer 3599 3599 myaccesstokenstring

But after I execute $writer.WriteLine($command), it hangs and after a few seconds I get the error:

Authentication failed: * BYE Connection is closed. 13

I don't really understand, what I did wrong. Maybe it's completely stupid for those who know what they are doing. I'm just trying to send e-mail notifications to myself and my understanding of the whole concept here is very basic.

Any help is greatly appreciated!


Solution

  • Alternatively, you can make use of Microsoft Graph API to send an authenticated email from PowerShell.

    I registered one Azure AD application and granted Mail.Send Application permission as below:

    enter image description here

    Now, I ran below modified script to send mail by installing Microsoft.Graph module like this:

    #Install-Module -Name Microsoft.Graph -Scope CurrentUser
    #Import-Module Microsoft.Graph.Users.Actions
    
    $tenantID = "tenantID"
    $appID = "appID"
    $secretValue = "secret"
    $ClientSecretPass = ConvertTo-SecureString -String $secretValue -AsPlainText -Force
    $ClientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appID, $ClientSecretPass
    
    # Connect to Microsoft Graph with Client Secret
    Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $ClientSecretCredential
    
    $userName = "sri@xxxxxxxxx.onmicrosoft.com"
    $senderAddress = $userName
    $recipientAddress = $userName
    
    $emailSubject = "Test e-mail"
    $emailBody = "This is a test e-mail"
    $type = "Text" 
    
    $params = @{
        Message         = @{
            Subject       = $emailSubject
            Body          = @{
                ContentType = $type
                Content     = $emailBody
            }
            ToRecipients  = @(
                @{
                    EmailAddress = @{
                        Address = $recipientAddress
                    }
                }
            )
        }
    }
    
    Send-MgUserMail -UserId $senderAddress -BodyParameter $params
    

    Response:

    enter image description here

    When I checked the same in Inbox folder of user, mail added successfully as below:

    enter image description here

    Note that, The Graph API is not replacing the Exchange Online API. But you can access multiple Microsoft 365 services including Exchange Online, SharePoint, OneDrive, Teams, etc. via Graph API.

    As Microsoft is moving towards consolidating its APIs under the Microsoft Graph API, it's possible that the Exchange Online API may eventually be deprecated in favor of the Graph API. So, it is recommended to use the Graph API for new development that simplifies the complexity.