pythonazuremicrosoft-entra-id

Azure auth for Outlook Graph API


I have a list of outlook emails that come in the format

email:password:refresh token:client ID

I want to access the outlook graph API to add some mailbox rules. However the refresh token is generated with not enough permissions to do so, so it seems I need to make a new refresh token. I made a Microsoft Azure account and setup an application and located the client id and tenant id and added a secret. When I try to code below to get a refresh token I get the error below because the client ID for the account does not match my client ID I am assuming. How can I get temp access to a refresh token to simply add this mailbox rule?

{'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'}
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"
)

Also I used to use the code below but it stopped working 2 weeks ago. The code below let me simply feed the email and password and get a good access token with the right perms.

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

Solution

  • The error The user account {EUII Hidden} does not exist in the a7163cca-35ea-483a-837e-ae68f9820cff directory occurred when you specify tenant_id in the token URL, the personal outlook account isn't part of your tenant. Hence Azure AD can't find the user.

    I too got the same error as you when I try your code.

    enter image description here

    To fix the issue you can use below code.

    import requests
    import webbrowser
    import threading
    from flask import Flask, request
    # Replace with your app's values
    CLIENT_ID = "client_id_of your_app"
    CLIENT_SECRET = "client_secret"
    REDIRECT_URI = "http://localhost:8000/callback"
    SCOPES = "offline_access Mail.ReadWrite MailboxSettings.ReadWrite"
    auth_url = (  f"https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
        f"?client_id={CLIENT_ID}"
        f"&response_type=code"
        f"&redirect_uri={REDIRECT_URI}"
        f"&response_mode=query"
        f"&scope={SCOPES}"
        f"&state=12345"
    )
    # Initiate local Flask server to catch redirect
    app = Flask(__name__)
    auth_code = None
    
    @app.route('/callback')
    def callback():
        global auth_code
        auth_code = request.args.get('code')
        return "Authorization code received. You may close this tab."
    
    def run_server():
        app.run(port=8000)
    threading.Thread(target=run_server, daemon=True).start()
    print("Opening browser for login...")
    webbrowser.open(auth_url)
    while auth_code is None:
        pass
    token_url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
    
    data = {
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'grant_type': 'authorization_code',
        'code': auth_code,
        'redirect_uri': REDIRECT_URI,
        'scope': SCOPES,
    }
    
    response = requests.post(token_url, data=data)
    tokens = response.json()
    print("Access Token:", tokens.get("access_token", ""))
    print("Refresh Token:", tokens.get("refresh_token", ""))
    
    access_token = tokens.get("access_token")
    
    if access_token:
        rule_url = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messageRules"
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json"
        }
        rule = {
            "displayName": "Test Rule",
            "sequence": 1,
            "conditions": {
                "subjectContains": ["phishing"]
            },
            "actions": {
                "moveToFolder": "junkemail",
                "stopProcessingRules": True
            },
            "isEnabled": True
        }
        rule_response = requests.post(rule_url, headers=headers, json=rule)
        print("Rule creation status:", rule_response.status_code)
        print("Response:", rule_response.json())
    else:
        print("Failed to get access token.")
    

    enter image description here

    As you see I got the refresh token and using that created mailbox rule.