Toolmaker Produkt-Dokumentation

Microsoft Exchange 365 - POP mit XOAUTH


Auf den Microsoft Exchange 365-Servern wird im Laufe des Januar 2023 die Basic Authentication für die Protokolle POP und IMAP abgeschaltet (siehe Microsoft Exchange 365 - Keine Basic Authentication mehr ab Oktober 2022).

Ab dann ist es erforderlich, dass Systeme, die E-Mails von diesen Server abrufen wollen, sich mit XOAUTH identifizieren.

Directmail und directspool unterstützen ab Version 5.60.xx XOAUTH.

Um diese Berechtigung auf der IBM i konfigurieren zu können, werden folgende Informationen benötigt:

  1. tenantID
  2. clientID
  3. clientsecret
  4. targetMailbox

Um die Nr. 1-3 zu ermitteln (bzw. zu konfigurieren), folgen Sie den unten beschriebenen Schritten. (Sie müssen in Microsoft Azure, dem Microsoft Active Directory und auf dem Microsoft Exchange 365 - Server konfiguriert werden.)

Nr. 4 ist der Name der Mailbox in Exchange 365 (z.B. "directmail@acmeco.onmicrosoft.com")

Inhaltsübersicht

.

Schritt 1: registrieren einer App in Azure

Die hierfür erforderlichen Schritte sind auf der Seite https://learn.microsoft.com/de-de/azure/active-directory/develop/quickstart-register-app beschrieben.

Um directmail/directspool für XOAUTH zu enablen müssen die folgenden Schritte ausgeführt werden, die dort beschrieben sind:


Registrieren der Anwendung  - Definition der clientID und Ermitteln der tenantID

Beschreibung: https://learn.microsoft.com/de-de/azure/active-directory/develop/quickstart-register-app#register-an-application

Ergänzende Hinweise zu dieser Seite:

  • in Schritt 6 wird "Nur Konten in diesem Organisationsverzeichnis" benötigt (im Screenshot heisst das: "Accounts in this organizational directory only .. - Singlet tenant")


  • Bei Redirect URI (optional) - nichts eingeben


  • Das Registrieren der App endet mit der Anzeige der clientID
    Hinweis: diese wird auch als "Application ID" bezeichnet

Notieren Sie die beiden Angaben:

1. Application (client) ID → clientID
2. Directory (tenant) ID → tenantID


Hinzufügen von Anmeldeinformation  - Definition der clientID und Ermitteln des clientsecret

Beschreibung: https://learn.microsoft.com/de-de/azure/active-directory/develop/quickstart-register-app#add-a-client-secret

Ergänzende Hinweise zu dieser Seite:

  • Wenn das clientsecret erstellt ist, wird es genau einmal angezeigt. Sofort notieren!clientsecret
  • Wir benötigten nicht:
    • Zertifikat
    • Verbundanmeldeschlüssel

Schritt 2: POP für "Clients" (also directmail/directspool) verfügbar machen

POP-Berechtigung hinzufügen

Beschreibung: https://learn.microsoft.com/de-de/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#add-the-pop-and-imap-permissions-to-your-aad-application

Ergänzende Hinweise zu dieser Seite:

  • für directmail/directspool muss "POP.AccessAsApp" aktiviert werden
  • IMAP ist nicht erforderlich

Zustimmung des Mandantenadministrators abrufen

Beschreibung: https://learn.microsoft.com/de-de/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#get-tenant-admin-consent

Ergänzende Hinweise zu dieser Seite:

  • Grant admin consent aktivieren


Schritt 3: in Exchange 365: Dienstprinzipale registrieren

Beschreibung: https://learn.microsoft.com/de-de/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#register-service-principals-in-exchange

Ergänzende Hinweise zu dieser Seite:

  • Diese Schritte erfolgen in PowerShell, mit einem eigens zu installierenden PowerShell-Applet namens "ExchangeOnlineManagement"
  • Hiermit wird das Recht vergeben, dass man mit dem clientsecret Zugriff auf die targetMailbox hat.

Testen mit Powershell

