ArtikelRahmen V5 MS365

You know the game: The deactivation of Basic Authentication forces us all to act. Your DMS, ticket system or archive should finally access Exchange Online via OAuth2.

In Azure, everything looks green, the token comes, but access pops. Why this happens and how to fix the “LogonDenied” error.

grafik 239

The switch to the so-called “Client Credentials Flow” is actually standard. Register the app, give rights, done. But especially with legacy protocols like IMAP, there is an error devil lurking that can cost you hours.

Scenario: Everything green, but still “access denied”

You’ve done your homework: Your app is neatly registered in Azure AD (Entra ID). For your background service – be it a DMS importer, a ticket system scanner or an archiving solution – you use established libraries such as MailBee, Aspose or Chilkat.

The code is in place, the OAuth token is successfully retrieved from the Azure endpoint. So the client secret is correct. But at the moment of truth, when the application wants to log in to the mailbox via IMAP, there is a bang.

In the log file, an error message stares at you, which lures you onto the wrong track: BadCredentials.

Exception: MailBeeImapLoginBadCredentialsException
Response: MBN00000002 NO AUTHENTICATE failed.
Error="AuthFailed:LogonDenied-None-OAuthLog:AuthExc_Auth Failed. [...] User:XXXXXXXX@..."

Don’t let “bad credentials” fool you. Your password (secret) is correct. The crucial clue is hidden in the details: LogonDenied.

Cause: Misunderstanding between Azure and Exchange Online

Why does this happen? You set the API permissions in Azure, didn’t you? The problem lies in the strict separation of responsibilities within Microsoft 365. You’ve fallen victim to the difference between authentication and authorization .

Think of it as in front of an exclusive club:

  1. Azure AD (Entra ID) is the Gatekeeper (Authentication): It checks your app’s ID (Client ID & Secret). Is the ID genuine? Yes. Result: The bouncer puts a stamp on your app’s hand (the access token) and lets it into the building. Azure has done its job.
  2. Exchange Online is the operator of the VIP area (Authorization): Your app is now in the hallway with the stamp and wants to go to the VIP area (the specific mailbox). The operator looks at the stamp, but shrugs his shoulders. He knows who the app is, but it’s not on his guest list for this particular VIP table. Result: LogonDenied.

The technical background: In Client Credentials Flow (App-Only Access), there is no user context. By default, Exchange Online simply does not know that this abstract “Service Principal” from Azure should be authorized to read the emails from docscan@... .

Unlike the modern Graph API, where rights often take a more global effect, the old IMAP interface in Exchange is conservative. It requires an explicit link: “This service principal is allowed to look into this mailbox.”

And it is precisely this link that is missing – even if you have checked “FullAccess” in Azure. Azure cannot “push” this right all the way to the Exchange Store. This link must be set manually in the backend of Exchange. And this can only be done via PowerShell.

Here are the three stumbling blocks you need to check now:

1: The Graph Trap (or: Why Modern Doesn’t Always Work)

Microsoft’s direction of travel is clear: Everything should run via the Microsoft Graph API . So when you register a new app, you automatically end up there. The problem: Your legacy application speaks IMAP (RFC 3501), not REST/JSON.

If you grant permissions such as Mail.Read in the Graph API, you’ll get a valid token – but that token is meant for the Graph interface. The IMAP endpoint (outlook.office365.com) often rejects this token or accepts it only in the delegated context (user login), but not for background services (app-only).

The Solution: The Hidden Legacy API You absolutely need the API “Office 365 Exchange Online”. However, Microsoft has hidden these from the default shortlist to push developers to Graph. If you simply search for the name, you often won’t find it at all.

Here’s how to get it anyway (the “pro trick”):

  1. Go to App Registrations -> Your App -> API Permissions.
  2. Click on “Add permission”.
  3. Go to the “APIs used by my organization” tab at the top.
  4. Ignore the name search! Instead, type in the internal GUID of Exchange Online directly: 00000002-0000-0ff1-ce00-000000000000
  5. Click on the hit Office 365 Exchange Online.
  6. Select Application permissions – not “Delegated permissions”!
  7. IMAP Search and check the boxIMAP.AccessAsApp.
  8. Vital: Then click on the “Grant administrator consent” button at the top. Without this click, the tick is worthless.


2: The wrong object ID

This is the absolute classic where almost all manual PowerShell attempts fail. If we use the script to marry Exchange and Azure, the cmdlet asks for the ServiceId (object ID).

