Microsoft Foundry – Publishing Agent to Microsoft Teams – Part 3 – The Programmatic Experience

Microsoft Foundry – Publishing Agent to Microsoft Teams – Part 3 – The Programmatic Experience

This is part of my series on Microsoft Foundry:

  1. Microsoft Foundry’s Evolution
  2. Microsoft Foundry BYO AI Gateway (BYO Model) – Part 1
  3. Microsoft Foundry BYO AI Gateway (BYO Model) – Part 2
  4. Microsoft Foundry BYO AI Gateway (BYO Model) – Part 3
  5. Microsoft Foundry Publishing Agent to Microsoft Teams – Part 1 – Overview
  6. Microsoft Foundry Publishing Agent to Microsoft Teams – Part 2 – The GUI Experience
  7. Microsoft Foundry Publishing Agent to Microsoft Teams – Part 3 – The programmatic experience

And I’m back again with the third post in my series on publishing agents built in the Microsoft Foundry Agent Service to Microsoft Teams. I started the series by walking through the benefits of publishing a Foundry agent to Teams and explained the purpose Bot Services serves today in the flow. In the second post I walked through the GUI experience, the workflow occurring under the hood, and why the GUI experiencing publishing flow won’t work for most customers. For this third post we’re gonna jump down into the weeds and dissect the programmatic approach using raw calls to the Azure REST API.

Let’s get to it!

What used to be required?

Back when I published this initial series way back in the olden days of May 2026, publishing a Foundry agent to Microsoft Teams when the Foundry account was locked down was quite complex. My very smart peer Graeme Foster put together a stellar blog post on the topic. I’m not going to go into deep detail on the old flow because you can grab those details from Graeme’s original post. Instead, we’re going to hit the key challenges of the old method.

As I discussed in my last post, most large enterprises have requirements around controlling and governing inbound traffic to their Azure resources. For resources deployed to Microsoft Foundry, like an model deployment or an agent, that control is exercised through the service firewall native to most all Microsoft PaaS services. The typical configuring is to disable inbound public network access and restrict access through a Private Endpoint. Restricting to a Private Endpoints means the traffic needs to be routed through the customer’s virtual network. With Microsoft Teams traffic, this is not possible today and that traffic can only reach an endpoint with a public IP.

Foundry with inbound public network access denies Teams traffic

We got a problem here right? Teams cannot reach the Private Endpoint in the customer virtual network. The solution around this problem has historically been to funnel that traffic in through a firewall, reverse proxy like Application Gateway, or a publicly exposed API Gateway like API Management by modifying the activity endpoint on the Bot Service resource. Which ingress solution you picked depended on your risk tolerance. It’s unlikely your security team is willing to allow all IP addresses through the firewall or reverse proxy, so you’d probably filter with a firewall rule or WAF (web application firewall) rule. Teams is a multi-tenant service, so you may want to evaluate the headers in the incoming request to check if the incoming header matches your Entra ID tenant ID. While those controls are nice, the real thing you very much want to do is validate the JWT (JSON web token) being passed in the request is from a Bot Service in your Entra ID tenant. Like I mentioned in my last post, Bot Service uses its own STS which uses a common signing certificate for the service holistically (from what I’ve seen) so validating the claims within the JWT is a must as Graeme covers in his post.

Below is an example of an authorization header generated by Bot Services and the x-ms-tenant-id header that would be validated by the customer in the old method.