Die Angaben in spitzen Klammern (<...>) müssen durch die ermittelten/relevanten Werte ersetzt werden.

Applet Get-IMAPAccessToken.ps1


Get-IMAPAccessToken.ps1 -tenantID "<tenantID>" -clientId "<clientID>"   -clientsecret "<clientsecret>"  -targetMailbox "<targetmailbox>" -verbose


Testscript


<#
  .SYNOPSIS
  Allows IMAP OAuth testing with Office 365.
  Please install MSAL.PS Powershell module as prerequisite. 
  https://github.com/DanijelkMSFT/ThisandThat/blob/main/Get-IMAPAccessToken.ps1
  Refercing article with more insides 
  https://www.linkedin.com/pulse/start-using-oauth-office-365-popimap-authentication-danijel-klaric
  https://techcommunity.microsoft.com/t5/exchange-team-blog/announcing-oauth-2-0-client-credentials-flow-support-for-pop-and/ba-p/3562963
  https://techcommunity.microsoft.com/t5/exchange-team-blog/notes-from-the-field-using-oauth-for-activesync-and-pop-imap-in/ba-p/3606118
  .DESCRIPTION
  The function helps admins to test their IMAP OAuth Azure Application, 
  with Interactive user login und providing or the lately released client credential flow
  using the right formatting for the XOAuth2 login string.
  After successful logon, a simple IMAP folder listing is done, in addition it also allows to 
  test shared mailbox acccess for users if fullaccess has been provided. 
  
  Using Windows Powershell allows MSAL to cache the access+refresh token on disk for further executions for interactive login scenario.
  It´s a simple proof of concept with no further error managment.
  .PARAMETER tenantID
  Specifies the target tenant.
  .PARAMETER clientId
  Specifies the ClientID/ApplicationID of the registered Azure AD Application with needed IMAP Graph permissions
  .PARAMETER clientsecret
  Specifies the ClientSecret configured in the Azure AD Application for client credential flow
  .PARAMETER clientcertificate
  Specifies the ClientCertificate Thumbprint configured in the Azure AD Application for client credential flow
  .PARAMETER targeMailbox
  Specifies the primary emailaddress of the targetmailbox which should be accessed by service principal which has fullaccess to for client credential flow
  .PARAMETER redirectUri
  Specifies the redirectUri of the registered Azure AD Application for authorization code flow (interactive flow)
  .PARAMETER LoginHint
  Specifies the Userprincipalname of the logging in user for authorization code flow (interactive flow)
  .PARAMETER SharedMailbox (optinal)
  Specifies the primary emailaddress of the Sharedmailbox logged in user has fullaccess to for authorization code flow (interactive flow)
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -redirectUri "https://localhost" -LoginHint "user@contoso.com"
   PS C:\Kunden\toolmaker\DIRMAIL\OAUTH2>  .\Get-IMAPAccessToken.ps1 -tenantID "<tenantID>" -clientId "<clientID>" -redirectUri "https://login.microsoftonline.com/common/oauth2/nativeclient" -LoginHint "<targetMailbox>" 
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -redirectUri "https://localhost" -LoginHint "user@contoso.com" -SharedMailbox "sharedmailbox@contoso.com"
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -redirectUri "https://localhost" -LoginHint "user@contoso.com" -Verbose
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -clientsecret '' -targetMailbox "targetmailbox@contoso.com"
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -clientcertificate '' -targetMailbox "targetmailbox@contoso.com" 
  .EXAMPLE
  PS> .\Get-IMAPAccessToken.ps1 -tenantID "" -clientId "" -clientsecret '' -targetMailbox "targetmailbox@contoso.com" -Verbose
  ##PS  .\Get-IMAPAccessToken.ps1 -tenantID "<tenantID>" -clientId "<clientID>"   -clientsecret "<clientsecret>" -targetMailbox "<targetMailbox>" -Verbose
  .\Get-IMAPAccessToken.ps1 -tenantID "<tenantID>" -clientId "<clientID>"   -clientsecret "<clientsecret>" -targetMailbox "<targetMailbox>" -Verbose
  .\Get-IMAPAccessToken.ps1 -tenantID "<tenantID>" -clientId "<clientID>"   -clientsecret "<clientsecret>"  -targetMailbox "<targetMailbox>" -Verbose