The mean thing: Azure shows you an “object ID” for your app in two different places – and it’s two different numbers!

  1. The App Registration ID: This is the “blueprint” of your app. You can find this ID under “App Registrations”.
    • For Exchange, this ID is useless!!
  2. The Enterprise Application ID: This is the so-called “Service Principal” – the specific instance of the app that runs in your tenant.
    • This is exactly the ID Exchange needs!!

The error: If you take the ID from the app registry, PowerShell throws the error: AADServicePrincipalNotFound. Exchange is basically saying, “I don’t know this guy.”

grafik 237

Here’s how to do it right:

  • Exit the App Registrations section.
  • Search for “Enterprise Applications” at the top of the Azure portal.
  • Search for your app (by client ID or name).
  • Click on it and copy the object ID from the overview. (Pro tip: This ID is different from the one you’ve been using!)


3: The Time Factor & the Cache Problem (The Patience Trap)

This is the moment when admins doubt their sanity. You fixed the API, found the correct ID, and ran the PowerShell script. Everything reports “Success”.

You start the retrieval and … LogonDenied. Still.

Before you tear everything down again: That’s normal. Two mechanisms play against your impatience here:

  1. Exchange replication: When you Add-MailboxPermission run, this info must first be replicated by the Microsoft cloud. This officially takes up to 60 minutes. In practice, usually 5 to 15 minutes.
  2. The Access Token Caching (The Final Boss): That is the more important point. Your application gets an OAuth token from Azure. This token is valid for 1 hour. If your app got a token before your fix, this token does not yet say that it can be accessed by the mailbox. So the app stubbornly tries the old, invalid “ticket” for the next 50 minutes.

The solution: Restart the Windows service of your application (service) once. This forces the app to throw away the old token and request a fresh one from Azure – this time with the correct rights. After: Get a coffee and give Exchange 10 minutes.

The solution: The PowerShell fix

Enough of the theory. In order for Exchange to allow access, you need to make the Service Principal created in Azure known in Exchange and give them full access to the target mailbox.

Grab your Exchange Online PowerShell (as an admin) and the object ID from the enterprise applications (see #2) you wrote down:

Connect-ExchangeOnline

# --- KONFIGURATION ---
# Die Application (Client) ID (bleibt gleich)
$AppId = "DEINE-APP-CLIENT-ID" 

# ACHTUNG: Hier die Objekt-ID aus den UNTERNEHMENSANWENDUNGEN rein!
$ServiceId = "DEINE-ENTERPRISE-APP-OBJECT-ID" 

$Mailbox = "ziel-email@deine-domain.de"
$DisplayName = "Name deiner App"
# ---------------------

# 1. Service Principal in Exchange registrieren
# Falls er schon existiert, kommt ein Fehler - den kannst du ignorieren.
try {
    New-ServicePrincipal -AppId $AppId -ServiceId $ServiceId -DisplayName $DisplayName -ErrorAction Stop
    Write-Host "Service Principal registriert." -ForegroundColor Green
}
catch {
    Write-Host "Info: $($_.Exception.Message)" -ForegroundColor Yellow
}

# 2. Zugriff auf das Postfach gewähren
# Das ist der entscheidende Schritt gegen "LogonDenied"
Add-MailboxPermission -Identity $Mailbox -User $ServiceId -AccessRights FullAccess

Write-Host "Fertig! Jetzt heißt es warten..."

Checklist: If it still doesn’t work

You ran the script without errors, start your application and still AUTHENTICATE failedsee ? Don’t panic, check these three points:

  1. Patience is mandatory: Exchange works asynchronously. Changes via Add-MailboxPermission officially need up to 60 minutes of replication time until they are active on all servers worldwide. In practice, it usually works after 15-20 minutes. Grab a coffee.
  2. Restart service (token cache)! This is the most common reason for unnecessary headaches. Your application probably got an access token 30 minutes ago. This token is valid for 1 hour and the new rights are still missing. As long as the app uses the old token, you’ll stay out. Restart your app’s Windows service once to force a fresh token.
  3. Is the user activated? Even if it’s a shared mailbox, OAuth often requires the user object to be enabled in Azure (AccountEnabled = $true), even if no human logs in interactively. Check if “Block Login” is active on the user and take it out as a test.

Good luck with the changeover!

OAuth and legacy protocols are a tricky mix, but with the right PowerShell grip, you can get that to work stably too. If you still have questions or stumbled upon another exotic error code, just write it in the comments – we’re all still learning.

This post is also available in: Deutsch English

Be the first to comment

Leave a Reply

Your email address will not be published.


*