You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

获取有权限的Outlook Graph API刷新令牌问题排查

问题描述

我有一批格式为email:password:refresh token:client ID的Outlook邮箱账号,想要调用Outlook Graph API添加邮箱规则,但现有刷新令牌权限不足,需要生成新的刷新令牌。我已经创建了Microsoft Azure账号并配置了应用,获取了客户端ID、租户ID和密钥,但使用ROPC流代码获取令牌时出现AADSTS50034错误,提示用户账号不在指定目录中;另外之前用的一套通过用户名密码获取有效权限访问令牌的代码两周前失效了,请问怎么获取临时有权限的刷新令牌来完成邮箱规则添加?

错误信息

{'error': 'invalid_grant', 'error_description': 'AADSTS50034: The user account {EUII Hidden} does not exist in the a7163cca-35ea-483a-837e-ae68f9820cff directory. To sign into this application, the account must be added to the directory. Trace ID: e20ef63b-6ca6-4f6a-be4e-36b9789b3400 Correlation ID: 10d267d7-1ecf-438f-bf06-9bc539ae0819 Timestamp: 2025-06-15 01:04:27Z', 'error_codes': [50034], 'timestamp': '2025-06-15 01:04:27Z', 'trace_id': 'e20ef63b-6ca6-4f6a-be4e-36b9789b3400', 'correlation_id': '10d267d7-1ecf-438f-bf06-9bc539ae0819', 'error_uri': 'https://login.microsoftonline.com/error?code=50034'}

ROPC流代码

def get_tokens_ropc(tenant_id, client_id, client_secret, username, password):
    url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    
    data = {
        'grant_type': 'password',
        'client_id': client_id,
        'client_secret': client_secret,
        'scope': 'https://graph.microsoft.com/.default',
        'username': username,
        'password': password
    }
    
    response = requests.post(url, data=data)
    return response.json()

tokens = get_tokens_ropc(
    tenant_id=TENANT_ID,
    client_id=CLIENT_ID, 
    client_secret=SECRET,
    username="JobyTrible235@outlook.com",
    password="5Bfhac7yKB"
)

原失效代码

def get_headers(additional_headers: dict = {}):
    _DEFAULT_HEADERS = {
        'accept': '*/*',
        'accept-encoding': 'gzip, deflate, br',
        'accept-language': 'en-US,en;q=0.9',
        'sec-ch-ua': '"Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': 'Windows',
        'sec-fetch-dest': 'empty',
        'sec-fetch-mode': 'cors',
        'sec-fetch-site': 'same-origin',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Thunderbird/128.2.3'
    }

    _DEFAULT_HEADERS.update(additional_headers)
    return _DEFAULT_HEADERS

def handler_let_app(postUrl: str, respStr2: str, cookies, proxies):
    post_headers = get_headers({'content-type': "application/x-www-form-urlencoded"})

    matches = re.finditer("<input type=\"hidden\" name=\"(.*?)\" id=\"(.*?)\" value=\"(.*?)\"", respStr2)

    my_dict = {}
    for match in matches:
        my_dict[match.group(1)] = match.group(3)

    encoded_data = urllib.parse.urlencode(my_dict)

    my_dict["ucaction"] = "Yes"
    encoded_data = urllib.parse.urlencode(my_dict)

    letapp_resp2 = requests.post(postUrl, data=encoded_data, headers=post_headers, cookies=cookies, allow_redirects=False, proxies=proxies)

    redirect_url = letapp_resp2.headers.get('Location')

    loginLive_resp2 = requests.post(redirect_url, data=encoded_data, headers=post_headers, cookies=cookies, allow_redirects=False, proxies=proxies)

    redirect_url = loginLive_resp2.headers.get('Location')

    return redirect_url

