2025, Oct 20 12:00

exchangelib stopped working after Outlook on Office 365 migration? Switch from Basic auth to OAuth for shared mailbox access

Python exchangelib fails with 'wrong credentials' after Office 365 migration. Basic auth is deprecated; see how to fix it by switching to OAuth today.

When a mailbox integration works flawlessly for years and then suddenly starts returning “wrong credentials,” it’s rarely about typos. A recent move from an on-premises style URL to Outlook on Office 365 is exactly that kind of change: the authentication model under your feet has shifted.

Context: what changed and why the code broke

Access used to go through a URL like https://webmail.domain.it/owa/shared_mailbox@domain.it. After a migration to the cloud, the entry point moved to https://outlook.office365.com/mail. The Python integration relied on exchangelib with a username/password pair via Credentials. It used a custom HTTP adapter to force a proxy and enabled autodiscover and delegate access to a shared mailbox. Before the migration, the same code returned a valid Account object. After the migration, the same credentials now yield a “wrong credentials” error.

Disabling autodiscover avoided the immediate credential failure but produced a server back-off message instead.

WARNING:exchangelib.util:Server requested back off until 2025-07-31 08:42:34.617995. Sleeping 9.998383 seconds

Attempts to switch the username format and to force the proxy through environment variables or a different adapter did not change the outcome.

Original code that started failing after migration

class RelayHTTPAdapter(requests.adapters.HTTPAdapter):
    def send(self, req, stream=False, timeout=None, verify=True, cert=None, proxies=None):
        proxies = {
            'http': 'proxy',
            'https': 'proxy',
        }
        return super().send(req, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
def acquire_mailbox():
    try:
        usr = "UT-*****-DEV"
        pwd = "Password"
        BaseProtocol.HTTP_ADAPTER_CLS = RelayHTTPAdapter
        cfg = Configuration(
            server='outlook.office365.com',
            retry_policy=FaultTolerance(max_wait=900),
            credentials=Credentials(usr, pwd)
        )
        return Account(
            'shared_mailbox@domain.it',
            config=cfg,
            autodiscover=True,
            access_type=DELEGATE
        )
    except Exception as exc:
        print(f"Account fetch failed: {exc}...")
        return None

The root cause

Basic (username/password) authentication is no longer supported on office365.com. That’s why a Credentials(user, password) flow that used to pass is now rejected, even if the values are correct. The back-off warning seen when toggling autodiscover is a side effect, not the fix.

The fix

Switch the integration to OAuth. The required setup and code changes are documented here: https://ecederstrand.github.io/exchangelib/#impersonation-oauth-on-office-365

At a high level, the change in the Python side is to stop constructing Configuration with Credentials(user, password) and instead supply an OAuth-based credentials object as described in the official guide. The rest of the flow—proxy adapter, server, retry policy, autodiscover, and delegate access—can remain structurally the same.

Adjusted example with OAuth-based credentials injected

class RelayHTTPAdapter(requests.adapters.HTTPAdapter):
    def send(self, req, stream=False, timeout=None, verify=True, cert=None, proxies=None):
        proxies = {
            'http': 'proxy',
            'https': 'proxy',
        }
        return super().send(req, stream=stream, timeout=timeout, verify=verify, cert=cert, proxies=proxies)
def acquire_mailbox_oauth(oauth_creds):
    try:
        BaseProtocol.HTTP_ADAPTER_CLS = RelayHTTPAdapter
        cfg = Configuration(
            server='outlook.office365.com',
            retry_policy=FaultTolerance(max_wait=900),
            credentials=oauth_creds
        )
        return Account(
            'shared_mailbox@domain.it',
            config=cfg,
            autodiscover=True,
            access_type=DELEGATE
        )
    except Exception as exc:
        print(f"Account fetch failed: {exc}...")
        return None

The oauth_creds object must be created according to the exchangelib documentation linked above.

Why this matters

Authentication deprecations break integrations in subtle ways. A service can look identical—same hostname, same mailbox—but a shift in the auth pipeline renders long-standing scripts unusable. Recognizing that “wrong credentials” post-migration can be a policy change rather than a typing error saves hours of troubleshooting proxies, autodiscover, and endpoint URLs.

Takeaways

If your exchangelib integration stopped working after moving to Outlook on Office 365, and you are still passing a username and password, that is the problem. Replace Basic auth with OAuth following the official exchangelib instructions. Keep your proxy handling and configuration intact; change only the credentials mechanism. This aligns the integration with the current authentication model and restores access to the shared mailbox without relying on deprecated flows.

The article is based on a question from StackOverflow by zacthebigkub and an answer by Erik Cederstrand.