[
{
"TraceRecords": {
"Authorization": {
"header": {
"alg": "RS256",
"kid": "PNDitLKaGJW-60l42Kz7-4RqwWM",
"x5t": "PNDitLKaGJW-60l42Kz7-4RqwWM",
"typ": "JWT"
},
"payload": {
"serviceurl": "https://smba.trafficmanager.net/amer/6c80de31-d5e4-4029-93e4-XXXXXXXXXXX/",
"nbf": 1788568132,
"exp": 1788571732,
"iss": "https://api.botframework.com",
"aud": "be377583-32aa-4263-b657-7ec426f9f6fc"
}
},
"X-Forwarded-For": "52.112.116.181:46080;10.0.8.4",
"x-ms-tenant-id": "6c80de31-d5e4-4029-93e4-XXXXXXXXXXXX",
}
]

This inevitably led to some variant of the complex architecture seen below for most complex and regulated enterprises. It was a lot of complexity, extra infrastructure, potential bottlenecks, and extra latency. Nothing good.

In addition to this inbound flow, you also needed to account for the outbound flow from the agent back to the Bot Service. As I described in my first post in this series, the reply message to the user is a separate TCP call initiated by the agent to the Bot Services. That outbound flow required you had had a firewall rule allowing traffic to smba.trafficmanager.net.

The flow in the olden days of May 2026

Thankfully, the Product Group felt for us poor central IT folks and have now made this whole flow drastically easier.

What is required now?

Gone is the overly complex inbound flow and the required outbound flow to smba.trafficmanager.net (at least I don’t see it anymore in my firewall logs). So how is this accomplished? The public documentation gives some good detail on exactly what has changed. In my simple brain terms, Microsoft has created an alternative path to the activity endpoint (other Foundry and agent endpoints are not available through this path) of the Foundry agent that Microsoft Teams can route to. Instead of the customer having to do IP filtering, Microsoft performs it. Instead of the customer having to crack open the JWT issued by the Bot Service, Microsoft does it. This new configuration shifts responsibility for this security controls from the customer to Microsoft. Not too shabby right?

The new flow!

This is accomplished through a new property of the agent called enable_m365_public_endpoint which must be set to true.

Alright, so you’re celebrating a less painful flow now. Let’s bounce over to how we’d enable this programmatically.

Programmatically publishing an agent

The Product Group has done a great job creating a detailed write-up of the necessary Azure ARM REST API calls required to publish an agent programmatically. I’ll be walking through this using a Jupyter Notebook and some simple Python.

Before we can begin publishing we need to deploy an instance of Microsoft Foundry where inbound public network access is restricted and outbound traffic of the agent is controlled (using either VNet integration or managed VNet). The product group has published samples for both these scenarios in this the official samples repository. If you want to make your eyes bleed reading awful Terraform code, I have some samples in my personal repo. You’ll then need to deploy a prompt or hosted agent to Microsoft Foundry. The public documentation has a ton of examples of this, so I’m going to assume you’re successful in making that happen.

Once you have the environment setup up and agent deployed you can begin the publishing process. As a reminder, here is the general workflow to publish as it stands today.

Foundry agent publishing workflow

The first step in the publishing process (at least for today, this is likely going away in the future) you need to create a Bot Service. As discussed in my first post, the Bot Service will service as the intermediary between Microsoft Teams and the activity endpoint in the agent. Before we can create the Bot Service we need to get the agent’s Entra ID Agent Identity Blueprint’s principal id. We can do this like seen below:

import os
import json
import requests
from dotenv import load_dotenv
# Load environmental variables
load_dotenv(override=True)
# Function that gets the agent object
def get_foundry_agent(account_name: str, project_name: str, agent_name: str, token: str):
"""This function retrieves a Foundry agent by name from a Foundry project
Args:
account_name (str): The name of the Foundry account
project_name (str): The name of the Foundry project
agent_name (str): The name of the Foundry agent to retrieve
token (str): The authentication token to use for the API request
Returns:
dict: The Foundry agent details if found, otherwise None
"""
response = requests.get(
f"https://{account_name}.services.ai.azure.com/api/projects/{project_name}/agents/{agent_name}?api-version=v1",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
)
if response.status_code == 200:
return response.json()
else:
logging.error(f"Failed to retrieve agent: {response.status_code} - {response.text}")
return None
# Grab the principal_id of the Entra ID Agent Identity associated with the Foundry Agent
foundry_account_name = os.getenv("FOUNDRY_ACCOUNT_NAME")
project_name = os.getenv("FOUNDRY_PROJECT_NAME")
agent_name = "hosted-azure-expert-agent"
agent = get_foundry_agent(foundry_account_name, project_name, agent_name, user_token.token)
agent_principal_id = agent.get("instance_identity", {}).get("principal_id")
print(f"Foundry Agent Principal ID: {agent_principal_id}")
print(json.dumps(agent, indent=2))

In addition to the principal id of the blueprint, we’ll need the Entra ID tenant ID, and the activity endpoint. The activity endpoint will look something like: https://FOUNDRY_ACCOUNT_NAME.services.ai.azure.com/api/projects/PROJECT_NAME/agents/AGENT_NAME/endpoint/protocols/activityProtocol?api-version=2025-05-15-preview. Once you have those inputs you can create the Bot Service resource. I have a Terraform sample of how to structure the resource located in this repository.

Once the Bot Service resource is created you’re ready for the next step which is going to be enabling the activity endpoint on the agent, setting the authorization scheme for the activity endpoint (which in this case I set to BotServiceTenant since I’ll be publishing for my whole organization), and I set the magical property of enable_m365_public_endpoint to true.

import os
import json
import requests
from dotenv import load_dotenv
# Load environmental variables
load_dotenv(override=True)
# Function that enables the activity protocol for the agent and configures the required Bot Service authorization scheme
def enable_agent_activity_protocol(account_name: str, project_name: str, agent_name: str, token: str):
"""This function enables the activity protocol for a Foundry agent and configures the required Bot Service authorization scheme
Args:
account_name (str): The name of the Foundry account
project_name (str): The name of the Foundry project
agent_name (str): The name of the Foundry agent to retrieve
token (str): The authentication token to use for the API request
Returns:
dict: The updated Foundry agent details if the update was successful, otherwise None
"""
#
body = {
"agent_endpoint": {
"protocol_configuration": {
"responses": {},
"activity": {
"enable_m365_public_endpoint": True
}
},
"authorization_schemes": [
{
# Entra authentication for responses endpoint
"type": "Entra",
},
{
# Allow all users in the Entra ID tenant to call the agent via Teams
"type": "BotServiceTenant"
}
]
}
}
response = requests.patch(
f"https://{account_name}.services.ai.azure.com/api/projects/{project_name}/agents/{agent_name}",
params={"api-version": "2025-11-15-preview"},
headers={
"Content-Type": "application/merge-patch+json",
"Authorization": f"Bearer {token}"
},
json=body
)
if response.status_code == 200:
return response.json()
else:
logging.error(f"Failed to enable agent activity protocol: {response.status_code} - {response.text}")
return None
# Grab the principal_id of the Entra ID Agent Identity associated with the Foundry Agent
foundry_account_name = os.getenv("FOUNDRY_ACCOUNT_NAME")
project_name = os.getenv("FOUNDRY_PROJECT_NAME")
agent_name = "hosted-azure-expert-agent"
enabled_agent = enable_agent_activity_protocol(foundry_account_name, project_name, agent_name, user_token.token)
enabled_agent_guid = enabled_agent.get('versions', {}).get("latest", {}).get("agent_guid", {})
print(f"Enabled Agent GUID: {enabled_agent_guid}")
updated_agent_endpoint = enabled_agent.get('agent_endpoint', {})
print(f"Updated Agent Endpoint: {json.dumps(updated_agent_endpoint, indent=2)}")

Next, I’m ready to publish the agent to Teams. For this I’m going to use the /microsoft365/publish endpoint of my agent resource in the Foundry API. Just like in the last post, I want to populate some information for the Agent 365 Agent Registry entry and the Team Store application.

import os
import json
import requests
from dotenv import load_dotenv
# Load environmental variables
load_dotenv(override=True)
def publish_agent_teams(
# Foundry stuff
agent_name: str,
project_name: str,
account_name: str,
# Bot Service stuff
bot_resource_id: str,
# Teams stuff
agent_display_name: str,
app_version: str,
publish_scope: str,
publish_as_autopilot: bool,
short_description: str,
full_description: str,
developer_name: str,
developer_website_url: str,
privacy_url: str,
terms_of_use_url: str,
token: str
):
"""This function uses the Foundry API to publish a Foundry agent to Microsoft Teams
Args:
agent_name (str): The name of the Foundry agent to publish
project_name (str): The name of the Foundry project
account_name (str): The name of the Foundry account
bot_resource_id (str): The resource ID of the Bot registered in Entra ID for this agent
agent_display_name (str): The display name of the agent to show in Teams
app_version (str): The version of the Teams app to publish
publish_scope (str): The scope to publish the Teams app to, either "Shared" (available to you) or "Tenant (admin must approve)"
publish_as_autopilot (bool): Whether to publish the agent as an Autopilot in Teams
short_description (str): A short description of the agent to display in Teams
full_description (str): A full description of the agent to display in Teams
developer_name (str): The name of the developer or organization that created the agent, to display in Teams
developer_website_url (str): The URL for the developer's website, to display in Teams
privacy_url (str): The URL for the privacy policy for this agent, to display in Teams
terms_of_use_url (str): The URL for the terms of use for this agent, to display in Teams
token (str): The Entra ID access token with the scope of https://ai.azure.com/.default to authenticate the API request
Returns:
dict: The response from the Foundry API if the publish was successful, otherwise None
"""
body = {
"agentDisplayName": agent_display_name,
"botServiceArmId": bot_resource_id,
"publishScope": publish_scope,
"publishAsAutopilot": publish_as_autopilot,
"appVersion": app_version,
"developerName": developer_name,
"developerWebsiteUrl": developer_website_url,
"fullDescription": full_description,
"privacyUrl": privacy_url,
"shortDescription": short_description,
"termsOfUseUrl": terms_of_use_url
}
response = requests.post(
url = f"https://{account_name}.services.ai.azure.com/api/projects/{project_name}/agents/{agent_name}/microsoft365/publish",
params = {
"api-version": "2025-11-15-preview"
},
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {token}",
},
json=body
)
if response.status_code == 200:
print("Agent published successfully! Status code: 200")
else:
logging.error(f"Failed to publish agent: {response.status_code} - {response.text}")
return None
AGENT_DISPLAY_NAME = "FHA - Azure Expert - Teams"
APP_VERSION = "1.0.1"
PUBLISH_SCOPE = "Tenant"
PUBLISH_AS_AUTOPILOT = False
SHORT_DESCRIPTION = "FHA - Azure Expert"
FULL_DESCRIPTION = "FHA - Azure Expert published to Teams from Microsoft Foundry"
DEVELOPER_NAME = "Carl Carlson"
DEVELOPER_WEBSITE_URL = "https://www.example.com"
PRIVACY_URL = "https://www.example.com/privacy"
TERMS_OF_USE_URL = "https://www.example.com/terms"
publish_response = publish_agent_teams(
# Foundry stuff
agent_name = os.getenv("FOUNDRY_AGENT_NAME"),
project_name = os.getenv("FOUNDRY_PROJECT_NAME"),
account_name = os.getenv("FOUNDRY_ACCOUNT_NAME"),
# Bot Service stuff
bot_resource_id = os.getenv("BOT_RESOURCE_ID"),
# Teams stuff
agent_display_name = AGENT_DISPLAY_NAME,
app_version = APP_VERSION,
publish_scope = PUBLISH_SCOPE,
publish_as_autopilot = PUBLISH_AS_AUTOPILOT,
short_description = SHORT_DESCRIPTION,
full_description = FULL_DESCRIPTION,
developer_name = DEVELOPER_NAME,
developer_website_url = DEVELOPER_WEBSITE_URL,
privacy_url = PRIVACY_URL,
terms_of_use_url = TERMS_OF_USE_URL,
token = user_token.token
)

