通过OAuth2和python MSAL库进行的Office 365 IMAP认证
我正在尝试升级一个传统的邮件机器人,以便通过Oauth2而不是Basic认证进行认证,因为它是从现在起两天后讲被弃用。
该文件指出,应用程序可以保留其原来的逻辑,而只换掉认证位。
Application developers who have built apps that send, read, or otherwise process email using these protocols will be able to keep the same protocol, but need to implement secure, Modern authentication experiences for their users. This functionality is built on top of Microsoft Identity platform v2.0 and supports access to Microsoft 365 email accounts.
注意我明确地选择了客户证书流程,因为文档中说到
This type of grant is commonly used for server-to-server interactions that must run in the background, without immediate interaction with a user.
所以我有一个python脚本,使用MSAL python库检索一个访问令牌。现在我正试图使用该访问令牌与IMAP服务器进行验证。我想我的情况和这个很接近,只是我连接的是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服务器上获得更多关于认证失败原因的有力信息?
我所看的东西
注意这个答案不再有效,因为建议的作用域未能生成访问令牌。
客户端凭证流程似乎规定了
https://graph.microsoft.com/.default
授予。我不确定这是否包括IMAP资源所需的范围https://outlook.office.com/IMAP.AccessAsUser.All
?根据MS文档中的示例,验证了从Google线程中提取的代码能够正确地产生SASL XOAUTH2字符串。
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
- 这个主题似乎表明需要获取多个令牌,一个用于图形,另一个用于IMAP连接;这可能是我所遗漏的吗?
试试下面的步骤。
对于客户凭证流,你需要在应用注册中指定 "应用权限",而不是 "委托权限"。
- 添加权限 "Office 365 Exchange Online / IMAP.AccessAsApp"(应用程序)。
- 授予管理员对您的申请的同意权
- 服务原则和交流。
- 一旦服务委托人在Exchange Online上注册,管理员就可以运行Add-Mailbox Permission cmdlet来为服务委托人分配接收权限。
- 使用范围'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')
https://outlook.office365.com/.default
。我们还分配了"应用权限",正如你所建议的。最后是这个问题和MS方面的一个变化。这是与SPN的对象标识有关的问题。当你进行注册时,企业应用程序被创建,企业应用程序的对象标识需要被使用。
- quickshiftin 2022-10-12
imaplib.IMAP4.error: AUTHENTICATE failed
错误的发生是因为文档中的一个点不是那么清楚。
在通过Powershell设置服务主体时,你需要输入App-ID和一个Object-ID。很多人会认为,这就是你在已注册的App的概览页面上看到的Object-ID,但事实并非如此!此时,你需要从"Azure Active Directory --> Enterprise Applications --> Your-App --> Object-ID"获得Object-ID。
New-ServicePrincipal -AppId <APPLICATION_ID> -ServiceId <OBJECT_ID> [-Organization <ORGANIZATION_ID>]
微软公司说:
The OBJECT_ID is the Object ID from the Overview page of the Enterprise Application node (Azure Portal) for the application registration. It is not the Object ID from the Overview of the App Registrations node. Using the incorrect Object ID will cause an authentication failure.
当然,你需要注意API权限和其他东西,但对我来说,这才是重点。 所以,让我们再看一遍,就像文档页上所解释的那样。 使用OAuth认证IMAP、POP或SMTP连接。
- 在你的租户中登记申请
- 为应用程序设置一个客户端密钥。
- 设置API权限,选择我的组织使用的API标签,并搜索"Office 365 Exchange Online"->应用权限->选择IMAP和IMAP.AccessAsApp
- 在邮箱上为你的应用程序设置服务主体和完整的访问权限。
- 检查邮箱的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分钟,让变化生效,然后进行测试。
用这个脚本试试吧:
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