#>

[CmdletBinding()]
param (
    [Parameter(Mandatory = $true)][string]$tenantID,
    [Parameter(Mandatory = $true)][String]$clientId,
    
    [Parameter(Mandatory = $true,ParameterSetName="authorizationcode")][String]$redirectUri,
    [Parameter(Mandatory = $true,ParameterSetName="authorizationcode")][String]$LoginHint,
    [Parameter(Mandatory = $false,ParameterSetName="authorizationcode")][String]$SharedMailbox,

    [Parameter(Mandatory = $true,ParameterSetName="clientcredentialsSecret")][String]$clientsecret,
    [Parameter(Mandatory = $true,ParameterSetName="clientcredentialsCertificate")][String]$clientcertificate,

    [Parameter(Mandatory = $true,ParameterSetName="clientcredentialsSecret")]
    [Parameter(Mandatory = $true,ParameterSetName="clientcredentialsCertificate")]
    [String]$targetMailbox
)

function Test-IMAPXOAuth2Connectivity {
# get Accesstoken via user authentication and store Access+Refreshtoken for next attempts
if ( $redirectUri ){
    $MsftPowerShellClient = New-MsalClientApplication -ClientId $clientID -TenantId $tenantID -RedirectUri $redirectURI  | Enable-MsalTokenCacheOnDisk -PassThru
    try {
        $authResult = $MsftPowerShellClient | Get-MsalToken -LoginHint $LoginHint -Scopes 'https://outlook.office365.com/.default'
    }
    catch  {
        Write-Host "Ran into an exception while getting accesstoken user grant flow" -ForegroundColor Red
        $_.Exception.Message
        $_.FullyQualifiedErrorId
        break
    }
}

if ( $clientsecret ){
    $SecuredclientSecret = ConvertTo-SecureString $clientsecret -AsPlainText -Force
    $MsftPowerShellClient = New-MsalClientApplication -ClientId $clientID -TenantId $tenantID -ClientSecret $SecuredclientSecret 
    try {
        $authResult = $MsftPowerShellClient | Get-MsalToken -Scopes 'https://outlook.office365.com/.default'
    }
    catch  {
        Write-Host "Ran into an exception while getting accesstoken using clientsecret" -ForegroundColor Red
        $_.Exception.Message
        $_.FullyQualifiedErrorId
        break
    }
}


if ( $clientcertificate ){
    $ClientCert = Get-ChildItem "cert:\currentuser\my\$clientcertificate"
    $MsftPowerShellClient = New-MsalClientApplication -ClientId $clientID -TenantId $tenantID -ClientCertificate $ClientCert
    try {
        $authResult = $MsftPowerShellClient | Get-MsalToken -Scopes 'https://outlook.office365.com/.default'
    }
    catch  {
        Write-Host "Ran into an exception while getting accesstoken using certificate" -ForegroundColor Red
        $_.Exception.Message
        $_.FullyQualifiedErrorId
        break
    }
}



$accessToken = $authResult.AccessToken
Write-Verbose "Access Token -- $accessToken"
$userName = $authResult.Account.Username

# build authentication string with accesstoken and username like documented here
# https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#authenticate-connection-requests

# in the case of client credential usage we need to add the target mailbox like shared mailbox access to the SASL string
if ( $targetMailbox) { $SharedMailbox = $targetMailbox }

if ( $SharedMailbox ) {
    $b="user=" + $SharedMailbox + "$([char]0x01)auth=Bearer " + $accessToken + "$([char]0x01)$([char]0x01)"
    Write-Host "Accessing Sharedmailbox - $SharedMailbox - with Accesstoken of User $userName." -ForegroundColor DarkGreen
} else {
        $b="user=" + $userName + "$([char]0x01)auth=Bearer " + $accessToken + "$([char]0x01)$([char]0x01)"
        }

$Bytes = [System.Text.Encoding]::ASCII.GetBytes($b)
$POPIMAPLogin =[Convert]::ToBase64String($Bytes)

Write-Verbose "SASL XOAUTH2 login string $POPIMAPLogin"

# connecting to Office 365 IMAP Service
Write-Host "Connect to Office 365 IMAP Service." -ForegroundColor DarkGreen
$ComputerName = 'Outlook.office365.com'
##$Port = '993'
$Port = '995'
    try {
        $TCPConnection = New-Object System.Net.Sockets.Tcpclient($($ComputerName), $Port)
        $TCPStream = $TCPConnection.GetStream()
        try {
            $SSLStream  = New-Object System.Net.Security.SslStream($TCPStream)
            $SSLStream.ReadTimeout = 5000
            $SSLStream.WriteTimeout = 5000     
            $CheckCertRevocationStatus = $true
            $SSLStream.AuthenticateAsClient($ComputerName,$null,[System.Security.Authentication.SslProtocols]::Tls12,$CheckCertRevocationStatus)
        }
        catch  {
            Write-Host "Ran into an exception while negotating SSL connection. Exiting." -ForegroundColor Red
            $_.Exception.Message
            break
        }
    }
    catch  {
    Write-Host "Ran into an exception while opening TCP connection. Exiting." -ForegroundColor Red
    $_.Exception.Message
    break
    }    

    # continue if connection was successfully established
    $SSLstreamReader = new-object System.IO.StreamReader($sslStream)
    $SSLstreamWriter = new-object System.IO.StreamWriter($sslStream)
    $SSLstreamWriter.AutoFlush = $true
    $SSLstreamReader.ReadLine()

    Write-Host "Authenticate using XOAuth2." -ForegroundColor DarkGreen
    # authenticate and check for results
 ##   $command = "A01 AUTHENTICATE XOAUTH2 {0}" -f $POPIMAPLogin
    $command = "AUTH = XOAUTH2"
    Write-Verbose "Executing command -- $command"
    $SSLstreamWriter.WriteLine($command)
   
    ## begin 
    $command = $POPIMAPLogin
    $SSLstreamWriter.WriteLine($command) 
    ## end
    #respose might take longer sometimes
    while (!$ResponseStr ) { 
        try { $ResponseStr = $SSLstreamReader.ReadLine() } catch { }
    }

    if ( $ResponseStr -like "*OK AUTHENTICATE completed.") 
    {
        $ResponseStr
        Write-Host "Getting mailbox folder list as authentication was successfull." -ForegroundColor DarkGreen
        $command = 'A01 LIST "" *'
        Write-Verbose "Executing command -- $command"
        $SSLstreamWriter.WriteLine($command) 

        $done = $false
        $str = $null
        while (!$done ) {
            $str = $SSLstreamReader.ReadLine()
            if ($str -like "* OK LIST completed.") { $str ; $done = $true } 
            elseif ($str -like "* BAD User is authenticated but not connected.") { $str; "Causing Error: IMAP protcol access to mailbox is disabled or permission not granted for client credential flow. Please enable IMAP protcol access or grant fullaccess to service principal."; $done = $true} 
            else { $str }
        }

        Write-Host "Logout and cleanup sessions." -ForegroundColor DarkGreen
        $command = 'A01 Logout'
        Write-Verbose "Executing command -- $command"
        $SSLstreamWriter.WriteLine($command) 
        $SSLstreamReader.ReadLine()

    } else {
        Write-host "ERROR during authentication $ResponseStr" -Foregroundcolor Red
    }

    # Session cleanup
    if ($SSLStream) {
        $SSLStream.Dispose()
    }
    if ($TCPStream) {
        $TCPStream.Dispose()
    }
    if ($TCPConnection) {
        $TCPConnection.Dispose()
    }
}

#check for needed msal.ps module
if ( !(Get-Module msal.ps -ListAvailable) ) { Write-Host "MSAL.PS module not installed, please check it out here https://www.powershellgallery.com/packages/MSAL.PS/" -ForegroundColor Red; break}

# execute function
Test-IMAPXOAuth2Connectivity