Once publishing is complete, I’ll then need to approve it using the Microsoft 365 Admin Portal as seen in the last post. Poking through the Microsoft Graph APIs, I couldn’t find a specific API endpoint for the Agent Registry to programmatically approve and push it to the organization’s Teams store. The APIs for Agent 365 in general are not great right now. Vasil Michev did a great blog post on the topic. Unfortunately, the state of the APIs are much better months after his post. You can poke around the Agent Registry API which is really badly named the Copilot Package Management API if you want to poke around programmatically. If you can get the https://graph.microsoft.com/beta/copilot/agentRegistrations/{agent_registration_id} endpoint working without it throwing an unauthorized response, please drop a post in the comment and tell me how you did it. You will be my hero.

I’ve put a sample notebook here if you want to muck around with the programmatic publishing process.

Summing it up

So yeah, this new publishing process is just a TAD less painful for us poor central IT folks. It will likely get even easier in the very near future so keep an eye out on the public documentation for updates. Maybe that pesky Bot Service resource gets removed? 😉

So key takeaways:

  1. For most organizations you’ll need to do the programmatic publishing process because it’s unlikely you’re cool with public inbound network access.
  2. The new publishing process removing the complex architecture that used to be required to make this work.
  3. Use the new enable_m365_public_endpoint property when you publish if you value your sanity.
  4. Keep an eye on the Agent 365 API documentation. If the APIs are documented somewhere that I don’t know about, educate me. You’ll be my best buddy!
  5. Keep a close eye on the public documentation for upcoming changes that will further simplify this process.

Network Security Perimeters – Part 6 – NSP Perimeter Links

Network Security Perimeters – Part 6 – NSP Perimeter Links

This is part of my series on Network Security Perimeters:

  1. Network Security Perimeters – Part 1 – The Problem They Solve
  2. Network Security Perimeters – Part 2- NSP Components
  3. Network Security Perimeters – Part 3- NSPs in Action – Key Vault Example
  4. Network Security Perimeters – Part 4 – NSPs in Action – AI Workload Example
  5. Network Security Perimeters – Part 5 – NSPs for Troubleshooting
  6. Network Security Perimeters – Part 6 – NSP Perimeter Links

Hello once again fellow geek. In this post I will be continuing my series of NSPs (Network Security Perimeters). Over the past few posts in this series I’ve explained the problem NSPs solve, the components that make up an NSP, demonstrated NSPs through a Key Vault use case and an AI workload use case, and even shown how they can used to troubleshoot the dreaded Private Endpoints and forward web proxy problem we all run into. After reading those posts you are probably wondering why you aren’t seeing them more talked about in the Azure world. Besides the fact it’s not called “Frontier Network Agentic Security AI Perimeter”, the reason NSPs haven’t been getting the love they should is due to a technical gap that is finally in the process of being lifted.

The Challenge

As I’ve covered throughout this series, NSPs exist to control inbound and outbound traffic through the public IP of a supported PaaS service. They have nothing to do nor do they care about (although they will log) inbound traffic through a Private Endpoint. They seek to standardize and modernize the old school PaaS service firewall that (depending on the service) supported limited IP whitelisting, service whitelisting, and resource whitelisting. Where NSPs really shine is egress control. The challenge with that shine is they shine a bit too bright in that space such that when you enforce NSPs it blocks all outbound traffic, including diagnostic log traffic being delivered to a Log Analytics Workspace, Storage Account, or Event Hub.

NSPs block delivery of diagnostic logs

The only way to work around this limitation is to place the delivery destination (LAW, storage account, event hub) in the same NSP. While this sounds like a simple solution, a given resource can only belong to a single NSP. If you attempt to add a resource to an NSP when it’s already associated to another NSP it will fail as seen below.

Resources can only be associated to a single NSP

If you’ve ever done logging as scale in Azure, you already see the problem with this limitation. When logging at scale in Azure, it’s very common to send logs to a centralized Log Analytics Workspace, Event Hub, or Storage Account. There are a number of benefits to doing this beyond the scope of this post which you can read about in the CAF (Cloud Adoption Framework) documentation. These resource are typically managed by a security team or a platform team and live in subscriptions controlled by those teams.

If you’re like, your first instinct is to create an outbound rule to allow the access to the Log Analytics Workspace. Good instinct, but not possible because today NSPs only support outbound rules of type FQDN and there is no FQDN for platform diagnostic logs that Microsoft provide today. A service tag would be the best bet, but sadly it is not yet supported for NSP outbound rules.

