Search code examples
pythonazureazure-active-directory

Microsoft Azure Public Client Application flow error


I want to do upload some files from local to my OneDrive. This case I'm using MSAL(Microsoft Authentication Library) library. There is a little issue in ms_graph.py file. The flow user_code doesn't responding back correctly. How can I fix this issue? In the normal case code needs to print user_code to access OneDrive account.

ms_graph.py:

import webbrowser
from datetime import datetime
import json
import os
import msal

GRAPH_API_ENDPOINT = 'https://graph.microsoft.com/v1.0'


def generate_access_token(app_id, scopes):
    # Save Session Token as a token file
    access_token_cache = msal.SerializableTokenCache()

    # read the token file
    if os.path.exists('ms_graph_api_token.json'):
        access_token_cache.deserialize(open("ms_graph_api_token.json", "r").read())
        token_detail = json.load(open('ms_graph_api_token.json', ))
        token_detail_key = list(token_detail['AccessToken'].keys())[0]
        token_expiration = datetime.fromtimestamp(int(token_detail['AccessToken'][token_detail_key]['expires_on']))
        if datetime.now() > token_expiration:
            os.remove('ms_graph_api_token.json')
            access_token_cache = msal.SerializableTokenCache()

    # assign a SerializableTokenCache object to the client instance
    client = msal.PublicClientApplication(client_id=app_id, token_cache=access_token_cache)

    accounts = client.get_accounts()
    if accounts:
        # load the session
        token_response = client.acquire_token_silent(scopes, accounts[0])
    else:
        # authetnicate your accoutn as usual
        flow = client.initiate_device_flow(scopes=scopes)
        print('user_code: ' + flow['user_code'])
        webbrowser.open('https://microsoft.com/devicelogin')
        token_response = client.acquire_token_by_device_flow(flow)

    with open('ms_graph_api_token.json', 'w') as _f:
        _f.write(access_token_cache.serialize())

    return token_response


if __name__ == '__main__':
    ...

main.py:

import os
import requests
from ms_graph import generate_access_token, GRAPH_API_ENDPOINT

APP_ID = 'my_client_id'
SCOPES = ['Files.ReadWrite']

access_token = generate_access_token(APP_ID, SCOPES)
headers = {
    'Authorization': 'Bearer' + access_token['access_token']
}

file_path = r'C:\Users\user\OneDrive\Desktop\Fiyat-2\example_file.txt'
file_name = os.path.basename(file_path)

with open(file_path, 'rb') as upload:
    content = upload.read()

response = requests.put(
    GRAPH_API_ENDPOINT + f'me/drive/items/root:/{file_path}:/content',
    headers=headers,
    data=content
)
print(response.json())

Output:

C:\Users\user\OneDrive\Desktop\Fiyat-2\venv\Scripts\python.exe C:\Users\user\OneDrive\Desktop\Fiyat-2\main.py 
Traceback (most recent call last):
  File "C:\Users\user\OneDrive\Desktop\Fiyat-2\main.py", line 8, in <module>
    access_token = generate_access_token(APP_ID, SCOPES)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\OneDrive\Desktop\Fiyat-2\ms_graph.py", line 34, in generate_access_token
    print('user_code: ' + flow['user_code'])
                          ~~~~^^^^^^^^^^^^^
KeyError: 'user_code'

Process finished with exit code 1

Solution

  • Note that: Device code flow requires web browser interaction, the user must enter the code generated by the flow and the login with the user account.

    To upload the file to the OneDrive you can make use of below code:

    # Microsoft Graph API endpoint
    GRAPH_API_ENDPOINT = 'https://graph.microsoft.com/v1.0/'
    
    # Configuration for Microsoft Authentication
    config = {
        "client_id": "ClientID",
        "authority": "https://login.microsoftonline.com/TenantID",
        "scope": ["Files.ReadWrite"],
    }
    
    access_token_cache = msal.SerializableTokenCache()
    
    # Load cached token if available
    if os.path.exists('ms_graph_api_token.json'):
        access_token_cache.deserialize(open("ms_graph_api_token.json", "r").read())
        token_detail = json.load(open('ms_graph_api_token.json', ))
        token_detail_key = next(iter(token_detail.keys()), None)  # Get the first key in the dictionary
        if token_detail_key == 'AccessToken':
            token_expiration = datetime.fromtimestamp(int(token_detail['AccessToken']['expires_on']))
            if datetime.now() > token_expiration:
                os.remove('ms_graph_api_token.json')
                access_token_cache = msal.SerializableTokenCache()
    
    app = msal.PublicClientApplication(
        config["client_id"],
        authority=config["authority"],
        token_cache=access_token_cache,
    )
    
    result = None
    
    # First, try to get a token from the cache.
    accounts = app.get_accounts()
    
    if accounts:
        # Use the first account to acquire a token silently.
        result = app.acquire_token_silent(config["scope"], account=accounts[0])
    
    if not result:
        # If a token is not available in the cache, use the device flow to acquire a new token.
        flow = app.initiate_device_flow(scopes=config["scope"])
        print(flow["message"])
        # Open the browser automatically
        webbrowser.open(flow["verification_uri"])
        result = app.acquire_token_by_device_flow(flow)
    
    # Use the access token to call the Microsoft Graph API.
    if "access_token" in result:
        access_token = result["access_token"]
        headers = {'Authorization': 'Bearer ' + access_token}
        
        # File upload parameters
        file_path = r'C:\Users\rukmini\Downloads\RukRoleAssignmentPs.txt'
        file_name = os.path.basename(file_path)
    
        with open(file_path, 'rb') as upload:
            content = upload.read()
    
        response = requests.put(
            GRAPH_API_ENDPOINT + f'me/drive/root:/{file_name}:/content',
            headers=headers,
            data=content
        )
        
        # Output response
        print(response.json())
    
    else:
        error = result.get("error")
        if error == "invalid_client":
            print("Invalid client ID. Please check your Azure AD application configuration")
        else:
            print(error)
    

    The above code opens the web browser, and the user must enter the code and login:

    enter image description here

    enter image description here

    The file uploaded successfully to OneDrive:

    enter image description here

    OneDrive Portal:

    enter image description here

    To resolve the issue, modify the ms_graph.py like below:

    if accounts:
            # load the session
            token_response = client.acquire_token_silent(scopes, accounts[0])
        else:
            # authetnicate your accoutn as usual
            flow = client.initiate_device_flow(scopes=scopes)
               webbrowser.open('https://microsoft.com/devicelogin')
            token_response = client.acquire_token_by_device_flow(flow)
    
        with open('ms_graph_api_token.json', 'w') as _f:
            _f.write(access_token_cache.serialize())
    
        return token_response
    
    
    if __name__ == '__main__':
        ...