通过OAuth2和Python MSAL库进行Office 365 IMAP身份验证

21

我正在尝试将一个传统的邮件机器人升级,以便通过Oauth2进行身份验证,而不是使用基本身份验证,因为基本身份验证将在两天后被废弃

文档指出,应用程序可以保留其原始逻辑,只需更换身份验证部分即可。

构建使用这些协议发送、读取或处理电子邮件的应用程序开发人员将能够保持相同的协议,但需要为其用户实现安全的现代身份验证体验。此功能基于Microsoft Identity平台v2.0构建,并支持访问Microsoft 365电子邮件帐户。

请注意,我明确选择了客户端凭据流程,因为文档指出:

此类授权通常用于必须在后台运行而不需要与用户立即交互的服务器间交互。

因此,我有一个使用MSAL python库检索访问令牌的Python脚本。现在我正在尝试使用该访问令牌进行与IMAP服务器的身份验证。有一些现有的线程展示了如何连接到Google,我想我的情况非常接近这个,只不过我要连接到Office 365 IMAP服务器。以下是我的脚本:

import imaplib
import msal
import logging

app = msal.ConfidentialClientApplication(
    'client-id',
    authority='https://login.microsoftonline.com/tenant-id',
    client_credential='secret-key'
)

result = app.acquire_token_for_client(scopes=['https://graph.microsoft.com/.default'])

def generate_auth_string(user, token):
  return 'user=%s\1auth=Bearer %s\1\1' % (user, token)

# IMAP time!
mailserver = 'outlook.office365.com'
imapport = 993
M = imaplib.IMAP4_SSL(mailserver,imapport)
M.debug = 4
M.authenticate('XOAUTH2', lambda x: generate_auth_string('user@mydomain.com', result['access_token']))

print(result)

IMAP认证失败,尽管设置了M.debug = 4,输出并不是很有帮助。

  22:56.53 > b'DBDH1 AUTHENTICATE XOAUTH2'
  22:56.53 < b'+ '
  22:56.53 write literal size 2048
  22:57.84 < b'DBDH1 NO AUTHENTICATE failed.'
  22:57.84 NO response: b'AUTHENTICATE failed.'
Traceback (most recent call last):
  File "/home/ubuntu/mini-oauth.py", line 21, in <module>
    M.authenticate("XOAUTH2", lambda x: generate_auth_string('user@mydomain.com', result['access_token']))
  File "/usr/lib/python3.10/imaplib.py", line 444, in authenticate
    raise self.error(dat[-1].decode('utf-8', 'replace'))
imaplib.IMAP4.error: AUTHENTICATE failed.

有任何想法我可能做错了什么,或怎样从IMAP服务器获取更强大的信息来解决认证失败的问题?

我看过的东西

import base64

user = 'test@contoso.onmicrosoft.com'
token = 'EwBAAl3BAAUFFpUAo7J3Ve0bjLBWZWCclRC3EoAA'

xoauth = "user=%s\1auth=Bearer %s\1\1" % (user, token)

xoauth = xoauth.encode('ascii')
xoauth = base64.b64encode(xoauth)
xoauth = xoauth.decode('ascii')

xsanity = 'dXNlcj10ZXN0QGNvbnRvc28ub25taWNyb3NvZnQuY29tAWF1dGg9QmVhcmVyIEV3QkFBbDNCQUFVRkZwVUFvN0ozVmUwYmpMQldaV0NjbFJDM0VvQUEBAQ=='

print(xoauth == xsanity) # prints True
  • 这个帖子 建议需要获取多个令牌,一个用于Graph API,另一个则用于IMAP连接;那可能是我遗漏的吗?

1
我有同样的问题。 还有另一个关于这个问题的提问。 在尝试使用之前获得的令牌进行IMAP身份验证后,出现相同的错误:“AUTHENTICATE失败”。 如果您找到解决方案,请发布它。 - Sardar Agabejli
5个回答

8

请尝试以下步骤。

在应用程序注册中,对于客户端凭据流程,您需要分配“应用程序权限”,而不是“委派的权限”。

  1. 添加权限“Office 365 Exchange Online / IMAP.AccessAsApp”(应用程序)。 enter image description here
  2. 向您的应用程序授予管理员同意。
  3. 服务主体和Exchange。
  4. 注册了服务主体后,管理员可以运行Add-Mailbox Permission cmdlet来分配接收权限给服务主体。
  5. 使用范围'https://outlook.office365.com/.default'。

现在,您可以通过将此访问令牌和邮箱用户名组合在一起来生成SALS身份验证字符串,以便使用IMAP4进行身份验证。

#Python 代码

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')

2
你回答中正确的一件事是范围,我将其更改为 https://outlook.office365.com/.default。我们还分配了“应用程序权限”,正如你所建议的那样。最终问题出在这里以及 MS 端的更改。这与 SPN 的 Object Id 有关。当你进行注册时,企业应用程序会被创建,并且需要使用企业应用程序的 Object Id。 - quickshiftin
@quickshiftin 你好,很高兴听到你解决了这个问题。你能否详细说明一下你评论的后半部分吗?从“最后是这个和MS方面的改变……等等”开始。我无法弄清楚需要做什么。谢谢! - henry434
2
嘿@henry434,我有一些来自微软的复盘笔记。需要得到客户的许可才能在这里分享,但我想他们会同意的,并且我将在接下来的几天内添加一个带有笔记的答案。抱歉耽搁了。 - quickshiftin
1
@quickshiftin 感谢您提供 Object-ID 的提示,经过检查后现在它对我有效了!我已经分享了一个答案。 :) - Sardar Agabejli
在这种情况下,我们如何连接到共享邮箱? - Andoni
显示剩余2条评论

5

