Python Azure Graph API request for Delegated request needing Admin consent

83 Views Asked by At

So, I'm trying to chain together a basic CLI Python app that makes a Azure Graph API call using the az CLI "credentials" from my environment and then getting a "OnBehalfOf" credential object where that App Registration has the Admin consent permissions needed to call the SecurityIncident.Read.All and SecurityAlert.Read.All scopes.

My code is broken up into functions, etc..., but the meat and potatoes is:

import azure.identity as AZID
import azure.keyvault.secrets as AZKS
import msgraph as AZGR

    #tenant ID, app registration id and SecretId
    TenantId = 'XXXXX'
    AppRegId = 'XXXXX'
    AppRegSecretId = 'XXXXX'

    #Graph Scopes
    GraphScopeDict = ['SecurityAlert.Read.All', 'SecurityIncident.Read.All']

    #KV info
    AzKvUrl = 'https://KVURL'
    AzKvSecretName = 'KVSECRETNAME'

    #az CLI creds to pull the App Registration secret from keyvault
    AzKvCreds = AZID.DefaultAzureCredential()
    AzKvCredsToken = AzKvCreds.get_token('https://graph.microsoft.com/.default')
    
    #pull the secret value using our azure CLI creds
    AzKvSecretClient = AZKS.SecretClient(vault_url=AzKvUrl, credential=AzKvCreds)
    AppRegSecretValue = (AzKvSecretClient.get_secret(AzKvSecretName)).value
    
    #generate the on-behalf-of creds to use the App Registration to make the request to the Graph API as a delegated request
    AzAppRegCreds = AZID.OnBehalfOfCredential(tenant_id=TenantId, client_id=AppRegSecretId, client_secret=AppRegSecretValue, user_assertion=AzKvCredsToken)

    #graph client
    AzGraphClient = AZGR.GraphServiceClient(AzAppRegCreds, GraphScopeDict)
    

    #get incidents
    result = asyncio.run(AzGraphClient.security.incidents.get())

I've used the DefaultAzureCredential before to make calls to Azure services so I know that works. It works fine for pulling the KV secret for the App Registration client secret from Azure.

If I change the get_token() and the GraphScopeDict to be https://graph.microsoft.com/.default then I can call result = asyncio.run(AzGraphClient.users.by_user_id('UPNHERE').get()) and it works. However, any other value in either of those places and it tells me I need to login with az login --scope https://graph.microsoft.com/User.Read/.default , for example.

The problem is that:

  1. the AzKvCreds.get_token() call ONLY seems to accept that string. The short form .default doesn't work and putting in scopes like User.Read or anything else just tells me it's invalid and/or I need to login with az login --scope https://graph.microsoft.com/User.Read/.default which doesn't work. I get errors. So this URL (https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc) seems to be wrong or misleading on how you define scopes.

  2. If I use the full https://graph.microsoft.com/.default value then I get no errors until making the AzGraphClient.security.incidents.get() call where it tells me that:"error":"invalid_grant","error_description":"AADSTS500061: Assertion failed signature validation. If I have a different value for the OnBehalfOfCredential() call then I get a malformed JWT error. So, the token is at least a valid format, but I'm stuck as to what the issue is.

Do I need to "scope" the initial token? How? That would seemingly be annoying to have to login with az CLI --scope for every different "command" I'm trying to run. I would assume that the scope sent to the GraphClient object would take care of that and that the initial token is just "authing" me to the App Registration. Do those 2 scopes need to match? Do I need to drop down to MSAL or raw HTTP to go outside the "work flow" of the SDKs? I'd rather not.

I do NOT want to do a "application" consent level of permissions. This has to flow through user delegation flow in order to scope the permissions correctly to what the user can see. So, ClientSecretCredential() is out as an option.

0

There are 0 best solutions below