This guide shows you how to protect a web API with Microsoft Entra ID (formerly Azure AD) using Microsoft.Identity.Web.
Time to complete: ~10 minutes
- .NET 9 SDK
- A Microsoft Entra ID tenant (create a free account)
- An app registration for your API
dotnet new webapi --auth SingleOrg --name MyWebApi
cd MyWebApiUpdate appsettings.json with your app registration details:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id"
}
}dotnet runYour API is now protected at https://localhost:5001.
✅ Done! Requests now require a valid access token.
dotnet add package Microsoft.Identity.Webusing Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
var builder = WebApplication.CreateBuilder(args);
// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration, "AzureAd");
// Add authorization
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication(); // ⭐ Add authentication middleware
app.UseAuthorization();
app.MapControllers();
app.Run();{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Identity.Web": "Information"
}
}
}Require authentication for all endpoints:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[Authorize] // ⭐ Require valid access token
[ApiController]
[Route("api/[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
// Access user information
var userId = User.FindFirst("oid")?.Value;
var userName = User.Identity?.Name;
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = "Protected data"
});
}
}Require specific scopes:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class TodoController : ControllerBase
{
[HttpGet]
[RequiredScope("access_as_user")] // ⭐ Require specific scope
public IActionResult GetAll()
{
return Ok(new[] { "Todo 1", "Todo 2" });
}
[HttpPost]
[RequiredScope("write")] // ⭐ Different scope for write operations
public IActionResult Create([FromBody] string item)
{
return Created("", item);
}
}dotnet runTest with a tool like Postman or curl:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" https://localhost:5001/api/weatherforecast✅ Success! Your API now validates bearer tokens.
- Sign in to the Azure portal
- Navigate to Microsoft Entra ID > App registrations > New registration
- Enter a name (e.g., "My Web API")
- Select Single tenant (most common for APIs)
- No redirect URI needed for APIs
- Click Register
- In your API app registration, go to Expose an API
- Click Add a scope
- Accept the default Application ID URI or customize it (e.g.,
api://your-api-client-id) - Add a scope:
- Scope name:
access_as_user - Who can consent: Admins and users
- Admin consent display name: "Access My Web API"
- Admin consent description: "Allows the app to access the web API on behalf of the signed-in user"
- Scope name:
- Click Add scope
Copy the Application (client) ID - this is your ClientId in appsettings.json.
To call your API, you need a client app:
- In Microsoft Entra ID > App registrations, create another registration
- Name it (e.g., "My API Client")
- Select account types
- Add redirect URI:
https://localhost:7000/signin-oidc(if it's a web app) - Click Register
- In the client app registration, go to API permissions
- Click Add a permission > My APIs
- Select your API registration
- Check the
access_as_userscope - Click Add permissions
- Click Grant admin consent (if required)
- Go to Certificates & secrets
- Click New client secret
- Add a description and expiration
- Click Add
- Copy the secret value immediately - you won't be able to see it again
- Create a new request in Postman
- Set up OAuth 2.0 authentication:
- Grant Type: Authorization Code (for user context) or Client Credentials (for app context)
- Auth URL:
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize - Access Token URL:
https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token - Client ID: Your client app's client ID
- Client Secret: Your client app's secret
- Scope:
api://your-api-client-id/access_as_user
- Click Get New Access Token
- Use the token to call your API
// In a console app or client application
using Microsoft.Identity.Client;
var app = ConfidentialClientApplicationBuilder
.Create("client-app-id")
.WithClientSecret("client-secret")
.WithAuthority("https://login.microsoftonline.com/{tenant-id}")
.Build();
var result = await app.AcquireTokenForClient(
new[] { "api://your-api-client-id/.default" }
).ExecuteAsync();
var accessToken = result.AccessToken;
// Use the token to call your API
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync("https://localhost:5001/api/weatherforecast");Instead of using the [RequiredScope] attribute, configure required scopes globally:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "your-tenant-id",
"ClientId": "your-api-client-id",
"Scopes": "access_as_user"
}
}For multi-tenant APIs:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "common",
"ClientId": "your-api-client-id"
}
}builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi() // If your API calls other APIs
.AddInMemoryTokenCaches();Now that you have a protected API:
✅ Authorization Guide - RequiredScope attribute, authorization policies, tenant filtering ✅ Customization Guide - Configure JWT bearer options and validation parameters ✅ Logging & Diagnostics - Troubleshoot authentication issues with correlation IDs
✅ Call downstream APIs - Call Microsoft Graph or other APIs on behalf of users ✅ Configure token cache - Production cache strategies for OBO scenarios ✅ Long-running processes - Handle background jobs with OBO tokens ✅ Deploy behind API Gateway - Azure API Management, Azure Front Door, Application Gateway
Problem: API returns 401 even with a token.
Possible causes:
- Token audience (
audclaim) doesn't match your API'sClientId - Token is expired
- Token is for the wrong tenant
- Required scope is missing
Solution: Decode the token at jwt.ms and verify the claims. See Logging & Diagnostics for detailed troubleshooting.
Problem: Token signature validation fails.
Solution: Ensure your TenantId and ClientId are correct. The token must be issued by the expected authority. Enable detailed logging to see validation errors.
Problem: [RequiredScope] attribute fails.
Solution:
- Verify the client app has permission to the scope
- Ensure admin consent was granted (if required)
- See Authorization Guide for complete scope validation patterns
- Check that the scope is requested when acquiring the token (e.g.,
api://your-api/.defaultor specific scopes)
See more: Web API Troubleshooting Guide