Centralized logging problem with NSPs

Up until very recently, NSPs in enforcement mode have been pretty much a non-starter for enterprise customers due to this limitation. Notice I say “enforcement”. If you aren’t using NSPs in learning (or transition mode) you are losing out on insanely valuable egress visibility you can’t get using any other means as I describe in my troubleshooting post.

The PG (Product Group) for NSPs is one of those amazing product groups who listens to the customer, wants to see their product adopted, and works their butt off. Just a few weeks ago they introduced a new feature into public preview that begins to address this technical limitation

The Solution

The solution for this technical problem is perimeter links. In the ARM (Azure Resource Manager) API, these are referred to as links and are a child resource of the Network Security Perimeter. Perimeter links allow traffic to flow between NSPs either per profile or for all profiles. This means we can slap a perimeter link between the NSP containing our resources that need to deliver our logs and the NSP containing the resource we’re delivering the logs to!

Perimeter links can be across a single profile or all profiles within an NSP

In the image below I’ve created a perimeter link between an NSP containing a storage account the NSP containing the log analytics workspace the storage account diagnostic log setting has been configured to deliver logs to. This link enables the traffic to flow freely between the two NSPs.

Perimeter link example

Once the perimeter link is created, new inbound and outbound access rules are created in each profile allowing the two NSPs to communicate with each other.

In the example above, once the perimeter link is in place and rules have been automatically created and taken affect (takes about 10-15 seconds) the spice will flow! Alright, logs will flow but I’ve been re-reading Dune so leave me alone in my nerdy world. In the screenshot below you can see the logs being blocked initially and then being allowed after the perimeter and its automatically created rules take affect.

The spice will flow!

The good and gotchas of perimeter links

So first some of the good things about perimeter links:

  1. You can now do cross perimeter communication.
  2. In my testing, terraform state doesn’t get botched up when the perimeter link triggers the automatic creation of the access rule. A reapply after didn’t complain and didn’t overwrite them.
  3. Perimeter links work across subscriptions. Note there is a (as there should be) to make that work.

As I mentioned in the beginning of this post, perimeter links are in public preview as of the date of this post (I’ll remove this when they go GA) so you’ll need to enable the preview feature in your subscription to muck around with them. Perimeter links also only affect a specific subset of resources (fewer than are supported for NSPs all up).

The biggest pain point of them right now is the maximum amount of perimeter links per NSP is 10. Obviously, this is no where near the scale needed for the centralized logging use case. However, this is still preview and I’m confident the PG will increase that limit (hopefully a shit ton) prior to GA.

Well folks, let me leave you with a few recommendations:

  1. If you’re not using NSPs as a detective control for data exfiltration you are missing out. You need to make this a priority, if only for the visibility into egress.
  2. This is preview, so muck around with it, but don’t go rolling this to prod.

Have a great night!

Entra ID – Deep Dive – Protocol Primer – Part 2

This is part of my series on Microsoft Entra ID:

  1. Entra ID – Deep Dive – The Basics – Part 1
  2. Entra ID – Deep Dive – Protocol Primer – Part 2
  3. Entra ID – Deep Dive – Entra ID Authentication – Part 3
  4. Entra ID – Deep Dive – Workload Identity Federation – Bonus

Welcome back folks. Today I’ll be continuing my deep dive series into Entra. In my last post I went over the basics of Entra ID covering what it is at a high level and the important resources you’ll want to understand to grasp how human and non-humans identities are treated within the service. One of the features of Entra ID that I highlighted in that post was that it provides authentication services. It is capable of providing authentication of a human or non-human through older protocols like Kerberos (don’t get me started on this feature or else I’ll spend the whole post ranting) and LDAP (through Entra ID Domain Services, another service I’m no fan of), but also more modern protocols such as SAML and OIDC (OpenID Connect). Before I dive into Microsoft’s implementation of OIDC and the protocol it’s built on top of, OAuth, I figured it was a good time to do a light protocol primer (primarily for my own benefit because I can only re-read the RFC so many times before it stops being fun. Yeah I find it fun to read a good RFC, so what?).

What is OAuth?

You are probably thinking, “Why the hell are we talking about OAuth?” We need to talk about OAuth (Open Authorization) because OIDC is built on top of the OAuth protocol. If you have a basic understanding of OAuth, then OIDC makes a lot more sense. I’m not going to try to make you an expert, because to make you an expert I’d need to expert which I am very far from. Instead, I’m going to give you the basics. If you want a better/smarter explanation, start with the RFC(s) and then take a read through Vittorio Bertocci’s (an industry legend) many articles, ebook, and videos online.

There are a lot of misconceptions out there where folks will talk about OAuth authentication, which is not a real thing. OAuth itself exists as an authorization protocol to provide a framework (lots of SHOULDs/COULDs and not a ton of MUSTs in that RFC) for how applications can get limited access to a user’s data based around the user’s consent using delegation.

The protocol refers to this limited access as a scope. The assignment of a specific scope to an application gives you the ability to do delegation vs impersonation. In impersonation, the application will typically act as you with your full permission set vs with delegation you grant consent for the application to access a subset of your data with a more restricted set of permissions. A good example would be delegating the application the read permission over your email vs the reading, writing new emails, and deleting emails impersonation might give the application.

When it comes to the whole process of a user delegating a scope of access to an application, a number of different roles are involved. These roles include:

  • Client
  • Resource Owner
  • Authorization Server
  • Resource Server

Client

The client is an application that needs to access some data. Within the protocol it’s important to divide applications into a few different buckets, because the protocol supports them in different ways as I’ll cover in a bit. The standard breaks them into three buckets: web applications, browser-based applications, and native applications. I’m going to keep it simple and consolidate those three buckets into two which will be web applications and non-web applications.

Clients can be either public clients (non-web apps) or confidential clients (web apps). Confidential clients have some type of credential they use to authenticate themselves to the authorization server while public clients do not have a credential (because their code runs on the user’s machine so there is no way to secure the credential). Clients must register with the authorization server either through dynamic registration (which Entra ID does not support today) or through some other type of process. Registration, at a minimum, will include providing the authorization server with a redirectUri, which grant types it will use, and whether it’s a public or confidential client. The client is then issued a unique identifier called a client_id and optionally a client_secret if a confidential client. We’ll see an example of how Entra does it later on this series. Examples of clients could be applications you develop, third-party applications you integrate with, or Microsoft-native applications like Microsoft Teams.

Resource Owner

