Practical Entra ID authentication
shiftavenue
Modern Infrastructure | Artificial Intelligence - Automate the future. Achieve more.
As cloud consultants focussing on Azure we all probably know those nagging questions about the difference between delegated and application permissions. To help you explain or understand them better, why not try a practical example?
At shiftavenue, all of us are big fans of personal knowledge management and open document formats like markdown. Some of you may already be familiar with Obsidian, for instance, which makes note-taking brilliant again. And as it is all based on markdown, all your notes can live in a source code repository.
The example we will be using is a conversion from OneNote pages in one or more notebooks to individual markdown documents, nested in a folder hierarchy resembling those notebooks. If you try looking for OneNote to Markdown online, you’ll mostly find either proprietary solutions or solutions that work with the OneNote desktop app, or manual exports which are then processed with pandoc. Yuck.
What we all want nowadays is of course the Graph API (https://learn.microsoft.com/en-us/graph/overview)! In this short article, you’ll get an overview of Graph and its usage, an overview of delegated and app credentials, and most importantly some code.
Graph API
The Graph API is a programming interface for everything Microsoft 365 and Entra ID (the artist formerly known as Azure Active Directory) with which all administrative tasks can be automated. Accessing the graph API is done through an app registration in Entra ID.
In our little example, we will now examine the preparations necessary in Entra ID and then continue with a little PowerShell script with which to programmatically access the API both with a personal Microsoft account as well as with an Entra ID account.
Preparations in Entra ID
To access the Graph API, we need a resource that describes the app and its required permissions. This will be an app registration which will later be visible as an enterprise app in other tenants. But more on that later.
To facilitate the export of both personal as well as corporate OneNote notebooks, the app needs to support multi-tenancy. If that isn't the case, all potential users of your application need to be members of your tenant - for example through invitation (B2C). This would exclude using the app to export a personal account’s OneNote though.
The app registration also needs to receive a list of required permissions. For this, and any other app you intend to develop yourself, the first stop in my humble opinion is usually the Graph API documentation. If we browse to the OneNote docs at https://learn.microsoft.com/en-us/graph/onenote-get-content, the required permissions are unfortunately well-hidden. You can search the list of all permissions at https://learn.microsoft.com/en-us/graph/permissions-reference to greater success. The fitting permission here is called "Notes.Read.All”. If you browsed to the permissions reference, you will also notice that there is a distinction between delegated and application permissions.
Delegated Permissions versus Application Permissions
Delegated permissions, as often described very well in the permissions reference, describe permissions in the context of the signed-in user. Those can be permissions on objects that the user already has access to like OneNote notebooks. Permissions with an Admin Consent required however will give a user access to all resources described for that API endpoint.
A permission called User.Read does not require Admin Consent for example, while User.Read.All most certainly does - it allows the signed-in user to read all users and their attributes. By narrowing down the access to the App Registration, the blast radius can be slightly decreased again.
Graph Authentication with MiniGraph
There are various ways of authenticating to the app registration or the enterprise app, if you are using the app registration of a third party. Here we are concentrating on end users accessing your app. This means either Browser or Device Code Authentication flows. If instead you plan on using the app like a service, say from within your CI workflow, federated credentials (OIDC), a client secret or a client certificate can be used.
The minimum information required for all methods are the tenant ID as well as the applications client ID. Using this information, a user can sign in and use the app if the delegated permissions are already approved. What - you might ask - is the tenant ID for personal accounts then? In these cases, the ID “Common” can be used!
Armed with the required information, we can use MiniGraph (https://github.com/FriedrichWeinmann/MiniGraph), developed by the brilliant Friedrich Weinmann . Check out his work on GitHub, in particular the modules PSFramework, PSModuleDevelopment, MiniGraph and ADMF.
Start the conversion
To start converting all OneNote notebooks to markdown using PowerShell, we start with the script parameters. All too often I see scripts without parameters, and instead variables with hardcoded values are sprinkled in everywhere. Fortunately, parameters can have default values and you can start cleaning up your code.
Fitting default values could be the TenantId, the User and the client ID. The below snippet uses default values for personal accounts, as this was my main use case.
Using requires-Statements we can ensure that both required modules are imported when running the script. I am using the module MarkdownPrince to convert OneNote’s HTML content to markdown - there often is no reason to reinvent the wheel.
Using the notebooks API, we first need to find all notebooks. The API supports the common filter parameter, we can query a list of notebooks as well. If in doubt, look at the docs at https://learn.microsoft.com/en-us/graph/overview!
A bit later in my code I noticed that for one particular API call I would need the bearer token. With this nifty little trick, we can extract it from the (imported and connected) module:
Keep this little snippet in mind! You'd be surprised how helpful it can be.
But back to our main topic. The following loops are what I consider PowerShell basics. For each notebook, get all sections. For each section, get all pages. Finally, for each page, get content and extract embeds. The sections and pages APIs will be used for this.
To extract embedded content, we need to quickly ramp up our PowerShell foundational skills. The content of each page is well-formed HTML, which is deserialized to an XmlDocument. This means we can use an XPath query to find all img tags, wherever they may be. Using the query //img we loop over all XML nodes and extract all <img> elements including their content and attributes. Neat!
After that, each image is downloaded, renamed and the src tag that MarkdownPrince uses to convert the HTML document to markdown is overwritten with the new destination. Easy!
Downloading the embedded resource is done using Invoke-RestMethod as the response is binary and it was a lot easier for me to extract the token than to use FileStreams to write the content. The best tool for the job at hand!
The full, working code can be found on GitHub: https://github.com/nyanhp/freeing-onenote
How do I know it is working? I wrote this article and developed the snippet in order to convert all my OneNote notebooks - successfully of course.
As you can see, automating Graph requests is not that hard. If I may leave you with some final tips:
And most importantly: Have fun!