dmmfll
dmmfll

Reputation: 2836

msal ConfidentialClientApplication Sucessfully Obtains Bearer Token; Authorization_RequestDenied Despite Full Privileges Granted

Thank you in advance for your help.

I am trying to access email messages for a specific email account for an organization that uses Microsoft Graph.

The Python app I am creating needs to read and forward email messages that are in this specific email account at "[email protected]". (I previously successfully read the emails using the Python library exchangelib, but now Basic Auth is no longer supported by Microsoft; exchangelib does not support Microsoft Graph).

The organization has registered an application "Email Service App" with the Microsoft identity platform. It appears that full permissions have been granted. See the screenshot the organization sent to me below. (I do not have access to the Graph Dashboard.)

Screenshot of Service Email App API Permissions

I have been following the excellent example provided in the GitHub repository for the msal library: https://github.com/AzureAD/microsoft-authentication-library-for-python/blob/dev/sample/confidential_client_secret_sample.py

The code I have written does successfully obtain a bearer token.

My expectation is that with that bearer token I could read emails for a specific email account: [email protected]

The error from the code is simple: "Insufficient privileges to complete the operation."

I am nonetheless confused because it does appear that full permissions have been granted. (See above screenshot.)

Here is the code I am using:

config_data = {
    "authority": "https://login.microsoftonline.com/<secret value>",
    "client_id": "<secret value>",
    "scope": ["https://graph.microsoft.com/.default"],
    "secret": "<secret value>",
    "endpoint": "https://graph.microsoft.com/v1.0/users"
}
# Optional logging
logging.basicConfig(level=logging.DEBUG)  # Enable DEBUG log for entire script
logging.getLogger("msal").setLevel(logging.INFO)  # Optionally disable MSAL DEBUG logs

config = json.loads(config_data)

# Create a preferably long-lived app instance which maintains a token cache.
app = msal.ConfidentialClientApplication(
    config["client_id"], authority=config["authority"],
    client_credential=config["secret"],
)

LOG OUTPUT:

DEBUG:urllib3.util.retry:Converted retries value: 1 -> Retry(total=1, connect=None, read=None, redirect=None, status=None)
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): login.microsoftonline.com:443
DEBUG:urllib3.connectionpool:https://login.microsoftonline.com:443 "GET /<secret>/v2.0/.well-known/openid-configuration HTTP/1.1" 200 1753

Code continues:

# The pattern to acquire a token looks like this.
result = None

# Firstly, looks up a token from cache
# Since we are looking for token for the current app, NOT for an end user,
# notice we give account parameter as None.
result = app.acquire_token_silent(config["scope"], account=None)

LOG OUTPUT:

INFO:root:No suitable token exists in cache. Let's get a new one from AAD.
DEBUG:urllib3.connectionpool:https://login.microsoftonline.com:443 "POST /<secret>/oauth2/v2.0/token HTTP/1.1" 200 1589

Code continues:

if not result:
    logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
    result = app.acquire_token_for_client(scopes=config["scope"])

LOG OUTPUT:

INFO:root:No suitable token exists in cache. Let's get a new one from AAD.
DEBUG:urllib3.connectionpool:https://login.microsoftonline.com:443 "POST /<secret>/oauth2/v2.0/token HTTP/1.1" 200 1589

Code continues:

print(result)

OUTPUT:

{'token_type': 'Bearer',
 'expires_in': 3599,
 'ext_expires_in': 3599,
 'access_token': <removed, was successful>}

Code continues:

if "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

LOG OUTPUT:

DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): graph.microsoft.com:443
DEBUG:urllib3.connectionpool:https://graph.microsoft.com:443 "GET /v1.0/users HTTP/1.1" 403 None

Graph API call result: {
  "error": {
    "code": "Authorization_RequestDenied",
    "message": "Insufficient privileges to complete the operation.",
    "innerError": {
      "date": "2022-11-06T13:07:13",
      "request-id": "13a2593f-e9c9-4844-aca5-7c362a7f83b8",
      "client-request-id": "13a2593f-e9c9-4844-aca5-7c362a7f83b8"
    }
  }
}

Is the issue with the code I am using or are there some other settings in Microsoft Graph that need to be set by the owning organization? Or is it another issue?

Thank you again for your help.

Upvotes: 0

Views: 1365

Answers (1)

Glen Scales
Glen Scales

Reputation: 22032

The problem is your trying to use the client credentials flow but all your permission are delegate permissions which aren't valid for that flow. You need to assign App permission and consent to them eg something like

enter image description here

By default this will give you access to every mailbox in the tenant as you mentioned you only want to access one mailbox you can then scope the permission down https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access

Upvotes: 3

Related Questions