The resource owner is the user or organization that owns the data the client wants to access and is the entity that is capable of granting access to that data through a consent process. Consent is a major focus in OAuth since it relies on delegation of a specific scope of access to the data. Consent is the process of the resource owner approving that delegation. Resource owners in the Entra ID world are going to be the enterprise at the top layer, business units underneath that layer, and finally its employees which are represented by user objects in Entra. Consent will either be granted for all users within the tenant by an administrator or by individual users to data they have permissions over. In Entra, which consent is required is defined by the type property in the application resource’s permissionScope.

Authorization Server

The authorization server is the role that glues all other roles together. This is the server authenticates the user (OAuth doesn’t care how), gets the user’s consent for the client to access the data, and issues an access token to the client. Entra ID fulfills this role in the Microsoft cloud world.

Resource Server

The resource server hosts the resource owner’s data. It will consume the access token obtained by the client from the authorization server and allow or deny access to the data. Resource servers in the Microsoft world could be your custom built application or the Microsoft Graph API.

The RFC has a basic diagram which does a good job explaining the flow at a high level.

High level OAuth flow

You’ll notice the the term authorization grant in the above image. An authorization grant represents the resource owner’s authorization (delegation) of a specific scope of access to their data and is used by the client to get an access token which the resource server consumes and approves/denies access. In the base specification for OAuth 2.1, there are three types of grants (there are a ton of extension grants, some of which we’ll cover in this series) which include the authorization code grant, the refresh token grant, and the client credentials grant.

Before I describe the grant types, it’s worth calling out that I’m going to be talking specifically about OAuth 2.1 (which is still a draft RFC right now). OAuth 2.1 seeks to address a lot of the security issues with OAuth 2.0. In OAuth 2.0 there were a bunch more grant types including resource owner credentials flow and the implicit flow there are somewhat of security nightmares. OAuth 2.1 removes those grant types and the official spec sticks to the three I described above while adding an additional requirement for PKCE (Proof-Key for Code Exchange) for both public AND confidential clients. PKCE helps to address authorization code interception attacks. This Okta article does a great job describing the security benefit brings. I’ll demonstrate this with MSAL in a later post. Now back to the authorization grant types.

The authorization code grant type involves sending the resource owner to the authorization server to authenticate and consent to the client’s access of their data, returning an authorization code to the client, and the client exchanging that with the authorization server for an access token. This is going to be your go to grant type any delegation use case. An example of this would be an application accessing my data in a storage account that belongs to me.

Authorization Code Flow

Next up is the client credentials flow grant type. In this flow there is no user consent because the data the client is trying to access is under its control. Essentially, the client uses its own identity context to access the data because it’s already been authorized to do so. An example here would be an application pulling Entra ID sign-in logs from the MS Graph API.

Client Credentials Flow

Lastly, we have the refresh token grant. This grant type is used by the client to exchange a refresh token for a fresh access token. Access tokens must be short lived (typically around an hour). Instead of having the resource owner go through the whole authentication and consent process again, the client can exchange its longer living refresh token (if it requested one) for a new access token of the same or lesser scope.

In addition to grant types above there are extension grant types. The one that will be relevant to this series is the jwt bearer type, or more formally the JSON Web Token (JWT) profile. In the Microsoft world, you’ll see this referred to as the on-behalf-of flow. This is the flow that Microsoft will use for any multi-hop OAuth. There is also another newer grant type to be aware of which is the token exchange flow. This has a similar use case as the jwt bearer flow for multi-hop OAuth but isn’t limited to JWTs and provides additional information in the access token which can be very helpful in identifying client (actor) vs the resource owner (subject) in the access token. Entra doesn’t support this flow to my understanding, so you’ll be using jwt-bearer instead for multi-hop flows as we’ll see in a future post.

In an authorization request will look something like the below:

GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
&code_challenge=6fdkQaPm51l13DSukcAH3Mdx7_ntecHYd1vi3n0hMZY
&code_challenge_method=S256&scope=User.Read HTTP/1.1
Host: server.example.com

In the above example we see the client is requesting the authorization code grant type, is specifying its client id and its redirect_uri (which were established during client registration), a code challenge (for PKCE), and the scope of access it is requesting.

Access tokens come in a few flavors which you can read about in the RFC. The most common type of token is a bearer token within Entra’s implementation. The bearer token is exactly what it sounds like, whoever bears the token holds the power! Bearer tokens are typically JWTs (JSON Web Tokens). While RFC doesn’t specifically require the access token to be cryptographically signed, the ones that Entra ID issues are. The public key used to verify the signature can be obtained for Entra ID from a public metadata endpoint we’ll see later.

Here is a sample access token issued by Entra:

{
"typ": "JWT",
"alg": "RS256",
"kid": "ABC123XYZ789KeyIdentifier"
}
{
"aud": "api://backend-app-client-id",
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
"iat": 1780963927,
"nbf": 1780963927,
"exp": 1780968246,
"aio": "AaQAW/8cAAAA...sessionData...",
"azp": "frontend-app-client-id",
"azpacr": "1",
"name": "John Doe",
"oid": "user-object-id-guid",
"preferred_username": "john.doe@example.com",
"rh": "1.AbcA...refreshTokenHash...",
"scp": "user_impersonation",
"sid": "session-id-guid",
"sub": "subject-claim-unique-identifier",
"tid": "tenant-id-guid",
"uti": "unique-token-identifier",
"ver": "2.0",
"xms_ftd": "xEyJj...federationMetadata..."
}

There are a few important endpoints the client needs to know about for the authorization server. This includes the authorization endpoint (where the resource owner is sent to authenticate and consent) and the token endpoint (where the client obtains an access token). These can be retrieved via a metadata endpoint. This is how Entra ID does it as we’ll see in a future post.

Ok, with that you should now have a high level understanding of OAuth and be aware of its role as an authorization protocol. Key in on that word, authorization. When I perform an OAuth flow I get an access token back to my app that I can use to access a resource owner’s data, but I would still need to authenticate the user to my application and get some basic profile information via another means. In comes OIDC.

What is OpenID Connect?

Like the prior section, my goal is give you a primer. If you want the gory details, take a read through the specification (another tolerable if not enjoyable read). Microsoft and Auth0 have solid one pagers if reading specifications isn’t your style.

The OIDC protocol is built on top of the OAuth (Open Authorization) protocol to provide an identity layer and authentication layer. It gives us the means get some assurance that the user is who they say they are and get some basic information about the user.

Within the OpenID protocol there are three roles that exist. These include:

  • End User
  • RP (Relying Party)
  • OP (OpenID Provider)

Since the protocol is built on top of the OAuth protocol, these roles will map nicely to the OAuth roles as we’ll see.

We first have the end user. The end user is the human participant that will be access our application. They are very often also the resource owner for any data we may want to access about them as we’ll see later.

Next, we have the RP. The RP is the application that is requesting end user authentication and claims (or data/attributes) about the user. This will be an OAuth client application.

