Skip to main content

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

  1. Go to Azure Portal -> Microsoft Entra ID
  2. Click App registrations -> New registration
  3. Fill in:
    • Name: My API (or your API name)
    • Supported account types: Choose based on your needs
    • Redirect URI: Leave blank for APIs
  4. 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

  1. Go to Expose an API
  2. Click Add next to Application ID URI
  3. Accept default or customize: api://your-client-id
  4. Click Save
  1. Still in Expose an API, click Add a scope
  2. Add scopes like:
    • api://your-client-id/read - Read access
    • api://your-client-id/write - Write access
    • api://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

PropertyRequiredDescription
NameYesFriendly name for logging
TypeYes"AzureAD"
AuthorityYeshttps://login.microsoftonline.com/<TENANT_ID>/v2.0 (or common/organizations)
IssuerYesSame as authority for v2 endpoints
AudiencesYesArray of allowed audiences (e.g., ["api://<CLIENT_ID>"])
RequireHttpsMetadataNoDefaults to true
DiagnosticsNoDev-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:

  • userId uses oid when present, otherwise sub.
  • email uses email when present, otherwise preferred_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:

  1. Go to API Permissions in Azure Portal
  2. 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 common or organizations endpoint

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 issuerMulti-Issuer Setup ->
Harden local/dev tokensLocal JWT Guide ->
Revisit basics quicklyIdentity Quick Start ->