imaplib.IMAP4.error: AUTHENTICATE failed错误发生的原因是文档中有一个点不够清晰。

在通过PowerShell设置服务主体时,您需要输入App-ID和Object-ID。许多人会认为这是您在注册应用程序的概述页面上看到的Object-ID,但事实并非如此!

此时,您需要来自"Azure Active Directory->企业应用程序->Your-App->Object-ID"的Object-ID。

New-ServicePrincipal -AppId <APPLICATION_ID> -ServiceId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]

Microsoft表示:

OBJECT_ID是企业应用程序节点(Azure门户)概述页面中的对象ID,用于应用程序注册。它不是App Registrations节点概述中的Object ID。使用错误的Object ID将导致身份验证失败。

当然你还需要注意API权限和其他内容,但对我来说,这就是重点。所以让我们再次浏览一下文档页面上的内容。 使用OAuth验证IMAP、POP或SMTP连接

  1. 在租户中注册应用程序
  2. 为应用程序设置客户端密钥
  3. 设置API权限,选择“我的组织使用的API”选项卡并搜索“Office 365 Exchange Online” -> 应用程序权限 -> 选择IMAP和IMAP.AccessAsApp
  4. 为应用程序设置服务主体和对邮箱的完全访问权限
  5. 检查邮箱是否已激活IMAP

这是我用来测试的代码:

import imaplib
import msal
import pprint

conf = {
    "authority": "https://login.microsoftonline.com/XXXXyourtenantIDXXXXX",
    "client_id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXX", #AppID
    "scope": ['https://outlook.office365.com/.default'],
    "secret": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", #Key-Value
    "secret-id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", #Key-ID
}
    
def generate_auth_string(user, token):
    return f"user={user}\x01auth=Bearer {token}\x01\x01"    

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['scope'])

    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 = imaplib.IMAP4('outlook.office365.com')
    imap.starttls()
    imap.authenticate("XOAUTH2", lambda x: generate_auth_string("target_mailbox@example.com", result['access_token']).encode("utf-8"))

设置了服务主体并在邮箱上授予了应用程序完全访问权限后,请等待15-30分钟以使更改生效并进行测试。

2
很高兴听到你已经解决了。让我与我的微软专家再次确认,如果他们认为这是一个公正的规范答案,我将接受并向您提供适当的奖励。 - quickshiftin

1
在一番小挣扎后(非微软用户),我成功地使用sardar-agabejli的示例代码进行了身份验证。我的问题在于不理解“设置服务主体”的含义。从Ubuntu Linux上,我需要做以下操作:
$ snap install powershell
$ pwsh
ps> install-module exchangeonlinemanagement
ps> Connect-ExchangeOnline
ps> New-ServicePrincipal -AppId <appid> -ObjectId <objid>
ps> Add-MailboxPermission -Identity <email@domain> -User <ObjectId> -AccessRights FullAccess`
ps> exit
$

根据上述说明,在Azure中查找应用程序ID和对象ID。如果您未能输入最终的PowerShell命令,您将进行身份验证但无法连接,导致出现此错误:SELECT command error: BAD [b'User is authenticated but not connected.']


0

请尝试使用这个脚本:

import json
import msal

import requests

client_id = '***'
client_secret = '***'
tenant_id = '***'
authority = f"https://login.microsoftonline.com/{tenant_id}"

app = msal.ConfidentialClientApplication(
    client_id=client_id,
    client_credential=client_secret,
    authority=authority)

scopes = ["https://graph.microsoft.com/.default"]

result = None
result = app.acquire_token_silent(scopes, account=None)

if not result:
    print(
        "No suitable token exists in cache. Let's get a new one from Azure Active Directory.")
    result = app.acquire_token_for_client(scopes=scopes)

# if "access_token" in result:
#     print("Access token is " + result["access_token"])


if "access_token" in result:
    userId = "***"
    endpoint = f'https://graph.microsoft.com/v1.0/users/{userId}/sendMail'
    toUserEmail = "***"
    email_msg = {'Message': {'Subject': "Test Sending Email from Python",
                             'Body': {'ContentType': 'Text', 'Content': "This is a test email."},
                             'ToRecipients': [{'EmailAddress': {'Address': toUserEmail}}]
                             },
                 'SaveToSentItems': 'true'}
    r = requests.post(endpoint,
                      headers={'Authorization': 'Bearer ' + result['access_token']}, json=email_msg)
    if r.ok:
        print('Sent email successfully')
    else:
        print(r.json())
else:
    print(result.get("error"))
    print(result.get("error_description"))
    print(result.get("correlation_id"))

来源: https://kontext.tech/article/795/python-send-email-via-microsoft-graph-api


2
感谢您的帖子,但有两个问题 - 1.我需要使用所有现有的IMAP库逻辑,我不想重写整个代码,只需更改身份验证。2.这是对SMTP的调用,我的问题是关于IMAP的。 - quickshiftin

0

我无法让上述任何解决方案起作用。在我看来,微软似乎不再希望您通过IMAP与office365电子邮件帐户进行交互,而是希望您使用Microsoft Graph Outlook REST API。以这种方式设置事情更简单,我个人发现API比IMAP更容易交互。

  1. 添加Microsoft Graph Mail.ReadWrite 权限,如此处所示
  2. 使用@amit的代码来get_access_token()并使用请求与您的邮件进行交互...将范围更改为“https://graph.microsoft.com/.default”
  3. 通过Graph API与邮件进行交互
import requests

access_token = get_access_token()  # from @amit's answer above
base_url = "https://graph.microsoft.com/v1.0"

# example url to list folders for a user's mailbox
url = f"{base_url}/users/{user_id}/mailFolders"
response = requests.get(
     url, 
     headers={
          'Authorization': 'Bearer ' + access_token['access_token']
     }
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接