Finally we have the OP. The OP is the server capable of authenticating the user and providing claims about the user’s identity. This role is fulfilled by the OAuth authorization server in all instances (I don’t believe their are exceptions, but feel free to correct me) .

The OP will issue a security token referred to as an id token with claims about the authentication of the end user, including claims about the user.

This is another instance where the specification did a great job with a high level flow diagram.

High-level OIDC Flow

The structure of the authentication request is almost identical to the structure of an authorization request in OAuth.

GET /authorize?response_type=code&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
&scope=User.Read+openid+profile
&state=random-state-value
&nonce=random-nonce-value
&code_challenge=IFrWuREBBR_QJ39q5Ts4
&code_challenge_method=S256

Notice above that we have an added state and nonce. The state helps to provide CSRF (cross-site request forgery) attacks, such as fooling the victim into accessing an attacker’s account to get them to upload data or perhaps purchase things for the attacker’s account. The nonce helps to mitigate id token replay attacks (app validates the nonce in the id token matches what it expects for the user’s session). Now the major things to pay attention to is the additional scopes. Here we see the openid and profile scopes. The openid scope tells the OP the client is looking for an id token. The profile scope is an optional scope which tells the OP to include additional claims in the id token such as the user’s name, preferred_username, and the like.

Once the RP (client / application) exchanges its authorization code (think authorization code grant) to the OP/authorization server, it returns back the an OIDC id token in addition to the OAuth access token. The application can then use the claims in the id token to identify the user, get information into how the user authenticated, get group information, and anything else you can stuff into the claims. It gives the application context about the user.

Below is an example id token’s payload. ID tokens follow the JWT standard and are cryptographically signed by a private key held by the OP. Clients will need to verify the signature using the OPs public key which is usually published in the OIDC discovery endpoint which looks something like this https://{issuer}/.well-known/openid-configuration. We’ll see an example when we break down Entra’s implementation.

{
"iss": "http://exmaple.com.com",
"sub": "123456",
"aud": "myclientid",
"exp": 1311281970,
"iat": 1311280970,
"name": "Homer Simpson",
"given_name": "Homer",
"family_name": "Simpson",
"gender": "male",
"birthdate": "2025-10-31",
"email": "homersimpson@example.com",
"picture": "http://example.com/homersimpson/me.jpg"
}

Your takeaways

At this point you should have a reasonably decent high level understanding of OAuth/OIDC. If you are already an “expert” you likely snorted milk through you nose reading my shitty explanation. What I mainly want you to take away from this is that OAuth is an authorization protocol with OIDC providing an authentication layer nicely on top. I like to think of OAuth as the cake with OIDC as the frosting. That top layer doesn’t work without the bottom layer (you freaks that eat the frosting right out of the can shall remain silent) and the bottom layer is enriched by the frosting on top. It’s 7PM and I’m craving a sweet, lay off.

In my next post I’ll walk through building out the required components in Entra ID for the frontend application. You’ll read and recognize properties that translate directly back to these two protocols. Other properties may not have the same name, but you’ll understand why they exist.

Alright, my brain is fried. Enjoy the weekend!

Entra ID – Deep Dive – The Basics – Part 1

Entra ID – Deep Dive – The Basics – Part 1

This is part of my series on Microsoft Entra ID:

  1. Entra ID – Deep Dive – The Basics – Part 1
  2. Entra ID – Deep Dive – Protocol Primer – Part 2
  3. Entra ID – Deep Dive – Entra ID Authentication – Part 3
  4. Entra ID – Deep Dive – Workload Identity Federation – Bonus

Yeah… You read that right. I’m finally biting the bullet and doing a series on Entra ID. There are some cobwebs there, but once upon a time I was a half-baked “identity guy”. Nah, I’m not returning to that place, but I have had to visit it more frequently over the past few months. With the growing use of agents, identity has one again become of those things that apparently everyone is a so called “expert in”. I aint no expert, but I have been digging into the chatter around the implications of agents into the identity world. Given my employer, that has been spending some time in the Entra ID Agent Identity space.

Digging into that demanded I go back to some of the basics of Entra ID such as the purpose of an application registration versus a service principal and the request flow when authenticating a user to an application using OIDC (OpenID Connect) or executing an on-behalf-of flow in OAuth. I had conceptual knowledge of the above, but it was too ivory tower. I needed to eat the dog food and do it. After spending a few weeks reading through the documentation, reviewing captured requests and responses, reviewing RFCs, and poorly coding some applications to exercise on the concepts I figured it was time to brain dump what I learned before I get distracted with some new shiny widget and forget 60% of it.

So yeah… that’s where this series is coming from. Hopefully some folks out there draw some value out of it beyond serving as a refresher for my neural pathways.

With that rambling out of the way, let’s get to it.

WTF is Entra ID?

Many years ago when Microsoft first introduced Azure Active Directory my peers and I scratched our heads with this question. Given the name, the initial assumption was it was the Windows Active Directory “killer” (stop trying to make that happen, it aint gonna happen). That seems to have been a pretty common assumption given what I’ve heard from customers over the years as I sat in the vendor space and is likely why the name was shifted a few years back to Entra ID. Either that or marketing needed to justify their existence with another product rename.

If you want the professional explanation as to what Entra ID is you can read the public documentation and bask in the marketing mumbo jumbo. Since you’re here, you’re going to get the less fancy and quick and dirty Matt Felton explanation. My take is that Entra ID does a lot of shit, but at its core it is:

  • An identity store for the identities of human and non-human security principals, their attributes, and their credentials.
  • An authentication service which can act as an OAuth Authorization Server, OIDC identity provider, SAML IdP / SP, and even Kerberos / LDAP provider (gross)
  • A data store for resources that represent applications that can be used for OAuth and OIDC

It provides these core services to Microsoft’s cloud services such as M365, Azure, Dynamics 365, and whatever other clouds Microsoft has that I’m missing. It can be extended to provide these services to other applications by integrating with it via one of the supported protocols like we’ll see in further posts in this series.

Entra ID is divided into separate tenants which represent unique identity boundaries. Services for a given customer (such as an Azure subscription) are associated to an Entra ID tenant which acts as the identity boundary for those resources. Tenants can trust other tenants to facilitate cross tenant accessing of resources through features like Entra ID External ID.

The identity and data store pieces of the Entra ID service are where users, groups, many types of service principals, applications, and device resources are stored. For us old people, similar to an LDAP, each of these resources is similar to an object in LDAP that has a schema with specific properties that are consumed by native Microsoft and third-party services.

