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:
the
AzKvCreds.get_token()call ONLY seems to accept that string. The short form.defaultdoesn't work and putting in scopes likeUser.Reador anything else just tells me it's invalid and/or I need to login withaz login --scope https://graph.microsoft.com/User.Read/.defaultwhich 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.If I use the full
https://graph.microsoft.com/.defaultvalue then I get no errors until making theAzGraphClient.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 theOnBehalfOfCredential()call then I get amalformed JWTerror. 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.