Skip to main content

Entra ID + JWT

Overview

Microsoft Entra ID (formerly Azure Active Directory / Azure AD) is Microsoft's cloud-based Identity and Access Management (IAM) platform. It provides authentication, authorization, and identity services for web apps, APIs, and cloud-native applications.

JWT (JSON Web Token) is the industry-standard token format used by Entra ID to convey identity and authorization claims between parties.


Core Concepts

ConceptDescription
TenantYour organization's dedicated instance of Entra ID
Application RegistrationRegister your app to obtain credentials
Client IDUnique identifier for your registered app
Tenant IDYour organization's directory ID
Client Secret / CertificateApp credentials for server-to-server flows
ScopeWhat permissions the token grants
ClaimsKey-value pairs inside the JWT (user info, roles, etc.)
Access TokenShort-lived token to call APIs
ID TokenToken containing user identity information
Refresh TokenLong-lived token to obtain new access tokens

JWT Structure

A JWT consists of three Base64URL-encoded parts separated by dots:

header.payload.signature
{
"alg": "RS256",
"typ": "JWT"
}

Payload (Claims)

{
"iss": "https://login.microsoftonline.com/{tenant}/v2.0",
"sub": "abc123",
"aud": "api://your-client-id",
"exp": 1717000000,
"iat": 1716996400,
"name": "John Doe",
"preferred_username": "john@example.com",
"oid": "user-object-id",
"roles": ["Admin", "Reader"],
"scp": "User.Read"
}

Common Claims

ClaimDescription
issIssuer — Entra ID endpoint
subSubject — unique user ID (per app)
audAudience — your API's app ID
expExpiration time
oidObject ID — user's unique ID across the tenant
nameFull name
preferred_usernameEmail / UPN
rolesApp roles assigned to user
scpDelegated scopes
tidTenant ID

Authentication Flows

OAuth 2.0 / OIDC Flows

FlowUse Case
Authorization Code + PKCEWeb apps, SPAs
Client CredentialsService-to-service (no user)
On-behalf-of (OBO)API calling another API
Device CodeCLI tools, IoT devices

App Registration in Azure Portal

  1. Go to Azure Portal → Microsoft Entra ID → App registrations
  2. Click New registration, enter a name
  3. Set redirect URI (e.g., https://localhost:5001/signin-oidc)
  4. Copy Application (client) ID and Directory (tenant) ID
  5. Under Certificates & secrets, create a Client secret
  6. Under Expose an API, add a scope (e.g., api://your-client-id/access_as_user)
  7. Under App roles, define roles (e.g., Admin, Reader)

Setting Up ASP.NET Core with Entra ID

Install Package

dotnet add package Microsoft.Identity.Web

appsettings.json

{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-client-id",
"Audience": "api://your-client-id",
"CallbackPath": "/signin-oidc"
}
}

Program.cs — Protect an API

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Program.cs — Web App with Sign-In

builder.Services
.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddAuthorization();

Manual JWT Bearer (without Microsoft.Identity.Web)

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0";
options.Audience = "api://your-client-id";

options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});

Role-Based Authorization

Defining Roles in Entra ID

  1. In App Registration → App roles → Add roles (e.g., Admin, Reader)
  2. Assign roles to users or groups in Enterprise Applications → Users and groups

Using Roles in Controllers

[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
return Ok();
}

[Authorize(Roles = "Admin,Reader")]
[HttpGet]
public IActionResult GetAll()
{
return Ok();
}

Scope-Based Authorization

Require a Scope

[RequiredScope("User.Read")]
[HttpGet("profile")]
public IActionResult GetProfile()
{
return Ok();
}

Or via policy:

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadAccess", policy =>
policy.RequireScope("User.Read"));
});

Policy-Based Authorization

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));

options.AddPolicy("InternalUser", policy =>
policy.RequireClaim("tid", "your-tenant-id"));
});
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminPanel()
{
return Ok();
}

Reading Claims in a Controller

[Authorize]
[HttpGet("me")]
public IActionResult GetCurrentUser()
{
var userId = User.FindFirstValue("oid");
var name = User.FindFirstValue("name");
var email = User.FindFirstValue("preferred_username");
var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value);

return Ok(new { userId, name, email, roles });
}

Service-to-Service (Client Credentials)

Used when an API calls another API without a user context.

builder.Services.AddHttpClient<IExternalApiClient, ExternalApiClient>()
.AddHttpMessageHandler<BearerTokenHandler>();

Acquiring a Token with MSAL

var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.Build();

var result = await app.AcquireTokenForClient(
new[] { "https://graph.microsoft.com/.default" })
.ExecuteAsync();

string accessToken = result.AccessToken;

On-Behalf-Of (OBO) Flow

An API receives a token from a user and exchanges it for a token to call a downstream API.

var result = await app
.AcquireTokenOnBehalfOf(
new[] { "https://graph.microsoft.com/User.Read" },
new UserAssertion(incomingToken))
.ExecuteAsync();

Token Validation Checklist

CheckWhat to Validate
Issuer (iss)Must match your Entra ID tenant endpoint
Audience (aud)Must match your API's client ID
Expiry (exp)Token must not be expired
SignatureMust be signed by Entra ID's public keys
Tenant (tid)Optional: restrict to your tenant only

Security Best Practices

PracticeWhy
Use Managed Identity for API-to-APIEliminates client secrets
Store secrets in Key VaultNever store secrets in code or config files
Validate audience and issuerPrevent token misuse
Use short-lived access tokensReduce exposure window
Implement token cachingAvoid re-issuing tokens on every request
Use HTTPS everywherePrevent token interception
Restrict allowed tenantsPrevent cross-tenant attacks

Common Pitfalls

MistakeProblem
Not validating audienceAny Entra ID token accepted
Storing client secrets in codeSecurity breach risk
Not handling token expirySilent auth failures
Using wrong flow typeToken missing required claims
Missing scp or roles claim validationUnauthorized access

Testing Authentication

Disable Auth in Development

if (app.Environment.IsDevelopment())
{
// Optionally bypass auth
}

Mock JWT in Integration Tests

services.PostConfigure<JwtBearerOptions>(
JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.ValidateIssuer = false;
options.TokenValidationParameters.ValidateAudience = false;
options.TokenValidationParameters.IssuerSigningKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test-secret-key"));
});

Architecture Overview


Interview Questions

Beginner

  1. What is Microsoft Entra ID?
  2. What is a JWT and what does it contain?
  3. What is the difference between authentication and authorization?
  4. What is an access token vs an ID token?

Intermediate

  1. Explain the Authorization Code + PKCE flow.
  2. How do you protect an ASP.NET Core API with Entra ID?
  3. What is the client credentials flow?
  4. How do you implement role-based access with Entra ID?
  5. What claims should you validate in a JWT?

Advanced

  1. Explain the On-Behalf-Of (OBO) flow.
  2. How do you implement token caching in MSAL?
  3. How do you restrict access to a specific tenant?
  4. What is Continuous Access Evaluation (CAE)?
  5. How do you handle token refresh in a long-running background service?

Cheat Sheet

TaskCode / Config
Add JWT auth.AddMicrosoftIdentityWebApi(config.GetSection("AzureAd"))
Require auth[Authorize]
Require role[Authorize(Roles = "Admin")]
Require scope[RequiredScope("User.Read")]
Read claimUser.FindFirstValue("oid")
Client credentials tokenAcquireTokenForClient(scopes)
OBO tokenAcquireTokenOnBehalfOf(scopes, assertion)

Resources