That should be enough context to give you a very general idea of what Entra ID is. Like I mentioned above, it does a lot of shit (conditional access, privileged identity management, etc) that isn’t relevant to the point of this series and that I’m not going to discuss. If you can hammer it in your head those two core functions, it will be enough to get you through this series.

Human Resources in Entra

Within Entra ID human accounts will be represented by user resources. User resources have a selection of properties you’ll be familiar with if you’ve ever worked with an LDAP or Windows Active Directory. These include properties like givenName, surname, mail, and userPrincipalName. These user resources can be created directly in Entra to create “cloud users”, created through the Entra ID External ID, or created by a synchronization process like Entra Connect Sync or a third-party solution like Okta. Humans will use these user accounts (resources) to authenticate to Entra ID to access integrated services. A typical user account looks like the below:

  {
    "businessPhones": [
      "555-555-5503"
    ],
    "displayName": "John Smith",
    "givenName": "John",
    "id": "b44b0442-d7e8-48f9-5555-5bfe1a4dabbc",
    "jobTitle": "Accountant",
    "mail": "john.smith@sometenant.com",
    "mobilePhone": "555-555-5503",
    "officeLocation": "Oz",
    "preferredLanguage": null,
    "surname": "Smith",
    "userPrincipalName": "john.smith@sometenant.com"
  }

Users can be organized into group resources. Like Windows Active Directory, these groups can be used to grant access to resource or used as distribution groups for email. Entra has multiple types of groups including Microsoft 365 Groups (Unified, your mail-enabled security groups for the modern age), Security Groups (for providing access to Entra-integrated resources), and mail-enabled security groups and distribution groups synchronized from Windows Active Directory. You’ll likely use a mix and there are benefits and considerations of both paths. A typical Microsoft 365 group looks like the below:

 {
    "classification": null,
    "createdDateTime": "2024-07-03T18:20:02Z",
    "creationOptions": [
      "ExchangeProvisioningFlags:3552",
      "Team"
    ],
    "deletedDateTime": null,
    "description": null,
    "displayName": "Fabrikam Employees",
    "expirationDateTime": null,
    "groupTypes": [
      "Unified"
    ],
    "id": "dedf1a66-103d-46c1-5555-c62439776bfc",
    "infoCatalogs": [],
    "isAssignableToRole": null,
    "mail": "fabrikam_employees@fabrikam.com",
    "mailEnabled": true,
    "mailNickname": "fabrikam_employees",
    "membershipRule": null,
    "membershipRuleProcessingState": null,
    "onPremisesDomainName": null,
    "onPremisesLastSyncDateTime": null,
    "onPremisesNetBiosName": null,
    "onPremisesProvisioningErrors": [],
    "onPremisesSamAccountName": null,
    "onPremisesSecurityIdentifier": null,
    "onPremisesSyncEnabled": null,
    "preferredDataLocation": null,
    "preferredLanguage": null,
    "proxyAddresses": [
      "SMTP:fabrikam_employees@fabrikam.com"
    ],
    "renewedDateTime": "2024-07-03T18:20:02Z",
    "resourceBehaviorOptions": [
      "WelcomeEmailDisabled",
      "SubscribeMembersToCalendarEventsDisabled",
      "HideGroupInOutlook"
    ],
    "resourceProvisioningOptions": [
      "Team"
    ],
    "securityEnabled": false,
    "securityIdentifier": "S-1-12-1-3739163238-1187057725-555555555-4234901305",
    "serviceProvisioningErrors": [],
    "theme": null,
    "uniqueName": null,
    "visibility": "Public"
  }

That’s all I’m going to say about humans, since the primary purpose of this series is the non-human side of the fence.

Non-Human Resources

For non-human identities our main resources are applications and service principals. There are others such as device identities, but I won’t be getting into that.

First, there is the application resource. You’ll see this referred to as the application registration or app registration in Microsoft documentation and Microsoft GUI-based experiences like the Azure Portal. The application resource (or app registration) is the globally unique representation of an application across all of Entra ID and exists only within the Entra ID tenant it was registered in. It’s easiest to think of an application resource as an OAuth client registration, because OAuth and OIDC are likely why this type of resource exists anyway (I’ll cover this a bit more in some upcoming posts). An application resource can have a credential which is used to authenticate it to Entra ID (OAuth confidential client) and this credential can be a secret, certificate (very cool and under used), or a federated credential (basis of workload identity). This resource will include properties required for OAuth flows such as the client id (appId), the audience (identifierUris), any scopes it exposes (oauth2PermissionScopes), and redirectUris (when using interactive OAuth flows). Below is an example of an application resource associated with an application that authenticates users with Entra ID.

{
"id": "55555555-5555-5555-5555-555555555555",
"deletedDateTime": null,
"appId": "11111111-1111-1111-1111-111111111111",
"applicationTemplateId": null,
"disabledByMicrosoftStatus": null,
"createdByAppId": "88888888-8888-8888-8888-888888888888",
"createdDateTime": "2026-07-15T02:13:57Z",
"displayName": "Demo Entra ID application - Frontend",
"description": "This application is the frontend for my demo Entra ID application",
"groupMembershipClaims": "ApplicationGroup",
"identifierUris": [],
"isDeviceOnlyAuthSupported": null,
"isDisabled": null,
"isFallbackPublicClient": false,
"nativeAuthenticationApisEnabled": null,
"notes": null,
"publisherDomain": "sometenant.onmicrosoft.com",
"serviceManagementReference": "business_unit1@sometenant.onmicrosoft.com",
"signInAudience": "AzureADMultipleOrgs",
"tags": [],
"tokenEncryptionKeyId": null,
"uniqueName": null,
"samlMetadataUrl": null,
"defaultRedirectUri": null,
"certification": null,
"requestSignatureVerification": null,
"addIns": [],
"api": {
"acceptMappedClaims": null,
"knownClientApplications": [],
"requestedAccessTokenVersion": null,
"oauth2PermissionScopes": [],
"preAuthorizedApplications": []
},
"appRoles": [],
"info": {
"logoUrl": null,
"marketingUrl": null,
"privacyStatementUrl": null,
"supportUrl": null,
"termsOfServiceUrl": null
},
"keyCredentials": [],
"optionalClaims": {
"accessToken": [],
"idToken": [
{
"additionalProperties": [],
"essential": false,
"name": "groups",
"source": null
}
],
"saml2Token": []
},
"parentalControlSettings": {
"countriesBlockedForMinors": [],
"legalAgeGroupRule": "Allow"
},
"passwordCredentials": [
{
"customKeyIdentifier": null,
"displayName": null,
"endDateTime": "2028-07-15T02:14:07.273797Z",
"hint": "8pV",
"keyId": "eea9ef15-ec2f-4cbc-9e6d-2e1dc8ffba30",
"secretText": null,
"startDateTime": "2026-07-15T02:14:07.273797Z"
}
],
"publicClient": {
"redirectUris": []
},
"requiredResourceAccess": [
{
"resourceAppId": "55555555-5555-5555-5555-555555555555",
"resourceAccess": [
{
"id": "00000000-0000-0000-0000-000000000001",
"type": "Scope"
}
]
},
{
"resourceAppId": "00000003-0000-0000-c000-000000000000",
"resourceAccess": [
{
"id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
"type": "Scope"
},
{
"id": "98830695-27a2-44f7-8c18-0c3ebc9698f6",
"type": "Role"
}
]
}
],
"verifiedPublisher": {
"displayName": null,
"verifiedPublisherId": null,
"addedDateTime": null
},
"web": {
"homePageUrl": null,
"logoutUrl": null,
"redirectUris": [
"http://localhost:8100/callback"
],
"implicitGrantSettings": {
"enableAccessTokenIssuance": false,
"enableIdTokenIssuance": false
},
"redirectUriSettings": [
{
"uri": "http://localhost:8100/callback",
"index": null
}
]
},
"servicePrincipalLockConfiguration": {
"isEnabled": true,
"allProperties": true,
"credentialsWithUsageVerify": null,
"credentialsWithUsageSign": null,
"identifierUris": null,
"tokenEncryptionKeyId": null
},
"spa": {
"redirectUris": []
}
}