def authenticate_username_password(email: str, password: str, proxies):
    newUrl1 = "https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=9e5f94bc-e8a4-4e73-b8be-63364c29d753&redirect_uri=https://localhost&scope=offline_access https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/MailboxSettings.ReadWrite&login_hint=" + email

    headers = get_headers()
    post_headers = get_headers({'content-type': "application/x-www-form-urlencoded"})

    resp1 = requests.get(newUrl1, headers=headers, proxies=proxies)
    respStr1 = resp1.text

    match  = re.search("https://login.live.com/ppsecure/post.srf?(.*?)',", respStr1)
    postUrl = "https://login.live.com/ppsecure/post.srf" + match.group(1)
    match1  = re.search("<input type=\"hidden\" name=\"PPFT\" id=\"(.*?)\" value=\"(.*?)\"", respStr1)
    valuePPFT =  match1.group(2)

    bodyLogin = f"ps=2&psRNGCDefaultType=&psRNGCEntropy=&psRNGCSLK=&canary=&ctx=&hpgrequestid=&PPFT={valuePPFT}&PPSX=Passp&NewUser=1&FoundMSAs=&fspost=0&i21=0&CookieDisclosure=0&IsFidoSupported=1&isSignupPost=0&isRecoveryAttemptPost=0&i13=1&login={email}&loginfmt={email}&type=11&LoginOptions=1&lrt=&lrtPartition=&hisRegion=&hisScaleUnit=&passwd={password}"
    cookies = resp1.cookies.get_dict()

    login_response = requests.post(postUrl, data=bodyLogin, headers=post_headers, cookies=cookies, allow_redirects=False, proxies=proxies)
    redirect_url = login_response.headers.get('Location')
    cookies = login_response.cookies.get_dict()

    respStr2 = login_response.text

    if redirect_url is None or redirect_url == "":
        match = re.search("id=\"fmHF\" action=\"(.*?)\"", respStr2)
        postUrl = match.group(1)

        if "Update?mkt=" in postUrl:
            redirect_url = handler_let_app(postUrl, respStr2, cookies, proxies)
        elif "confirm?mkt=" in postUrl:
            pass
        elif "Add?mkt=" in postUrl:
            pass

    if not redirect_url:
        return None
    
    localhostCode = redirect_url.split('=')[1]
    bodyRequest3 = f"code={localhostCode}&client_id=9e5f94bc-e8a4-4e73-b8be-63364c29d753&redirect_uri=https://localhost&grant_type=authorization_code"
    login_response = requests.post("https://login.microsoftonline.com/common/oauth2/v2.0/token", data=bodyRequest3, headers=post_headers, proxies=proxies).json()

    return login_response.get("refresh_token")

def get_access_token_from_refresh(refresh_token: str, proxies) -> str:
    token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"

    data = {
        'client_id': '9e5f94bc-e8a4-4e73-b8be-63364c29d753',
        'scope': 'https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/MailboxSettings.ReadWrite',
        'refresh_token': refresh_token,
        'grant_type': 'refresh_token',
        'redirect_uri': 'https://localhost'
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    response = requests.post(token_url, data=data, headers=headers, proxies=proxies)

    if response.status_code == 200:
        return response.json().get("access_token")
    else:
        print("Mail Forwarding: Failed to get access token:", response.text)
        return None

解决方案

1. 修复ROPC流的AADSTS50034错误

Outlook.com个人账号属于Microsoft个人租户,并非你创建的Azure AD租户,因此用自己的租户ID会触发账号不在目录的错误。修正步骤:

  • 将请求URL中的tenant_id替换为common,个人账号必须使用公共租户端点:
    url = f"https://login.microsoftonline.com/common/oauth2/v2.0/token"
    
  • 在Azure门户的应用注册中,将支持的账户类型设置为“任何组织目录中的账户和个人Microsoft账户(例如Skype、Outlook.com)”。
  • 添加Mail.ReadWrite、MailboxSettings.ReadWrite和offline_access委托权限,个人账号登录时会自动完成授权。

2. 替代原失效的模拟浏览器登录代码

原代码依赖登录页面DOM结构,极易因Microsoft页面更新失效,推荐改用官方支持的授权码流程(可通过无头浏览器自动化):

自动化授权码流程示例

  1. 用Playwright或Selenium打开授权URL:
    https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=你的客户端ID&response_type=code&redirect_uri=https://localhost&scope=offline_access Mail.ReadWrite MailboxSettings.ReadWrite
    
  2. 自动输入用户名和密码完成登录(若账号启用MFA,需手动干预或改用设备代码流)。
  3. 提取回调URL中的授权码,调用令牌端点交换刷新令牌:
    def get_refresh_token_from_code(client_id, client_secret, code, redirect_uri):
        url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
        data = {
            "grant_type": "authorization_code",
            "client_id": client_id,
            "client_secret": client_secret,
            "code": code,
            "redirect_uri": redirect_uri
        }
        response = requests.post(url, data=data)
        return response.json().get("refresh_token")
    

3. 核心权限说明

添加邮箱规则必须的委托权限:

  • Mail.ReadWrite:读写邮件及创建规则
  • offline_access:获取刷新令牌,用于长期获取访问令牌
  • MailboxSettings.ReadWrite:可选,若规则涉及邮箱基础设置调整

注意事项

  • ROPC流不支持启用MFA的账号,此类账号需使用授权码或设备代码流。
  • 个人Microsoft账号仅支持委托权限,无法使用客户端凭据流(应用权限)。
  • 避免依赖模拟浏览器登录,此类方法稳定性极差,Microsoft会频繁更新登录页面结构导致代码失效。

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 21:19:55