Working with Microsoft Graph API Access Tokens in Python

Working with Microsoft Graph API Access Tokens in Python

If your organization is using Microsoft for Office productivity or your entire cloud services are powered by Microsoft Azure, chances are you might have come across Graph API for extending or integrating third party apps.

Just to be clear Graph API is not Graph QL. It is just another REST API service provided by Microsoft. More about it is here https://developer.microsoft.com/en-us/graph.

If you are simply testing your API calls using postman, you may not need to worry about access tokens for the Graph API a lot. Access token can be setup as a variable which can then be used later to pass in the headers. Here is the info on setting up access tokens in postman.

You can even use the Graph Explorer without even worrying about access tokens and refreshing them manually.

It gets a bit challenging to maintain multiple access tokens for different services and also continuously check whether or not a specific token is expired and avoid renewing the token unnecessarily.

First, let's look at how to get an Graph API access token in python before invoking any other REST API endpoints.

Step 1: Register an app in your Azure Portal under App Registrations

Step 2: Go to Certificates & Secrets and create a new client secret. Save the values as we need them later in our program.

Step 3. Go to API Permissions and add a permission. Select Microsoft Graph

Step 4: Choose Application Permissions since we are creating a background service in our case without a signed in user and get the access token.

Step 5: Filter & select the permissions needed for this app. In this case i'm selecting User.Read.All to be able to fetch the users in the organization.

Step 6: From Overview pane, get Client ID, Tenant ID. We already have a secret from step 2. Now we can programmatically get the access token in python.

Request an Access token

Here is a python function that returns an access token from Microsoft.

def get_access_token(tenant_id, client_id, client_secret, scope):
    token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    token_data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": scope,
    }

    try:
        token_response = requests.post(token_url, data=token_data)
        token_response.raise_for_status()
        return token_response.json().get("access_token")
    except requests.exceptions.RequestException as e:
        print("Error obtaining access token:", e)
        print("Response:", token_response.text)
        exit()

Now, let's say you are looking to fetch the users from Azure Tenant using Graph API. We need to set the scope accordingly. Here is the scope we need to pass to the above function.

graph_scope = "https://graph.microsoft.com/.default"
access_token = get_access_token(tenant_id, client_id, client_secret, graph_scope)

This access_token can now be used to make a request to the users endpoint in Graph API.

graph_headers = {
    "Authorization": "Bearer " + access_token,
    "Content-Type": "application/json",
}
users_endpoint = "https://graph.microsoft.com/v1.0/users"
try:
    response = requests.get(users_endpoint, headers=graph_headers)
    response.raise_for_status()
    users_data = response.json().get("value", [])

    return users_data

The access_token that is created is valid only up to 3600 seconds(1 hr) after which it needs to be refreshed to continue making requests to the Graph API endpoints. If you are expecting to have a service run longer than this duration, you should consider token expiry every time before making a call to an endpoint.

Renew an Access Token

from datetime import datetime, timedelta

def get_access_token(tenant_id, client_id, client_secret, scope):
    token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
    token_data = {
        "grant_type": "client_credentials",
        "client_id": client_id,
        "client_secret": client_secret,
        "scope": scope,
    }

    # Check if token is present and not expired
    if "access_token" not in get_access_token.__dict__ or datetime.utcnow() >= get_access_token.__dict__["token_expiration"]:
        # Refresh the token
        try:
            token_response = requests.post(token_url, data=token_data)
            token_response.raise_for_status()
            token_info = token_response.json()
            get_access_token.__dict__["access_token"] = token_info.get("access_token")
            expiration_seconds = token_info.get("expires_in", 0)
            get_access_token.__dict__["token_expiration"] = datetime.utcnow() + timedelta(seconds=expiration_seconds)
        except requests.exceptions.RequestException as e:
            print("Error obtaining access token:", e)
            print("Response:", token_response.text)
            exit()

    return get_access_token.__dict__["access_token"]

This function will now allow token renewals when a current one expires each time get_access_token() is called. This is sufficient if you are working with one Graph API scope. But, if your scope needs another resource such as Dynamics CRM or Intune etc., you are required to fetch another access token for those services with corresponding scope.

Managing Multiple Access Tokens in Same Application

Let's say we are working with Dynamics 365 within the same application using the same app registration. Then we have to set the scope for access_token like this.

dynamics365_scope = "https://myorg.dynamics.com/.default"
dynamics_access_token = get_access_token(tenant_id, client_id, client_secret, dynamics365_scope)

Now, get_access_token() function will not be able to keep track of all the different tokens that your application is requesting and refresh them when expired.

If this function is called again with a new scope, the unexpired token generated for previous request will still be returned. If the previously requested token is expired, it will be renewed with this new scope affecting other API calls that were depended on the previous access token. This function is not helpful when handling two or more access tokens.

If you are already familiar with these types of situations, it is a no brainer that we need to use a class in this case. Creating a class just for token management will be much more efficient than creating several functions for individual access token requests. We can simply convert this function into a class as shown below.

import requests
from datetime import datetime, timedelta

class AccessTokenManager:
    def __init__(self, tenant_id, client_id, client_secret, scope):
        self.tenant_id = tenant_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.scope = scope
        self.access_token = None
        self.token_expiration = None

    def get_access_token(self):
        if not self.access_token or datetime.utcnow() >= self.token_expiration:
            self.refresh_access_token()
            print(f"Access token renewed for {self.scope}")
        return self.access_token

    def refresh_access_token(self):
        token_url = f"https://login.microsoftonline.com/{self.tenant_id}/oauth2/v2.0/token"
        token_data = {
            "grant_type": "client_credentials",
            "client_id": self.client_id,
            "client_secret": self.client_secret,
            "scope": self.scope,
        }

        try:
            token_response = requests.post(token_url, data=token_data)
            token_response.raise_for_status()
            token_info = token_response.json()
            self.access_token = token_info.get("access_token")
            expiration_seconds = token_info.get("expires_in", 0)
            self.token_expiration = datetime.utcnow() + timedelta(seconds=expiration_seconds)
        except requests.exceptions.RequestException as e:
            print("Error obtaining access token:", e)
            print("Response:", token_response.text)
            exit()

This class can be now imported to any application your are developing to create token manager objects that are specific to a service without interfering with other access tokens.

from token_manager import AccessTokenManager

Now, it is easier to request, renew and manage multiple tokens in the same application using the AccessTokenManager Class.

#Example
graph_token_manager =  AccessTokenManager(tenant_id, client_id, client_secret, graph_scope)
dynamics_token_manager =  AccessTokenManager(tenant_id, client_id, client_secret, dynamics_scope)


print(graph_token_manager.get_access_token())
print(dynamics_token_manager.get_access_token())

As the reliance on Azure Cloud increases, there exists an increased need for creating new App Registrations every day for developers. Managing access tokens is a crucial part in developing applications that heavily rely on such cloud services. While this article aims at Azure in particular, access tokens are not different for other services. The underlying concept of requesting and renewing are the same. Hope you find this helpful for your application.