Microsoft Entra ID for Microsoft 365 Developers: What You Need to Know
A comprehensive guide to Microsoft Entra ID (formerly Azure AD) for Microsoft 365 developers covering authentication flows, app registrations, permissions, tokens, SSO, and how Entra ID underpins every M365 app you build.
Every Microsoft 365 (M365) app you build, whether it’s a SharePoint Framework (SPFx) web part, a Microsoft Teams tab, an Azure Function API, or a Microsoft 365 Copilot declarative agent, authenticates through the same identity layer: Microsoft Entra ID.
There’s no way around it. You can’t call Microsoft Graph without a token. You can’t build a Teams app without SSO. You can’t secure an Azure Function backend without validating who’s calling it. And all of those tokens come from one place.
If you’ve been building for the M365 ecosystem and felt like the identity side of things was confusing, unclear, or buried in Microsoft’s documentation… you’re not alone. I’ve spent over two decades working in this space, and I still think Microsoft could do a better job explaining how all the pieces fit together.
So let’s fix that. This article is your map to understanding Microsoft Entra ID as an M365 developer. I’ll cover the core concepts, link to deep dives on the specifics, and call out the places where things get confusing (and where Microsoft’s own documentation leaves gaps).
This isn’t aimed at IT admins or tenant administrators. This is for developers who need to understand how identity works in the apps they build.
What is Microsoft Entra ID?
Microsoft Entra ID (formerly Azure Active Directory, or Azure AD) is Microsoft’s cloud-based identity and access management service. If you’ve worked with M365 at all, you’ve interacted with Entra ID whether you knew it or not. It’s the identity backbone of every M365 tenant.
In July 2023, Microsoft rebranded Azure Active Directory to Microsoft Entra ID. To be clear: the technology is identical. The APIs didn’t change. The OAuth endpoints didn’t change. MSAL libraries didn’t change. Microsoft just renamed it.
I’ll be honest here: the rebrand was a source of unnecessary confusion for the developer community. Documentation, blog posts, and Stack Overflow answers still reference “Azure AD” everywhere, and the SDK package names still carry the old branding. If you search for “Azure AD app registration,” you’ll find plenty of content that’s still perfectly accurate for Entra ID. Don’t let the naming confuse you.
Here’s what matters for you as a developer: Entra ID is where your apps get their identity. It handles authentication (proving who someone is) and authorization (determining what they can do). Every time your code acquires an OAuth token, requests API permissions, or validates a JWT, Entra ID is the service doing the heavy lifting.
The broader “Microsoft Entra” family includes other services like Entra External ID (for customer-facing identity) and Entra Permissions Management, but those are separate products. When I say “Entra ID” in this article, I’m talking about the core identity platform that every M365 developer needs to understand.
If you want to see how these identity concepts evolved over the years, I’ve written about the authentication landscape going back to the early days of Office 365 APIs. You can trace the journey from ADAL and OWIN through OAuth 2.0 flows to today’s MSAL-powered patterns in posts like Understanding Microsoft Entra ID, OAuth2 & OpenID Connect and Demystifying the Authentication Dance for Office 365 APIs.
| Azure AD era (pre-July 2023) | Entra ID era (current) |
|---|---|
| Azure AD portal | Entra admin center |
| Azure AD app registrations | Entra ID app registrations |
| Azure AD tenant | Entra ID tenant |
| MSAL libraries | MSAL libraries (same packages) |
| OAuth 2.0 endpoints (v2.0) | OAuth 2.0 endpoints (unchanged) |
| Azure AD B2C | Entra External ID |
The technology, APIs, and SDKs are identical. Only the branding and portal UI changed.
Why every M365 developer needs to understand Entra ID
Here’s the thing: there’s no “anonymous mode” in M365 development. Every interaction with SharePoint APIs, Microsoft Graph, Teams platform services, or your own backend APIs requires authentication. It’s not optional.
Even when it seems like things “just work,” identity is involved. When an SPFx web part calls this.context.httpClient, the SharePoint page has already authenticated the user through Entra ID and passed along a token. When a Teams tab loads, Teams has already negotiated SSO behind the scenes. You’re standing on top of an identity layer whether you realize it or not.
The problem is that when you don’t understand that layer, things go wrong in ways that are hard to diagnose:
- You get cryptic
AADSTSerror codes and have no idea what they mean - Permission consent prompts fail silently or require admin approval you didn’t expect
- Tokens expire in production and your app breaks because you didn’t handle refresh properly
- API calls that work perfectly in your dev tenant fail in a customer’s production environment
I’ve seen all of these over the years, and the root cause almost always traces back to a gap in understanding how Entra ID works.
Understanding identity is what separates someone who copies sample code from someone who can architect real solutions. It’s the difference between building something that works in a demo and building something that works in a regulated enterprise with 50,000 users and a security team that reviews every app registration.
If you want to become the go-to M365 developer in your organization, this is the knowledge that gets you there.
- AADSTS error codes you can’t decode
- Permission consent prompts that never go through
- Tokens that expire at the worst possible time
- API calls that work in dev but fail in production
If you’ve hit any of these, you need a better understanding of Entra ID.
App registrations
An app registration is your app’s identity in Entra ID. Think of it as your app’s passport: it tells Entra ID who your app is, what it’s allowed to do, and how users and services should interact with it.
Every M365 app you build needs one. Whether it’s a Teams tab, an Azure Function API, a standalone SPA, or a background daemon, the app registration is where you configure the identity settings.
When you create an app registration in the Entra admin center (or through the Microsoft Graph API, or CLI), you get a few key identifiers:
- Application (client) ID: A unique GUID that identifies your app. You’ll use this in your code’s MSAL configuration.
- Directory (tenant) ID: Identifies the Entra ID tenant where the app is registered.
- Redirect URIs: The URLs where Entra ID sends users after authentication. Getting these wrong is one of the most common sources of auth failures.
One decision you’ll make early is whether your app registration is single-tenant or multi-tenant. Single-tenant means only users from your specific tenant can authenticate. Multi-tenant means users from any Entra ID tenant can use your app. For most internal enterprise M365 solutions, single-tenant is the right choice. Multi-tenant is for ISVs and apps that serve multiple organizations.
For authentication credentials, you have two options: client secrets and certificates. Client secrets are simple strings that expire and need rotation. Certificates are more secure and are the recommended approach for production workloads. If you’re using CI/CD pipelines, consider federated identity credentials as an even better alternative that eliminates secrets entirely.
You can also define custom permissions (app roles) on your app registration, which is powerful when you’re building your own APIs and want to control who can access them. I wrote a detailed walkthrough on this in Leverage Custom Permissions in Entra ID Applications.
1// Key fields from an Entra ID app registration manifest
2{
3 "appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
4 "displayName": "My M365 Custom App",
5 "signInAudience": "AzureADMyOrg", // single-tenant
6 "identifierUris": [
7 "api://a1b2c3d4-e5f6-7890-abcd-ef1234567890"
8 ],
9 "web": {
10 "redirectUris": [
11 "https://myapp.azurewebsites.net/auth/callback"
12 ]
13 },
14 "spa": {
15 "redirectUris": [
16 "http://localhost:3000"
17 ]
18 }
19}
Microsoft Entra ID portal
App registration overview page
Permissions and consent
The permissions model in Entra ID is where a lot of M365 developers get tripped up. Let me break it down clearly.
There are two types of permissions:
Delegated permissions act on behalf of a signed-in user. Your app can only do what the current user is allowed to do. When your SPFx web part reads the user’s calendar through Microsoft Graph, that’s a delegated permission at work. The app acts as the user.
Application permissions let your app act as itself, without any user context. A background service that reads all users’ mailboxes or a daemon that processes files across the tenant uses application permissions. These are powerful and typically require admin consent because they aren’t constrained by any individual user’s access level.
When to use which? If there’s a human using your app and the action is on their behalf, use delegated. If it’s a background process, a scheduled job, or any scenario without a user present, use application permissions.
Microsoft Graph is the most common API you’ll request permissions for. Permission scopes like Sites.Read.All, Mail.Send, User.Read, and Calendars.ReadWrite are the building blocks of what your app can do. The key principle here is least privilege: request only what you actually need. If your app reads SharePoint sites, don’t request Sites.FullControl.All. Admins will (rightfully) reject overly broad permission requests.
Admin consent is required for all application permissions and some delegated permissions that access organization-wide data. User consent is sufficient for basic delegated permissions like User.Read. Some tenants disable user consent entirely, meaning every permission request goes through an admin approval workflow.
For dynamic scenarios, incremental consent lets you request permissions only when the user actually needs them, rather than asking for everything upfront. This creates a better user experience and is the recommended pattern for delegated permissions.
If you’re building your own APIs, you can define custom permissions through app roles on your API’s app registration. I covered this in detail in Leverage Custom Permissions in Entra ID Applications. And if you’re working with SPFx, you’ll want to read about why you should be cautious with declarative permissions before adding them to your solution packages.
| Aspect | Delegated permissions | Application permissions |
|---|---|---|
| Who acts | App acts on behalf of a user | App acts as itself |
| User context | Required (user must sign in) | Not required (no user present) |
| Effective access | Intersection of app + user | Full permission scope granted |
| Consent type | User or admin consent | Admin consent only |
| Common scenarios | Web apps, SPFx, Teams tabs | Daemons, background jobs, CI/CD |
| Example scopes | User.Read, Mail.Send | Mail.ReadWrite.All, Sites.Read.All |
1// requiredResourceAccess in the app manifest
2// Shows both delegated and application permissions for Microsoft Graph
3{
4 "requiredResourceAccess": [
5 {
6 "resourceAppId": "00000003-0000-0000-c000-000000000000", // Microsoft Graph
7 "resourceAccess": [
8 {
9 "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d", // User.Read (delegated)
10 "type": "Scope" // "Scope" = delegated
11 },
12 {
13 "id": "df021288-bdef-4463-88db-98f22de89214", // User.Read.All (application)
14 "type": "Role" // "Role" = application
15 }
16 ]
17 }
18 ]
19}Authentication flows for M365 apps
OAuth 2.0 and OpenID Connect define several authentication flows, but as an M365 developer, you’ll primarily encounter four. Knowing which one to use in each scenario is critical. If you want to dig into the theory behind these flows, I covered the foundational concepts in Understanding Microsoft Entra ID OAuth2 authentication flows.
Authorization Code flow with PKCE is your default choice for any app where a user is present. SPAs, server-rendered web apps, Teams tabs… if someone’s clicking buttons and viewing data, this is the flow. MSAL handles the complexity for you. The “PKCE” (Proof Key for Code Exchange) part is a security enhancement that prevents authorization code interception. Always use it. There’s no reason not to.
Client Credentials flow is for daemon applications and background services that run without a user. An Azure Function that processes files on a schedule, a CI/CD pipeline that deploys to SharePoint… these use client credentials because there’s no human to authenticate interactively.
On-Behalf-Of (OBO) flow is the one that catches people off guard. When your backend API receives a token from a client app and needs to call another API (like Microsoft Graph) on behalf of that same user, you use OBO. This is critical for Teams SSO scenarios where your backend needs to exchange a Teams token for a Graph token. I’ll cover this more in the Teams SSO section.
Device Code flow is a niche but useful flow for CLI tools, IoT devices, and scenarios where the device doesn’t have a browser. The user authenticates on a separate device and the app polls for completion.
Implicit flow is deprecated. If you’re still using it, switch to Authorization Code with PKCE. The implicit flow returns tokens directly in the URL fragment, which is a security risk. MSAL v2+ uses auth code with PKCE by default.
1// MSAL.js - Authorization Code flow with PKCE (SPA)
2import { PublicClientApplication } from "@azure/msal-browser";
3
4const msalConfig = {
5 auth: {
6 clientId: "your-client-id-here",
7 authority: "https://login.microsoftonline.com/your-tenant-id",
8 redirectUri: "http://localhost:3000"
9 }
10};
11
12const msalInstance = new PublicClientApplication(msalConfig);
13
14// Sign in the user
15const loginResponse = await msalInstance.loginPopup({
16 scopes: ["User.Read"]
17});
18
19// Acquire a token silently for subsequent API calls
20const tokenResponse = await msalInstance.acquireTokenSilent({
21 scopes: ["Sites.Read.All"],
22 account: loginResponse.account
23});
24
25// Use tokenResponse.accessToken in your API calls
SPFx and Entra ID
SharePoint Framework (SPFx) has a unique relationship with Entra ID because SPFx web parts run inside the SharePoint page context, which means the user is already authenticated. You don’t need to handle sign-in yourself. But the moment you need to call an API beyond the SharePoint page, you need to understand how SPFx interacts with Entra ID.
SPFx provides two built-in clients for making authenticated API calls:
MSGraphClientV3 is SPFx’s wrapper around the Microsoft Graph client. It handles token acquisition automatically for the current user. If you need to call Microsoft Graph endpoints, this is the simplest path.
AadHttpClient is the more flexible option. It lets you call any Entra ID-protected API, including your own custom APIs hosted on Azure Functions or App Service. You provide the target API’s resource URI, and SPFx handles acquiring the right token.
Both of these rely on declarative permissions defined in your solution’s package-solution.json. When your SPFx package is deployed, a tenant admin approves the requested permissions through the SharePoint Admin Center’s API Access page. Here’s the thing though: I’d encourage you to think carefully before using declarative permissions. The permissions are granted tenant-wide, meaning any SPFx solution in the tenant could potentially use them. That’s a broader security surface than most developers realize.
If you’ve been in the SPFx world for a while, you might remember domain isolated web parts. These ran in a separate iframe with their own app registration, which provided better permission isolation. Unfortunately, Microsoft retired domain isolated web parts. If you had solutions relying on them, you’ll need to migrate. I wrote about how to secure SPFx solutions in the post-isolation era, which covers your options going forward.
The current best practice is to use AadHttpClient with a properly configured app registration for your backend API, and validate tokens on the API side. This gives you control over who can call your API and with what permissions, even though SPFx’s permission model is tenant-wide.
If you’re looking to deepen your SPFx skills, including how to properly handle authentication scenarios, my Mastering the SharePoint Framework course covers this in depth across 40+ hours of content.
| Approach | How it works | Status |
|---|---|---|
| Domain isolated web parts | Separate iframe, its own app registration | ⛔ Retired (2024) |
| Declarative permissions | package-solution.json, tenant-wide scope | ⚠️ Use with caution |
| AadHttpClient + custom app registration | Your own app registration, server-side token validation | ✅ Recommended |
1// SPFx - Using AadHttpClient to call a custom Entra ID-protected API
2import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
3
4// Inside your web part's method:
5const apiEndpoint = "https://my-api.azurewebsites.net/api/data";
6const apiResourceUri = "api://my-api-client-id";
7
8// Get an authenticated client for your API's resource URI
9const aadClient: AadHttpClient = await this.context
10 .aadHttpClientFactory
11 .getClient(apiResourceUri);
12
13// Make an authenticated GET request - SPFx handles the token
14const response: HttpClientResponse = await aadClient.get(
15 apiEndpoint,
16 AadHttpClient.configurations.v1
17);
18
19if (response.ok) {
20 const data = await response.json();
21 console.log("API response:", data);
22}Teams apps and SSO
Microsoft Teams provides a built-in single sign-on (SSO) mechanism that lets your app silently authenticate the current user without showing a login prompt. It’s a great user experience, but the way it works is different from standard web auth flows, and that difference trips up a lot of developers.
Here’s the key thing to understand: when you call authentication.getAuthToken() in the Teams JavaScript SDK, Teams returns a token that’s scoped to your app registration. It is not an access token for Microsoft Graph. You cannot take that token and call Graph endpoints with it. This is the single most common mistake I see developers make with Teams SSO.
So what do you do when your Teams app needs to call Microsoft Graph? You need the On-Behalf-Of (OBO) flow. Your client-side code sends the Teams SSO token to your backend API. Your backend then exchanges that token with Entra ID using OBO to get a proper Graph access token. The backend calls Graph, gets the data, and returns it to the client.
Setting up the app registration for Teams SSO has some specific requirements. You need an api:// identifier URI that follows the Teams format (api://your-domain.com/your-client-id), you need to configure specific redirect URIs, and you need to add the Teams client application IDs as authorized client applications. The Teams documentation covers the exact values, but the pattern is the same for every Teams SSO implementation.
Tab apps, bot-based apps, and message extensions each have slightly different auth patterns on the surface, but the underlying identity flow is fundamentally the same: Teams provides an initial token, and if you need more access, you exchange it server-side.
If you’re building your first Teams app and want structured, hands-on guidance, my Learn Microsoft Teams Apps & Tabs Development workshop walks through building Teams apps end to end, including the authentication patterns covered here.
1// Teams client-side SSO - acquiring and sending the token
2import { app, authentication } from "@microsoft/teams-js";
3
4// Initialize the Teams SDK
5await app.initialize();
6
7// Get the SSO token (this is NOT a Graph token)
8const ssoToken = await authentication.getAuthToken();
9
10// Send the SSO token to your backend for OBO exchange
11const response = await fetch("https://your-api.azurewebsites.net/api/data", {
12 method: "GET",
13 headers: {
14 "Authorization": `Bearer ${ssoToken}`,
15 "Content-Type": "application/json"
16 }
17});
18
19const data = await response.json();Validating tokens
If you’re building an API that receives Entra ID tokens (and if you’re following the patterns above, you are), you need to validate those tokens properly. Never trust a token without verification.
An OAuth access token from Entra ID is a JSON Web Token (JWT), which is three base64url-encoded segments separated by dots: header.payload.signature. I wrote a detailed walkthrough of the validation process in Validate Microsoft Entra ID generated OAuth tokens, which is worth reading if you’re implementing this yourself.
Token validation involves two steps:
First, verify the signature. The header tells you which signing key was used (the kid claim). You fetch Entra ID’s public signing keys from the JWKS (JSON Web Key Set) endpoint and use the matching key to verify the token’s signature. If the signature doesn’t match, the token has been tampered with or wasn’t issued by Entra ID. Reject it.
Second, validate the claims. The payload contains claims that tell you about the token:
1// Decoded JWT payload - key claims to validate
2{
3 "aud": "api://a1b2c3d4-e5f6-7890-abcd-ef1234567890", // Audience: must match YOUR API's identifier
4 "iss": "https://login.microsoftonline.com/{tenant-id}/v2.0", // Issuer: must be Entra ID
5 "iat": 1710000000, // Issued at: when the token was created
6 "nbf": 1710000000, // Not before: token isn't valid before this time
7 "exp": 1710003600, // Expiration: token expires after this (typically 60-90 min)
8 "tid": "your-tenant-id", // Tenant ID: validates which tenant issued the token
9 "oid": "user-object-id", // Object ID: unique identifier for the user
10 "scp": "access_as_user", // Scopes (delegated): what the token is authorized to do
11 // OR for application permissions:
12 // "roles": ["Data.Read"], // Roles (application): app-level permissions
13 "preferred_username": "user@contoso.com"
14}The claims you should always check: aud (audience) must match your API’s application ID or identifier URI. iss (issuer) must be the Entra ID endpoint for your tenant. exp (expiration) must be in the future. If any of these fail, reject the token.
One gotcha: Entra ID has v1.0 and v2.0 token endpoints, and they produce tokens with different issuer formats. The v1.0 issuer looks like https://sts.windows.net/{tenant-id}/ while v2.0 uses https://login.microsoftonline.com/{tenant-id}/v2.0. Make sure your validation logic matches the endpoint version you’re using. For new apps, use v2.0.
A quick note on debugging: Microsoft’s jwt.ms tool is handy for decoding tokens during development. But never paste a production token into any web-based decoder. Those tokens grant access to real data.
A JWT is three base64url segments joined by dots — eyJhbGci… . eyJhdWQi… . SflKxwRJ…:
| Segment | What’s in it |
|---|---|
| Header | Signing algorithm (RS256), token type (JWT), and the key ID (kid) used to sign it |
| Payload | The claims: audience (aud), issuer (iss), expiration (exp), scopes (scp)/roles, and user info |
| Signature | Digital signature you verify with Entra ID’s public key from the JWKS endpoint |
Validate in order: signature ✓ → audience ✓ → issuer ✓ → expiration ✓, then accept the token.
Securing Azure Function APIs with Entra ID
Azure Functions are the most common backend for M365 custom solutions. Whether you’re building a proxy for Microsoft Graph calls, processing webhook notifications, or serving data to an SPFx web part, you’ll likely end up with an Azure Function that needs to verify that the caller is who they say they are.
There are two approaches to securing an Azure Function with Entra ID:
Built-in App Service Authentication (“Easy Auth”) is configured through the Azure portal. You enable authentication, point it at your Entra ID app registration, and Azure handles token validation before your function code even runs. It’s quick to set up, but it gives you less control and can be harder to debug when things go wrong.
Manual token validation in code is more work upfront, but you get full control over the authentication pipeline. You extract the Bearer token from the Authorization header, validate the JWT signature and claims, and decide whether to proceed or return a 401. For production workloads, I prefer this approach because you can log exactly what’s happening, handle edge cases, and customize the validation logic.
I covered the full setup process in Securing an Azure Function App with Entra ID, including the app registration configuration and working code.
The app registration for your API needs a few specific configurations: you need to expose an API by defining a scope (like access_as_user), set the Application ID URI (typically api://your-client-id), and add authorized client applications so your SPFx solution or Teams app can request tokens for your API without requiring additional consent.
Don’t forget CORS configuration. If your SPFx web part or Teams app is calling your Azure Function from the browser, you need to allow the SharePoint domain or Teams domain in your CORS settings. This is a common “it works in Postman but not in the browser” issue.
The complete flow looks like this: your SPFx web part (or Teams tab) uses AadHttpClient to acquire a token scoped to your API. It sends the token in the Authorization: Bearer header. Your Azure Function extracts and validates the token, checks the claims, and processes the request if everything checks out.
1// Azure Function - Token validation middleware (Node.js/TypeScript)
2import { app, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions";
3import jwt from "jsonwebtoken";
4import jwksClient from "jwks-rsa";
5
6const client = jwksClient({
7 jwksUri: "https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys"
8});
9
10function getSigningKey(kid: string): Promise<string> {
11 return new Promise((resolve, reject) => {
12 client.getSigningKey(kid, (err, key) => {
13 if (err) reject(err);
14 resolve(key!.getPublicKey());
15 });
16 });
17}
18
19async function validateToken(token: string): Promise<jwt.JwtPayload> {
20 const decoded = jwt.decode(token, { complete: true });
21 if (!decoded) throw new Error("Invalid token format");
22
23 const signingKey = await getSigningKey(decoded.header.kid!);
24
25 return jwt.verify(token, signingKey, {
26 audience: "api://your-api-client-id",
27 issuer: `https://login.microsoftonline.com/{tenant-id}/v2.0`,
28 algorithms: ["RS256"]
29 }) as jwt.JwtPayload;
30}
31
32// In your function handler:
33export async function myFunction(req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> {
34 const authHeader = req.headers.get("authorization");
35 if (!authHeader?.startsWith("Bearer ")) {
36 return { status: 401, body: "Missing or invalid Authorization header" };
37 }
38
39 try {
40 const claims = await validateToken(authHeader.slice(7));
41 // Token is valid - proceed with your logic
42 return { status: 200, jsonBody: { user: claims.preferred_username } };
43 } catch (err) {
44 return { status: 401, body: "Token validation failed" };
45 }
46}Copilot agents and Entra ID
If you’re building Microsoft 365 Copilot declarative agents or custom engine agents, Entra ID is still the identity layer underneath. When a declarative agent uses API plugins to call external services, those API calls are authenticated through Entra ID using the same OAuth patterns covered in this article. The agent acquires tokens on behalf of the user through the same flows, and your API validates those tokens the same way.
I won’t go deep on Copilot agent architecture here because I’ve covered it in a dedicated content hub: How Microsoft 365 Copilot Works: the Tech Stack. If you’re building Copilot agents, start there and then come back here for the identity fundamentals.
Common pitfalls
Let me save you some pain. These are the mistakes I see M365 developers make most often with Entra ID:
Over-requesting permissions. Asking for Sites.FullControl.All when Sites.Read.All would work. Tenant admins review permission requests, and overly broad requests get rejected. Start with the minimum you need and expand only when necessary.
Not handling token refresh. Access tokens expire, typically in 60-90 minutes. If you’re making API calls directly (not through MSAL’s acquireTokenSilent), you need to handle expiration gracefully. MSAL handles refresh tokens automatically when you use acquireTokenSilent. Let it do the work.
Ignoring multi-tenant issues. If your app registration is set to multi-tenant, you must validate the tid (tenant ID) claim in incoming tokens. Without this check, a token from any Entra ID tenant passes signature validation, which is probably not what you want.
Hardcoding client secrets in code. This happens more than you’d think. Client secrets in source code, checked into git repos, deployed in plain text. Use Azure Key Vault, managed identities, or federated identity credentials for CI/CD pipelines. There’s no excuse for secrets in repos.
Mixing up v1.0 and v2.0 endpoints. The v1.0 and v2.0 Entra ID endpoints produce tokens with different issuer formats and slightly different claim structures. If your token validation expects a v2.0 issuer but you configured a v1.0 authority, validation will fail. For new applications, always use v2.0.
Only testing with admin accounts. Many developers test with a global admin and never realize their app requires admin consent for certain permissions. Test with a regular user account early. You’ll catch consent issues before your users do.
Still using the implicit flow. The implicit grant was common in older SPAs but has known security vulnerabilities. MSAL v2+ defaults to the authorization code flow with PKCE. If you have older code using implicit grant, migrate it.
| Pitfall | Fix |
|---|---|
| Over-requesting permissions | Request the minimum scopes. Use Sites.Read.All, not Sites.FullControl.All. |
| Not handling token expiration | Use MSAL’s acquireTokenSilent() — it handles refresh automatically. |
| Ignoring multi-tenant validation | Always validate the tid claim if your app is multi-tenant. |
| Hardcoding client secrets | Use Key Vault, managed identity, or federated identity credentials. |
| Mixing v1.0 and v2.0 endpoints | Use v2.0 for new apps. Match the issuer format in validation. |
| Testing only with admin accounts | Test with a regular user early to catch consent issues. |
| Using the deprecated implicit flow | Migrate to Authorization Code + PKCE via MSAL v2+. |
What’s next?
Entra ID is the foundation of everything you build in the M365 ecosystem. Whether you’re working with SPFx, Teams, Azure Functions, or Copilot agents, the identity patterns are the same: app registrations, OAuth tokens, permission scopes, and token validation.
I wrote this article to be the reference I wish I’d had when I started navigating Microsoft’s identity platform. Bookmark it, come back when you need a refresher, and dig into the deep-dive articles linked throughout for the implementation details.
Here are the deep dives to explore next, based on what you’re building:
- Building an API? Validate Microsoft Entra ID generated OAuth tokens and Securing an Azure Function App with Entra ID
- Working with SPFx? Beware of Declarative Permissions in SharePoint Framework Projects and Secure SPFx Solutions in a Post Isolated Web Part Retirement
- Defining custom API permissions? Leverage Custom Permissions in Entra ID Applications
- Setting up CI/CD? SharePoint Framework CI/CD with GitHub Actions & No Creds!
- Building Copilot agents? How Microsoft 365 Copilot Works: the Tech Stack
And if you’re just getting started with M365 development, my free guides to learning M365 app development will help you get your bearings.
What’s been your biggest Entra ID challenge as an M365 developer? I’d love to hear about it.