Azure AD / Entra ID Integration Guide
See also: Identity Validator Overview for package versions, OIDC/JWT options, and Node.js + .NET entrypoints.
Complete guide to integrating Microsoft Entra ID (formerly Azure AD) authentication with your .NET API using Primus Identity Validator.
Prerequisites
- Azure subscription with Entra ID tenant
- .NET 6.0+ project
- Permissions to register applications in Azure AD
Step 1: Install Package
dotnet add package PrimusSaaS.Identity.Validator
Step 2: Register Your API in Azure Portal
Create App Registration
- Go to Azure Portal -> Microsoft Entra ID
- Click App registrations -> New registration
- Fill in:
- Name:
My API(or your API name) - Supported account types: Choose based on your needs
- Redirect URI: Leave blank for APIs
- Name:
- Click Register
Note Your Values
After registration, note down:
- Application (client) ID:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - Directory (tenant) ID:
yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
Expose an API
- Go to Expose an API
- Click Add next to Application ID URI
- Accept default or customize:
api://your-client-id - Click Save
Add Scopes (Optional but Recommended)
- Still in Expose an API, click Add a scope
- Add scopes like:
api://your-client-id/read- Read accessapi://your-client-id/write- Write accessapi://your-client-id/admin- Admin access
Step 3: Configure appsettings.json
{
"PrimusIdentity": {
"RequireHttpsMetadata": true,
"ValidateLifetime": true,
"Issuers": [
{
"Name": "AzureAD",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"Issuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"Audiences": ["api://<CLIENT_ID>"]
}
],
"Diagnostics": {
"EnableDetailedErrors": true,
"IncludeTokenHintsInChallenges": true,
"IncludeDebugHeaders": true,
"LogTokenRejectionReasons": true,
"MaxRecentFailures": 50,
"AutoDetectDevelopment": true
}
}
}
Alternative: Multi-tenant (Azure AD common)
{
"PrimusIdentity": {
"RequireHttpsMetadata": true,
"Issuers": [
{
"Name": "AzureAD-Common",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/common/v2.0",
"Issuer": "https://login.microsoftonline.com/common/v2.0",
"Audiences": ["api://<CLIENT_ID>"]
}
],
"Diagnostics": {
"EnableDetailedErrors": true,
"IncludeTokenHintsInChallenges": true,
"IncludeDebugHeaders": true,
"LogTokenRejectionReasons": true,
"MaxRecentFailures": 50,
"AutoDetectDevelopment": true
}
}
}
Configuration Reference
| Property | Required | Description |
|---|---|---|
Name | Yes | Friendly name for logging |
Type | Yes | "AzureAD" |
Authority | Yes | https://login.microsoftonline.com/<TENANT_ID>/v2.0 (or common/organizations) |
Issuer | Yes | Same as authority for v2 endpoints |
Audiences | Yes | Array of allowed audiences (e.g., ["api://<CLIENT_ID>"]) |
RequireHttpsMetadata | No | Defaults to true |
Diagnostics | No | Dev-only diagnostics settings |
Step 4: Complete Program.cs
using PrimusSaaS.Identity.Validator;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
// ========================================
// Register Primus Identity for Azure AD
// ========================================
builder.Services.AddPrimusIdentity(opts =>
builder.Configuration.GetSection("PrimusIdentity").Bind(opts));
builder.Services.AddControllers();
var app = builder.Build();
// ========================================
// Middleware Pipeline (ORDER MATTERS!)
// ========================================
app.UseHttpsRedirection();
app.UseAuthentication(); // Must come before Authorization
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 5: Create a Protected Controller
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpGet("public")]
public IActionResult GetPublic() => Ok(new { message = "Public access" });
[Authorize]
[HttpGet("me")]
public IActionResult GetProfile() => Ok(new
{
objectId = User.FindFirst("oid")?.Value,
upn = User.FindFirst("upn")?.Value,
name = User.FindFirst("name")?.Value,
email = User.FindFirst("preferred_username")?.Value,
tenantId = User.FindFirst("tid")?.Value
});
}
Step 6: Get a Test Token
-
Azure CLI (service-to-service):
az account get-access-token --resource api://YOUR-CLIENT-ID --query accessToken -o tsv -
Client credentials (curl):
curl -X POST https://login.microsoftonline.com/YOUR-TENANT-ID/oauth2/v2.0/token -H "Content-Type: application/x-www-form-urlencoded" -d "client_id=YOUR-CLIENT-APP-ID" -d "client_secret=YOUR-CLIENT-SECRET" -d "scope=api://YOUR-API-CLIENT-ID/.default" -d "grant_type=client_credentials"
Step 7: Test Your API
# Start your API
dotnet run
# Test public endpoint
curl http://localhost:5000/api/user/public
# Test protected endpoint (should return 401)
curl http://localhost:5000/api/user/me
# Test with valid Azure AD token
curl http://localhost:5000/api/user/me \
-H "Authorization: Bearer YOUR-AZURE-AD-TOKEN"
Example API (Identity Validator Example .zip)
The example zip includes a minimal API with Azure AD validation.
GET /status
Headers: none
Response 200:
{
"packageInstalled": true,
"packageName": "PrimusSaaS.Identity.Validator",
"issuers": [
{ "name": "AzureAD", "type": "AzureAD", "configured": true, "details": "Configured" }
],
"message": "Identity Validator is installed and configured",
"nextSteps": "Test endpoints: /local (easiest), /auth0, /azuread"
}
GET /azuread
Headers: Authorization: Bearer <JWT>
Response 200:
{
"provider": "AzureAD",
"userId": "00000000-0000-0000-0000-000000000000",
"email": "user@company.com",
"validated": true,
"message": "Azure AD token validated successfully"
}
Notes:
userIdusesoidwhen present, otherwisesub.emailusesemailwhen present, otherwisepreferred_username.
Working Example: Full API with Azure AD
using PrimusSaaS.Identity.Validator;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Configure Azure AD authentication
builder.Services.AddPrimusIdentity(opts =>
builder.Configuration.GetSection("PrimusIdentity").Bind(opts));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthentication();
app.UseAuthorization();
// Health check
app.MapGet("/", () => new { status = "healthy", auth = "Azure AD" });
// Get current user info
app.MapGet("/me", [Authorize] (HttpContext ctx) => new {
objectId = ctx.User.FindFirst("oid")?.Value,
name = ctx.User.FindFirst("name")?.Value,
email = ctx.User.FindFirst("preferred_username")?.Value,
tenantId = ctx.User.FindFirst("tid")?.Value
});
app.Run();
appsettings.json
{
"PrimusIdentity": {
"RequireHttpsMetadata": true,
"ValidateLifetime": true,
"Issuers": [
{
"Name": "AzureAD",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"Issuer": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"Audiences": ["api://<CLIENT_ID>"]
}
],
"Diagnostics": {
"EnableDetailedErrors": true,
"IncludeTokenHintsInChallenges": true,
"IncludeDebugHeaders": true,
"LogTokenRejectionReasons": true,
"MaxRecentFailures": 50,
"AutoDetectDevelopment": true
}
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Troubleshooting
Error: "AADSTS50011: Reply URL mismatch"
Cause: Redirect URI mismatch (for auth code flow).
Solution: Add correct redirect URI in app registration.
Error: "AADSTS700016: Application not found"
Cause: Client ID doesn't exist in tenant.
Solution: Verify Client ID and Tenant ID are correct.
Error: "AADSTS65001: User or admin hasn't consented"
Cause: Permissions not granted.
Solution:
- Go to API Permissions in Azure Portal
- Click "Grant admin consent for [Tenant]"
Error: "IDX10205: Issuer validation failed"
Cause: Token from different tenant.
Solution:
- Check tenant ID matches
- For multi-tenant: use
commonororganizationsendpoint
Debug: Check token contents
# Decode JWT payload (base64)
echo "YOUR_JWT_PAYLOAD" | base64 -d | jq
Or use jwt.ms to inspect tokens.
Multi-Tenant Configuration
Primus Identity validates the iss claim against configured issuers. For multi-tenant apps, configure one issuer per tenant you allow.
{
"PrimusIdentity": {
"Issuers": [
{
"Name": "AzureAD-TenantA",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/TENANT-A/v2.0",
"Issuer": "https://login.microsoftonline.com/TENANT-A/v2.0",
"Audiences": ["api://YOUR-CLIENT-ID"]
},
{
"Name": "AzureAD-TenantB",
"Type": "AzureAD",
"Authority": "https://login.microsoftonline.com/TENANT-B/v2.0",
"Issuer": "https://login.microsoftonline.com/TENANT-B/v2.0",
"Audiences": ["api://YOUR-CLIENT-ID"]
}
]
}
}
Security note: If you allow multiple tenants, validate tenant access in your app:
var tenantId = User.FindFirst("tid")?.Value;
var allowedTenants = new[] { "tenant-1-id", "tenant-2-id" };
if (!allowedTenants.Contains(tenantId))
{
return Forbid();
}
Next Steps
| Want to... | See Guide |
|---|---|
| Add Auth0 as second issuer | Multi-Issuer Setup -> |
| Harden local/dev tokens | Local JWT Guide -> |
| Revisit basics quickly | Identity Quick Start -> |