The service principal resource is the other major non-human resource to be aware of. A service principal represents an instance of an application resource in an Entra ID tenant. While you’ll only ever have a single application resource in the registered Entra ID tenant to represent an application, that application might have multiple service principals in multiple Entra ID tenants if it was built as a multi-tenant application. When permissions are granted to an application to access a resource secured with Entra ID, it will use the permissions associated to the service principal. The Microsoft GUI-experience will call a service principal an Enterprise Application.

As you’ll see below, a service principal looks a lot like the application resource its providing an identity for. The appId property is the property used to directly map it back to the application resource. The service principal type in this scenario is Application. There are a variety of service principal types that provide a non-human identity for different use cases like for managed identities, agent identity blueprints, and agent identities.

{
"id": "44444444-4444-4444-4444-444444444444",
"deletedDateTime": null,
"accountEnabled": true,
"alternativeNames": [],
"appDisplayName": "Demo Entra ID application - Frontend",
"appDescription": "This application is the frontend for my demo Entra ID application",
"appId": "11111111-1111-1111-1111-111111111111",
"applicationTemplateId": null,
"appOwnerOrganizationId": "88888888-8888-8888-8888-888888888888",
"appRoleAssignmentRequired": false,
"createdByAppId": "88888888-8888-8888-8888-888888888888",
"createdDateTime": "2026-07-15T02:14:16Z",
"description": null,
"disabledByMicrosoftStatus": null,
"displayName": "Demo Entra ID application - Frontend",
"homepage": null,
"isDisabled": null,
"loginUrl": null,
"logoutUrl": null,
"notes": null,
"notificationEmailAddresses": [],
"preferredSingleSignOnMode": null,
"preferredTokenSigningKeyThumbprint": null,
"replyUrls": [
"http://localhost:8100/callback"
],
"servicePrincipalNames": [
"611111111-1111-1111-1111-111111111111"
],
"servicePrincipalType": "Application",
"signInAudience": "AzureADMultipleOrgs",
"tags": [],
"tokenEncryptionKeyId": null,
"samlSingleSignOnSettings": null,
"addIns": [],
"appRoles": [],
"info": {
"logoUrl": null,
"marketingUrl": null,
"privacyStatementUrl": null,
"supportUrl": null,
"termsOfServiceUrl": null
},
"keyCredentials": [],
"oauth2PermissionScopes": [],
"passwordCredentials": [],
"resourceSpecificApplicationPermissions": [],
"verifiedPublisher": {
"displayName": null,
"verifiedPublisherId": null,
"addedDateTime": null
}
}

The official documentation likes to refer to the application object as the template for the application and the service principal as the security principal. I think that’s a pretty damn good single sentence explanation.

What are we going to build?

Now that you have the bare bones basics of Entra, you likely want to understand how an application would go about using it for authentication and authorization. While you might not be doing this now and it may not seem relevant, it will become very relevant to you if you begin building agents in Microsoft’s clouds through Microsoft Foundry, CoPilot Studio, or the 18 other random services Microsoft allows agents to be built. This will also be relevant to you if you’re going to consume Microsoft resources (such as Azure) from other clouds through an application or an agent. So yeah, you should understand what this looks like to do. The whole Entra ID Agent Identity feature builds on these foundational pieces.

To see these concepts in action I’m going to walk through a very simplistic solution with a frontend website and backend API in Python. The frontend website is built using the Flask Framework and the backend API is built with fastapi.

Demo application architecture

The frontend website will use Entra ID to authenticate the user and will be issued an OIDC id token to identify the user. The frontend will get some information from the user from the id token and access token it receives from Entra and will make additional calls to the Microsoft Graph API using the client credentials flow to grab other attributes of the user’s identity. I’ll also show how to include the user’s Entra ID group information in the id or access token and how to handle nested group membership.

The frontend will have some pages that call functions within the backend API. One example is the story endpoint will pull a pre-built AI generated story about the user which has been uploaded to blob storage in an Azure Storage Account. The backend API will use the on-behalf-of flow to access the storage account as the user to pull the user’s specific story.

This will demonstrate some of the most common flows including authentication, OAuth client credentials flow, and OAuth on-behalf-of (or jwt bearer). I’ll show you how the id tokens and access tokens look in different scenarios pointing out the relevant claims and how they’re used upstream. I’ll even share some process flows so you understand what does what in a given flow.

My primary goal here is give you the basics so you can walk away with a more solid understanding of what’s happening under the hood and how Entra ID has decided to implement OIDC and OAuth. I can’t stress how helpful this will be for you once you start diving into the agent identity space (which I’ll be covering after this series).

Summing it up

Ok, so you know what you’re in for. This series is gonna be relatively deep in the weeds so bring your favorite caffeinated beverage for future posts. I’m doing everything direct with the REST APIs because I want to show you the gory details. No pretty SDKs for you. If you’ve had a “conceptual” idea of how this works without the implementation specifics (like I did before I went down this rabbit hole) this series should help to fill those gaps.

In the next post I’ll walk through setting up Entra for the frontend website, authenticating a user, and exploring the id token and access token. The post following that will walk through using the application’s identity context to get more information about the user from the Microsoft Graph API such as nested group membership, then I’ll finish up the series by walking through the on-behalf-of flow with the backend API to show you how to carry the user’s identity context through the application to the destination resource down the line.

See you next post!