Search code examples
pythonemailmicrosoft-graph-apioffice365

Is there a way to grant my app a permission only to access own e-mail account in Office 365?


I'm trying to create a Python Script to download e-mail attacments from Microsoft Office 365 using Microsoft Graph API. I'm using the script from Youtube tutorial (https://www.youtube.com/watch?v=E2brh3wuRPA) I'm getting 'Access is denied. Check credentials and try again.' when i try to call the API. When I replace the token generated in the script with a token generated with Graph Explorer it works fine. I supose that I´m getting the error because I would need a consent from Admin to use Mail.Read Permission. But if I ask the Admin for this permission I woud have access to all e-mail boxes and this is not what I want/need. Is there a way to grant my script permission only for my own account?

import msal
import json
import requests

#  Credenciais
clienteid = '*******************'
tenantid = '*******************'
secret = '**********************'

# Geração do Token
def get_access_token():
    tenantID = tenantid
    authority = 'https://login.microsoftonline.com/' + tenantID
    clientID = clienteid
    clientSecret = secret
    scope = ['https://graph.microsoft.com/.default']
    app = msal.ConfidentialClientApplication(client_id=clientID, authority=authority, client_credential=clientSecret)
    access_token = app.acquire_token_for_client(scopes=scope)
    return access_token

access_token = get_access_token()
token = access_token['access_token']
#token = "Token Generated with Graph Explorer"

date_received = '20**-**-**'
mail_subject = '******'
mail_sender = '***@***.***.br'
mail_user = '***.***@***.***.br'

# API URL
url = f"https://graph.microsoft.com/v1.0/users/{mail_user}/messages?$filter=startswith(from/emailAddress/address, '{mail_sender}') and subject eq '{mail_subject}' and receivedDateTime ge {date_received}"

headers = {
    "Authorization": f"Bearer {token}",
    "ContentType": "application/json"
}

# Envia Request
response = requests.get(url, headers=headers)
data = json.loads(response.text)
print(data)

# Pega Attachment
def get_email_attachment(message_id):
    url = f"https://graph.microsoft.com/v1.0/users/{mail_user}/messages/{message_id}/attachments"
    response = requests.get(url, headers=headers)
    return response.json()

for d in data["value"]:
    # print (d['id'])
    mes_id = d['id']

    attachments = get_email_attachment(mes_id)
    for attachment in attachments['value']:
        attachment_name = attachment['name']
        attachment_id = attachment['id']
        print(attachment_name)

        download_attachment_endpoint = f"https://graph.microsoft.com/v1.0/users/{mail_user}/messages/{mes_id}/attachments/{attachment_id}"
        headers_ = {
            "Authorization": f"Bearer {token}",
            "ContentType": "application/octet-stream"
        }

        response = requests.get(download_attachment_endpoint, headers=headers_)
        response.raise_for_status()

        # Salva Arquivo
        with open(attachment_name, 'wb') as f:
            f.write(response.content)

I tried using the token generated by Graph explorer and worked fine, but this is not permanent.


Solution

  • Your code is attempting to access Microsoft Graph as the app itself: direct access. Microsoft Graph Explorer, accesses Microsoft Graph on behalf of the signed-in user: delegated access.

    First, you should choose whether it makes more sense for your script to access Graph as the signed-in user (e.g. a user accessing their own mailbox), or as an unattended automation script. In both cases, you can limit your script to only have access to one (or a small set) of mailboxes.

    Access as the app (direct access)

    The first step is to use an Exchange role assignment to grant your app's service principal access only for certain mailboxes. You can use Exchange PowerShell or using Microsoft Graph PowerShell. Here's an example using Microsoft Graph PowerShell:

    ##
    ## Grant an Exchange role to a service principal at the scope of a single user
    ##
    
    Connect-MgGraph -Scopes "RoleManagement.ReadWrite.Exchange"
    
    $exchangeRoleName = "Application Mail.Read" # The Exchange role to assign
    $clientAppId = "{your-app-id}" # The client app's appId
    $targetUserPrincipalName = "{target-mailbox-upn}" # The target mailbox
    
    # Retrieve role definition, client app's service principal, and target user objects
    $roleDefinition = Invoke-MgGraphRequest -OutputType PSObject `
        -Method GET `
        -Uri "/beta/roleManagement/exchange/roleDefinitions?`$filter=displayName eq '$($exchangeRoleName)'" `
        | select -ExpandProperty value
    $clientServicePrincipal = Get-MgServicePrincipal -Filter "appId eq '$($clientAppId)'"
    $targetUser = Get-MgUser -UserId $targetUserPrincipalName
    
    # Create Exchange role assignment scoped to one user's mailbox
    Invoke-MgGraphRequest -OutputType PSObject `
        -Method POST `
        -Uri "/beta/roleManagement/exchange/roleAssignments"    
        -Body @{
            "roleDefinitionId" = $roleDefinition.id
            "principalId" = "/ServicePrincipals/$($clientServicePrincipal.Id)"
            "directoryScopeId" = "/Users/$($targetUser.Id)"
        }
    

    (Note: Unfortunately, the cmdlets Get-MgBetaRoleManagementExchangeRoleDefinitions and New-MgBetaRoleManagementExchangeRoleAssignment haven't been released yet, so we have to resort to using Invoke-MgGraphRequest a couple times.)

    That's it! There's no need to grant the app any of the tenant-wide Microsoft Graph application permissions (app roles) like Mail.Read, which would allow your app to access all mailboxes. Your script from your question should work now. (Though you may need to wait up to 2 hours for the new authorization to be effective.)

    Access as a signed-in user (delegated access)

    To access the mailbox on behalf of the signed-in user, your app needs to use something like from the docs or in this sample to acquire a token interactively for the user. (After the initial sign-in an token acquisition, the app can use a refresh token to maintain access without needing the user tp be present.)

    Here's a very basic example:

    import msal
    import json
    import requests
    
    # You app's app ID. Make sure this app has "http://localhost" registered as a public client 
    # redirect URL (in the portal, this is the "Mobile and desktop applications" platform type)
    client_id = '{your-app-id}'
    
    # The target mailbox
    mail_user = '{target-mailbox-upn}' 
    
    # Sign in and obtain a token to Graph, on behalf of the signed-in user
    app = msal.PublicClientApplication(client_id=client_id)
    auth_result = app.acquire_token_interactive(scopes=['Mail.Read'])
    
    # Use the token to retrieve 10 message from the mailbox
    headers = {
        "Authorization": f"Bearer {auth_result['access_token']}",
        "ContentType": "application/json"
    }
    url = f"https://graph.microsoft.com/v1.0/users/{mail_user}/messages?$top=10"
    response = requests.get(url, headers=headers)
    data = json.loads(response.text)
    
    # Print some details about each message
    for d in data["value"]:
        print (f"{d['subject']} received {d['receivedDateTime']}")
    

    On initial sign-in, this will attempt to prompt the user for consent to access their mailbox. Depending on your organization's settings, you might be able to grant consent yourself, or your admin might need to do it.