Thomas Ward
Thomas Ward

Reputation: 2752

Use IMAP with XOAUTH2 and MS365 Application permissions to authenticate IMAP for a user

I have an older Python project that uses standard IMAP mechanisms to retrieve emails, etc. from a mailbox for processing. Unfortunately, with MS365 now retiring non-OAuth2 and non-modern auth, I have to try and write an application that does NOT rely on user-credentials but has full access to authentication, etc. as other users that uses OAuth2.

I've got the MSAL library part down and can get access tokens from the remote - this application is configured with the workflows to login with a client secret and has access to all EWS for all users and all IMAP.UseAsApp in the application's permissions. I may be doing the request for information incorrectly, though, via the app integration.

The application is operating on the following permissions assigned to it in Azure AD:

enter image description here

Said application has authentication via shared secrets, not certificates.

We're pulling the Outlook scope because we want to use Office 365 Exchange Online's IMAP scope and use things with IMAP auth via this token and oauth, and I don't believe the MIcrosoft Graph API has any IMAP auth endpoint mechanisms available,

Here's basically an example of what I've got going in an attempt to chain MSAL OAuth2 with an application configured in my Azure AD to get a working imap.authenticate call, to at least figure out how to get the OAuth2 parts completed with the bearer token:

import imaplib
import msal
import pprint
import base64

conf = {
    "authority": "https://login.microsoftonline.com/TENANT_ID",
    "client_id": "APP_CLIENT_ID",
    "scope": ["https://outlook.office.com/.default"],
    "secret": "APP_SECRET_KEY",
    "secret-id": "APP_SECRET_KEY (for documentation purposes)",
    "tenant-id": "TENANT_ID"
}


def generate_auth_string(user, token):
    authstr = f"user={user}\x01auth=Bearer {token}".encode('utf-8')
    return base64.b64encode(authstr)


if __name__ == "__main__":
    app = msal.ConfidentialClientApplication(conf['client_id'], authority=conf['authority'],
                                             client_credential=conf['secret'])
    
    result = app.acquire_token_silent(conf['scope'], account=None)
    
    if not result:
        print("No suitable token in cache.  Get new one.")
        result = app.acquire_token_for_client(scopes=conf['scopes'])
    
    if "access_token" in result:
        print(result['token_type'])
        pprint.pprint(result)
    else:
        print(result.get("error"))
        print(result.get("error_description"))
        print(result.get("correlation_id"))
    
    # IMAP time!
    imap = imaplib.IMAP4('outlook.office365.com')
    imap.starttls()
    imap.authenticate("XOAUTH2", lambda x: generate_auth_string('[email protected]',
                                                                result['access_token']))
    
    # more IMAP stuff after this, but i haven't gotten further than this.

I get an AUTHENTICATE failed message every time I use this to access a valid user's account using AUTHENTICATE. The reason this has to be done as the application and not via delegated user authentication here is because it's a headless python application that needs to access numerous inboxes via IMAP (to pull RFC822 format messages) and not just one specific mailbox, and we want to not have to generate individual OAuth tokens for individual users, we would rather just have it at the application level.

Does someone know what I'm doing wrong, here? Or if someone can point me in the right direction to an example that would work, that'd be great.

Upvotes: 8

Views: 16781

Answers (3)

Amit
Amit

Reputation: 211

Try below steps as it worked for me.

For Client Credentials Flow you need to assign “Application permissions” in the app registration, instead of “Delegated permissions”.

  1. Add permission “Office 365 Exchange Online / IMAP.AccessAsApp” (application). enter image description here
  2. Grant admin consent to you application.
  3. Service Principals and Exchange.
  4. Once a service principal is registered with Exchange Online, administrators can run the Add-MailboxPermission cmdlet to assign receive permissions to the service principal, just like the granting of regular delegate access to mailboxes.
  5. Use scope 'https://outlook.office365.com/.default'.

Now you can generate the SALS authentication string by combining this access token and the mailbox username to authenticate with IMAP4.

#Python code

def get_access_token():
    tenantID = 'abc'
    authority = 'https://login.microsoftonline.com/' + tenantID
    clientID = 'abc'
    clientSecret = 'abc'
    scope = ['https://outlook.office365.com/.default']
    app = ConfidentialClientApplication(clientID, authority=authority, 
          client_credential = clientSecret)
    access_token = app.acquire_token_for_client(scopes=scope)
    return access_token

 def generate_auth_string(user, token):
    auth_string = f"user={user}\1auth=Bearer {token}\1\1"
    return auth_string

#IMAP AUTHENTICATE
 imap = imaplib.IMAP4_SSL(imap_host, 993)
 imap.debug = 4
 access_token = get_access_token_to_authenticate_imap()
 imap.authenticate("XOAUTH2", lambda x:generate_auth_string(
      'useremail',
       access_token['access_token']))
 imap.select('inbox')

Upvotes: 8

SAYAL RAZA
SAYAL RAZA

Reputation: 1

Steps:

  1. Enable IMAP for the account you are trying to access through code. https://learn.microsoft.com/en-us/exchange/clients/pop3-and-imap4/configure-mailbox-access?view=exchserver-2019
  2. Go to https://myaccount.microsoft.com/ after logging into outlook mail of the account you are trying to access through code.
  3. Go to Security Info.
  4. Add Sign-in Method
  5. Click App-Password and give a name to the Method.
  6. Copy the app password.
  7. Replace this app password with conventional password in code and try logging in with imaplib library.

Upvotes: 0

I'm trying to do something similar. The following change worked for me:

def generate_auth_string(user, token):
    return f"user={user}\x01auth=Bearer {token}\x01\x01"

Upvotes: 2

Related Questions