8

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('example@example.com',
                                                                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.

snakecharmerb
  • 47,570
  • 11
  • 100
  • 153
Thomas Ward
  • 2,714
  • 7
  • 36
  • 51
  • Since you are using client credential flow these docs apply: [Use client credentials grant flow to authenticate IMAP and POP connections](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#use-client-credentials-grant-flow-to-authenticate-imap-and-pop-connections). You show granting the permission to the app, but it looks like you also need to register the service principals in exchange by creating a service principal for the App and then granting specific mailbox permissions on that service principal. – Christopher Doyle Sep 01 '22 at 15:11

3 Answers3

7

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')
Amit
  • 171
  • 1
  • 6
  • Hi, thanks for the answer.. im also stuck with this. Can you give some more info for step 3 and 4? Im not sure what you mean by "Configure the permissions on the mailboxes you want to access." Thanks – henry434 Oct 12 '22 at 14:43
  • 2
    Hi, after registering your app and adding permission “Office 365 Exchange Online / IMAP.AccessAsApp” (application) refer this link https://office365itpros.com/2022/07/04/exchange-online-imap4-pop3/? – Amit Oct 13 '22 at 14:34
  • 1
    Thanks - i've followed the steps: 1. made `New-ServicePrincipal` and 2. `Add-MailboxPermission` for user, but im still getting an `imaplib.error: AUTHENTICATE failed.` on `conn.authenticate("XOAUTH2", lambda x: GenerateOAuth2String(user, result["access_token"]))` .... im pulling my hair out! Do you have any more suggestions?? Under the imap debug messages, can i ask how big your `write literal size ` is please? – henry434 Oct 13 '22 at 14:57
  • @henry434 my literal size is 2112 . I was also getting imaplib.error: AUTHENTICATE failed because in generate_auth_string I was using encode('utf-8') in auth_string and then was returning the string with base64.b64encode(auth_string). So, check and if you are also making the same mistake. If yes, then use above generate_auth_string function which I have mentioned in my Ans. – Amit Oct 14 '22 at 08:22
  • Thanks for the response. I was doing the steps correctly however a stupid error from me was failing the login .... I did not change the scope from `["https://graph.microsoft.com/.default"]` to `["https://outlook.office365.com/.default"]` .... that's a few days of my life im not getting back lol! Thanks for the help – henry434 Oct 18 '22 at 11:25
  • Thanks for your answer, was still stuck after creating the service principals and adding mailbox access. It took about 30 minutes to actually get access. – 3c71 Dec 08 '22 at 12:10
  • I believe I am struggling with step#3, specifically acquiring the correct ServiceID for the New-ServicePrincipal command. Instructions I found indicate there should be an "POP3 and IMAP4 OAuth 2.0 Authorization"" listed by Get-MgServicePrincipal, but there is not such an entry. – Adam Tauno Williams Jan 23 '23 at 20:52
2

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"
  • Thanks for catching that mistake on my end. Unfortunately, AUTHETNICATE requires a Bytes like response, so I still have to encode it. And it's still returning "AUTHENTICATE FAILED" on IMAP calls. So I'm confused what i'm doing wrong here. Or whether I've just got the app in the wrong configuration. – Thomas Ward Aug 17 '22 at 16:11
  • 1
    imaplib takes care of the base64 encoding, so don't provide a base64 encoded string. I'm not sure if `bytes` are valid input, but providing a `str` worked for me and was suggested here: https://stackoverflow.com/a/13467538/19785964. If providing a string does not work, the issue must be with the AAD permissions because the token is valid if you reach that point in the code. – ImitiationIsAFormOfFlattery Aug 18 '22 at 07:48
0

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.
halfer
  • 19,824
  • 17
  • 99
  • 186
  • I think this is the way, when MFA is enabled. But we have to use Oauth2 and access the Mailbox via the App, that has FullAccess on the Mailbox. Like you can see in the opening Post, there is no Password. Unfortunately this will not work in this case. – Sardar Agabejli Sep 30 '22 at 13:21