pythonpython-3.xazureoauthimaplib

Authenticate with Exchange to access email inbox with imaplib


I have a python script that used to login to an outlook inbox:

from imaplib import IMAP4_SSL

imap = IMAP4_SSL("outlook.office365.com")
imap.login("user", "password")

It now fails with an error:

Traceback (most recent call last):
  File "imap.py", line 4, in <module>
    imap.login("user", "password")
  File "/usr/lib/python3.8/imaplib.py", line 603, in login
    raise self.error(dat[-1])
imaplib.error: b'LOGIN failed.'

Microsoft has disabled basic authentication for Exchange Online. How should I authenticate now that basic auth has been deprecated?


Solution

  • In order to authenticate with OAUTH2 you need an existing app registration in Azure. I am assuming you already have this set up.

    Add Exchange API permissions to the application

    1. From within your app registration page, click API permissions along the left menu.
    2. Check if the Configured permissions section has the Office 365 Exchange Online API. If it does, you can skip to the next section:
      Configured permissions
    3. Click the Add a permission button above the table. Select the APIs my organization uses tab and search for Office 365 Exchange Online:
      Office 365 Exchange Online
    4. Select Application permissions for the type, enable the IMAP.AccessAsApp permission. Then click the Add permissions button.
      Permissions Name

    NOTE: An admin may need to approve the permission. They also may need to set up the Service Principal (there is one unclear step to be aware of).


    Request access token

    1. From within your app registration page, click Certificates & secrets along the left menu.
    2. Select the Client Secrets tab and click the New client secret button. Fill out the form and click Add.
    3. Copy the Value for the secret. Click Overview in the left menu and copy the Application (client) ID and Directory (tenant) ID. Use these variables in your script to make a POST request to the OAUTH2 endpoint:
    from urllib.parse import urlencode
    import requests
    
    tenant = "Directory (tenant) ID"
    url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
    payload = urlencode({
        "client_id": "Application (client) ID",
        "client_secret": "Secret Value",
        "scope": "https://outlook.com/.default",
        "grant_type": "client_credentials"
    })
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    
    response = requests.request("POST", url, headers=headers, data=payload)
    access_token = response.json()["access_token"]
    

    NOTE: Scope is important! I had to use https://outlook.com/.default. I've seen some documentation use https://graph.microsoft.com/.default or https://ps.outlook.com/.default, neither of which have access to authenticating with the IMAP server.


    Use access token to authenticate

    Change the imap.login call to imap.authenticate:

    imap.authenticate(
        "XOAUTH2",
        lambda _: f"user={email}\1auth=Bearer {access_token}\1\1".encode()
    )
    

    The second parameter expects bytes, so I encode the formatted string.