1

I'm trying to create a python script that continuously reads mail from a service account in my organization. I'm attempting to use the Microsoft Graph API, but the more I read, the more confused I get. I have registered an app in Azure Portal and have my client id, client secret, etc, then it's my understanding you have to use those, call the API that requires you to paste a url into your browser to log in to consent access, and that provides a token that only lasts an hour? How can I do this programmatically?

I guess my question is, has anyone had any luck doing this with the graph api? How can I do this without having to do the browser handshake every hour? I would like to be able to just run this script and let it run without worrying about needing to refresh a token ever so often. Am I just dumb, or is this way too complicated lol. Any python examples on how people are authenticating to the graph api and staying authenticated would be greatly appreciated!

1
  • 1
    In that case you can use ROPC using Microsoft Identity For Python there you can get the token, once you have the token then just call graph API using that token here is the sample for you. Commented Nov 19, 2021 at 1:18

1 Answer 1

2

I was just working on something similar today. (Microsoft recently deprecated basic authentication for exchange, and I can no longer send mail using a simple username/password from a web application I support.)

Using the microsoft msal python library https://github.com/AzureAD/microsoft-authentication-library-for-python, and the example in sample/device_flow_sample.py, I was able to build a user-based login that retrieves an access token and refresh token in order to stay logged in (using "device flow authentication"). The msal library handles storing and reloading the token cache, as well as refreshing the token whenever necessary.

Below is the code for logging in the first time

#see https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/dev/sample/device_flow_sample.py
import sys
import json
import logging
import os
import atexit
import requests
import msal

#   logging

logging.basicConfig(level=logging.DEBUG)  # Enable DEBUG log for entire script
logging.getLogger("msal").setLevel(logging.INFO)  # Optionally disable MSAL DEBUG logs


#   config

config = dict(
    authority = "https://login.microsoftonline.com/common",
    client_id = 'YOUR CLIENT ID',
    scope = ["User.Read"],
    username = 'user@domain',
    cache_file = 'token.cache',
    endpoint = 'https://graph.microsoft.com/v1.0/me'
)


#   cache

cache = msal.SerializableTokenCache()
if os.path.exists(config["cache_file"]):
    cache.deserialize(open(config["cache_file"], "r").read())

atexit.register(lambda:
    open(config["cache_file"], "w").write(cache.serialize())
    if cache.has_state_changed else None)


#   app

app = msal.PublicClientApplication(
    config["client_id"], authority=config["authority"],
    token_cache=cache)


#   exists?

result = None
accounts = app.get_accounts()
if accounts:
    logging.info("found accounts in the app")
    for a in accounts:
        print(a)
        if a["username"] == config["username"]:
            result = app.acquire_token_silent(config["scope"], account=a)
            break
else:
    logging.info("no accounts in the app")

#   initiate

if result:
    logging.info("found a token in the cache")
else:
    logging.info("No suitable token exists in cache. Let's get a new one from AAD.")

    flow = app.initiate_device_flow(scopes=config["scope"])
    if "user_code" not in flow:
        raise ValueError(
            "Fail to create device flow. Err: %s" % json.dumps(flow, indent=4))

    print(flow["message"])
    sys.stdout.flush()  # Some terminal needs this to ensure the message is shown

    # Ideally you should wait here, in order to save some unnecessary polling
    input("Press Enter after signing in from another device to proceed, CTRL+C to abort.")

    result = app.acquire_token_by_device_flow(flow)  # By default it will block
        # You can follow this instruction to shorten the block time
        #    https://msal-python.readthedocs.io/en/latest/#msal.PublicClientApplication.acquire_token_by_device_flow
        # or you may even turn off the blocking behavior,
        # and then keep calling acquire_token_by_device_flow(flow) in your own customized loop.

if result and "access_token" in result:
    # Calling graph using the access token
    graph_data = requests.get(  # Use token to call downstream service
        config["endpoint"],
        headers={'Authorization': 'Bearer ' + result['access_token']},).json()
    print("Graph API call result: %s" % json.dumps(graph_data, indent=2))
else:
    print(result.get("error"))
    print(result.get("error_description"))
    print(result.get("correlation_id"))  # You may need this when reporting a bug

You'll need to fix up the config, and update the scope for the appropriate privileges.

All the magic is in here:

    result = app.acquire_token_silent(config["scope"], account=a)

and putting the Authorization access_token in the requests headers:

    graph_data = requests.get(  # Use token to call downstream service
        config["endpoint"],
        headers={'Authorization': 'Bearer ' + result['access_token']},).json()

As long as you call acquire_token_silent before you invoke any graph APIs, the tokens will stay up to date. The refresh token is good for 90 days or something, and automatically updates. Once you login, the tokens will be updated and stored in the cache (and persisted to a file), and will stay alive more-or-less indefinitely (there are some things that can invalidate it on the server side).

Unfortunately, I'm still having problems because it's an unverified multi-tenant application. I successfully added the user as a guest in my tenant, and the login works, but as soon as I try to get more interesting privileges in scope, the user can't log in - I'll either have to get my mpn verified, or get my client's 3rd party IT guys admin to grant permission for this app in their tenant. If I had admin privileges for their tenant, I'd probably be looking at the daemon authentication method instead of user-based.

(to be clear, the code above is the msal example almost verbatim, with config and persistence tweaks)

Sign up to request clarification or add additional context in comments.

7 Comments

I can't help but thinking "there's got to be an easier way".
Thank you @dirck! This is what I was playing around with but couldn't get working. This helps a lot. After going to microsoft.com/devicelogin and entering the code and logging in, it says it was successful, but when I go back to the application and press enter, it gives me an error "invalid_client" with this message AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'. Any ideas? Also, how often will I need to do the browser login? Am I going to need to do this browser handshake every so often to keep my app working?
Refresh tokens are good for 90 days by default. As long as you use the application more often than that, you shouldn't need to log in again. Ref: learn.microsoft.com/en-us/azure/active-directory/develop/…
Excellent :-) FYI, I got my authorization problem solved by getting the other-tenant admin to invoke https://login.microsoftonline.com/{their-tenant-id}/adminconsent?client_id={my-client-id} which gave them an error because I hadn't configured a reply URL, but it enabled my app to run.
I started looking at this and even implemented it, but I just had the air deflate out of me. I just can't deal with this cr*p just for a simple mail login....
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.