Reputation: 2752
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:
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
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”.
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
Reputation: 1
Steps:
Upvotes: 0
Reputation: 21
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