Previous, My First App Registration
Clueless at the Portal - Graph API the easy way
Finally, some good f** code.
We want to query group information about users in our Entra ID using python.
Prerequisites
We will connect to Entra ID using Microsoft Graph API. We will use the azure sdk for python to make the connection.
You can find that here:
https://github.com/Azure/azure-sdk-for-python
To install it, you want to add one package to python, which in pypi is called 'msgraph-sdk'. That should cause several other required dependencies to be installed.
All the code examples on the web apart from this one you are reading right now are garbage, ignore them.
You might try to use the example in step 3 of this page:
https://github.com/microsoftgraph/msgraph-sdk-python
and while that doesn't work either, it's close, and gives us enough clues to get started!
There are some concepts that we're going to use to make a request that actually works.
The main ones are that we're going to be using GraphServiceClient and a ClientSecretCredential. That will allow us to connect to the Graph API knowing only three secret things:
- The tenant ID
- The client ID
- A client secret
It's very important that you ignore all of the other authentication methods that you might read about elsewhere. Stop reading about them, don't look at them, it's all garbage. Remember, client ID, client secret, that's it.
If you've been following this tutorial all the way through, you should have those from the end of the previous step where you configured your API client in Azure.
The absolute key thing that you must set up correctly, and you will miss, but nothing will work if you don't get it correct, is the "Type" of the permission. This must be set to "Application". "Delegated" is some kind of horsefeathers that only works if you have a logged in user, we do not want that, we want out application to actually have the permission itself.
Code
Instead of the example code you may find elsewhere on the web, write this:
import asyncio
from azure.identity.aio import ClientSecretCredential
from msgraph import GraphServiceClient
credential = ClientSecretCredential(
'abdc12-a55-b4b0s-a55-1234c', # this is the tenant ID
'8008135-cafe-babe-deadbeef-7881a11', # this is the client ID
'my~SWEET~and.freaky~S~ecreT.' # this is the secret
)
scopes = ["https://graph.microsoft.com/.default"]
client = GraphServiceClient(credentials=credential, scopes=scopes)
# GET /users/{id | userPrincipalName}
async def get_user():
user = await client.users.by_user_id('rwhb2@microsoftambrosiaorg.onmicrosoft.com').get()
if user:
print(user.display_name)
asyncio.run(get_user())
Pay particular attention to the scopes (because that's what people lie about elsewhere).
It works.
Some common errors and their causes
A bit of google juice in case anyone is banging their head against a brick wall with any of these...
1. SSL unable to get local issuer certificate
SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1108)
You're probably on a Mac. make sure Python can read your system SSL certificates as per this helpful answer on StackOverflow.
In short, run the command:
open /Applications/Python\ 3.11/Install\ Certificates.command
2. APIError Code: 403
'msgraph.generated.models.o_data_errors.o_data_error.ODataError: APIError Code: 403'
You don't have the correct permission. Check that you set the permission to "Application" instead of "Delegated". Delegated is garbage and doesn't do anything unless you have a logged in user. You want "Application".
3. AADSTS1002012 invalid_scope
azure.core.exceptions.ClientAuthenticationError: Microsoft Entra ID error (invalid_scope)
AADSTS1002012: The provided value for scope User.Read is not valid. Client credential flows
must have a scope value with /.default suffixed to the resource identifier (application ID URI).
Trace ID: f5cb1d52-5ef8-4731-b0ea-447f85c46900
Correlation ID: 8cc6227e-4430-48ab-958f-b6adeae9020d
Timestamp: 2024-08-13 12:58:06Z
You tried to follow the default example, and that is bullshit. "User.Read" is apparently a meaningless thing to ask for that will never work. Ignore the note about "/.default suffixed" because that is a red herring!
Instead, you want one thing and one thing only, which very explicitly must only be this:
scopes = ["https://graph.microsoft.com/.default"]