Skip to main content

🚀 Onboard a New API in Microsoft Entra and Consume It — with Full Examples

This article shows how to:

  1. Register your API in Microsoft Entra (Azure AD) and expose a scope.
  2. Register clients (daemon and web client) and grant permissions.
  3. Protect a .NET Web API (ASP.NET Core) using Microsoft.Identity.Web.
  4. Call the API from:
    • a daemon/service using client credentials
    • a backend that performs On-Behalf-Of (OBO) to call a downstream API

Replace placeholders like YOUR_TENANT_ID, API_CLIENT_ID, CLIENT_APP_ID, and CLIENT_SECRET with actual values from the portal.


✳️ Portal steps (brief)

  1. Register API (MyProductApi)

    • Azure Portal → Microsoft Entra ID → App registrations → New registration → MyProductApi
    • Note: Application (client) IDAPI_CLIENT_ID, Directory (tenant) IDYOUR_TENANT_ID
  2. Expose an API

    • App → Expose an API → Set Application ID URI to api://{API_CLIENT_ID} (default) or https://contoso.com/my-api
    • Add a scope:
      • Scope name: access_as_user
      • Admin consent display name: Access MyProductApi as user
      • Who can consent: Admins and users
      • Description: Allow the app to access MyProductApi on your behalf.
  3. Register Client 1 (Daemon)MyDaemonClient

    • New registration → note Client ID DAEMON_CLIENT_ID
    • Certificates & secrets → create Client secretDAEMON_CLIENT_SECRET
    • API permissions → Add permission → MyProductApi → Application permissions → pick the app-permission you added (or use .default on token call)
    • Grant admin consent (tenant admin)
  4. Register Client 2 (Web App)MyWebClient

    • New registration → note Client ID WEB_CLIENT_ID
    • Configure platform (Web) with Redirect URI if you will do interactive auth.
    • API permissions → Add permission → MyProductApi → Delegated permissionsaccess_as_user
    • (No admin consent if user consent allowed; otherwise admin consent)

1) Protected ASP.NET Core Web API — MyProductApi

Project: MyProductApi (ASP.NET Core 7+)

Add packages

dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.MicrosoftGraph  # optional if calling MS Graph later

appsettings.json

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "API_CLIENT_ID",
    "Audience": "api://API_CLIENT_ID"
  },
  "Logging": { "LogLevel": { "Default": "Information" } }
}

Program.cs

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.Resource;

var builder = WebApplication.CreateBuilder(args);

// Add Microsoft.Identity.Web (validates incoming JWT access tokens)
builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration.GetSection("AzureAd"));

// Optional: easier scope validation helper
string[] scopeRequiredByApi = new string[] { "access_as_user" };

builder.Services.AddAuthorization();

builder.Services.AddControllers();

var app = builder.Build();

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

app.MapControllers();

app.Run();

ProductsController.cs (validate scope inside action)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web.Resource;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Use ResourceValidation helpers (Microsoft.Identity.Web.Resource)
    private static readonly string[] scopeRequired = new string[] { "access_as_user" };

    [HttpGet]
    public IActionResult Get()
    {
        // Will throw 403 if scope not present
        HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequired);

        var products = new[] {
            new { Id = 1, Name = "Product A" },
            new { Id = 2, Name = "Product B" }
        };
        return Ok(products);
    }
}

Notes

  • VerifyUserHasAnyAcceptedScope reads scp claim in token and returns 403 if missing.
  • Audience / ClientId must match token aud (token audience). If you used custom Application ID URI, ensure aud matches that value.

2) Example A — Daemon / Service: Client Credentials flow (no user)

This example demonstrates a background service calling the API using client credentials. This flow uses application permissions (app-only).

Daemon console app (or any service)

Add NuGet

dotnet add package Microsoft.Identity.Client

Program.cs

using System.Net.Http.Headers;
using Microsoft.Identity.Client;

string tenantId = "YOUR_TENANT_ID";
string clientId = "DAEMON_CLIENT_ID";
string clientSecret = "DAEMON_CLIENT_SECRET"; // store securely in Key Vault in production
string apiScope = "api://API_CLIENT_ID/.default"; // use .default to request all app permissions

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

var result = await cca.AcquireTokenForClient(new string[] { apiScope }).ExecuteAsync();
Console.WriteLine("Access token acquired");

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);

var resp = await http.GetAsync("https://localhost:5001/api/products");
if (resp.IsSuccessStatusCode)
{
    var content = await resp.Content.ReadAsStringAsync();
    Console.WriteLine(content);
}
else
{
    Console.WriteLine($"Call failed: {resp.StatusCode} - {await resp.Content.ReadAsStringAsync()}");
}

Important

  • The API must support app-only calls (check roles or accept tokens with no scp). If you designed the API only for delegated scopes, add application roles in the API registration and verify roles claim in the API if required.

3) Example B — On-Behalf-Of (Frontend → API A → Downstream API B)

Scenario:

  • MyWebClient obtains a token for MyProductApi with scope api://API_CLIENT_ID/access_as_user.
  • Frontend calls MyProductApi passing that token.
  • MyProductApi needs to call DownstreamApi (e.g., another API or Microsoft Graph) on behalf of the signed-in user, using OBO.

Prerequisites

  • MyWebClient requests scope for MyProductApi.
  • MyProductApi registration must be configured as a confidential client (have a client secret or certificate).
  • MyProductApi must be granted permission to call the downstream API (as delegated permission) in the portal.

Add packages

dotnet add package Microsoft.Identity.Web
dotnet add package Microsoft.Identity.Web.MicrosoftGraph  // optional

appsettings.json (for MyProductApi)

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "API_CLIENT_ID",
    "ClientSecret": "API_CLIENT_SECRET",   // secret for OBO; use Key Vault in production
    "Domain": "yourtenant.onmicrosoft.com"
  }
}

Program.cs (register token acquisition service)

using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration.GetSection("AzureAd"));

// Register ITokenAcquisition to use AcquireTokenOnBehalfOf
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();

ProductsController.cs (OBO call)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;
using System.Net.Http.Headers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ITokenAcquisition _tokenAcquisition;
    private readonly HttpClient _http;

    public ProductsController(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpFactory)
    {
        _tokenAcquisition = tokenAcquisition;
        _http = httpFactory.CreateClient();
    }

    [HttpGet("with-downstream")]
    public async Task<IActionResult> GetProductsWithDownstreamCall()
    {
        // Verify caller has required scope
        HttpContext.VerifyUserHasAnyAcceptedScope(new[] { "access_as_user" });

        // Scopes for downstream API (example)
        string[] scopesForDownstream = new[] { "api://DOWNSTREAM_API_CLIENT_ID/access_as_user" };

        // Acquire token on behalf of the user (OBO)
        string accessTokenForDownstream = await _tokenAcquisition.GetAccessTokenForUserAsync(scopesForDownstream);

        // Call downstream API
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessTokenForDownstream);
        var response = await _http.GetAsync("https://downstream-api.example.com/api/values");

        var downstreamContent = await response.Content.ReadAsStringAsync();

        return Ok(new
        {
            products = new[] { "Product A", "Product B" },
            downstream = new { status = response.StatusCode, content = downstreamContent }
        });
    }
}

Notes

  • ITokenAcquisition (from Microsoft.Identity.Web) handles the OBO exchange. It uses the incoming user assertion (the access token sent to MyProductApi) and your API's confidential client credentials to obtain a new token for the downstream API.
  • The downstream API must trust tokens where aud matches its Application ID URI.

Troubleshooting checklist (quick)

  • Token aud mismatch → check Application ID URI and what the client requested.
  • Missing scp → ensure client requested api://API_CLIENT_ID/access_as_user.
  • OBO fails with AADSTS70011 or 401 → ensure API is configured with client secret/cert and the incoming token is an access token (not an id_token).
  • 403 from API → token does not contain required scope/role or VerifyUserHasAnyAcceptedScope blocked it. Inspect token with jwt.ms to confirm scp, roles, aud, iss.
  • Use Azure AD sign-in logs for troubleshooting authentication failures.

Security best practices (short)

  • Store secrets in Key Vault and use Managed Identity where possible.
  • Use certificates (preferred) instead of client secrets for confidential clients.
  • Validate scopes/roles on every request.
  • Apply least privilege to scopes and permissions.
  • Monitor and rotate credentials regularly.


Comments

Popular posts from this blog

🏗️ Deep Dive: Understanding Every Concept in Microsoft Entra API Onboarding for .NET Developers

When working with Microsoft Entra (formerly Azure Active Directory), you’ll hear terms like App Registration, Tenant, Client ID, Audience, Scopes, Roles, Tokens, OBO flow , and more. If you’re new, it can feel overwhelming. This guide breaks down every key term and concept , with definitions, examples, and how they connect when you onboard and consume a new API. 🔹 1. Tenant Definition : A tenant in Entra ID is your organization’s dedicated, isolated instance of Microsoft Entra. Think of it like : Your company’s identity directory. Example : contoso.onmicrosoft.com is a tenant for Contoso Ltd. 🔹 2. App Registration Definition : The process of registering an application in Entra to give it an identity and permission to use Microsoft identity platform. Why needed : Without registration, Entra doesn’t know about your app. What it creates : Application (Client) ID – unique identifier for your app Directory (Tenant) ID – your organization’s ID Types of apps : Web ...

🗑️ Garbage Collection & Resource Management in .NET (C#) — Beginner Friendly Guide

When you start working with .NET and C#, one of the biggest advantages is that you don’t need to manually manage memory like in C or C++. The Garbage Collector (GC) does most of the work for you. But here’s the catch — not everything is managed automatically. Some resources like files, database connections, sockets, and native memory still need special handling. This blog will help you understand: ✔ How the GC works ✔ What are managed vs unmanaged resources ✔ The difference between Dispose , Finalize , and using ✔ The Dispose pattern with examples ✔ Best practices every C# developer should know 1) How Garbage Collection Works in .NET Managed resources → Normal .NET objects (string, List, etc.). GC frees them automatically. Unmanaged resources → External resources like file handles, database connections, sockets, native memory. GC cannot clean them up — you must do it. 👉 GC uses a Generational Model for performance: Gen 0 : Short-lived objects (local variables, t...

☁️ Azure Key vault Short Notes

🟢 What is Azure Key Vault? A cloud service for securely storing and accessing secrets, keys, and certificates . Removes the need to keep secrets (like connection strings, passwords, API keys) inside code or config files. Provides centralized secret management, encryption, and access control . 👉 Think of it like a secure password manager but for your applications. 🟢 Key Features Secrets → store text values (e.g., DB connection string, API key). Keys → store cryptographic keys (RSA, EC) for encryption, signing. Certificates → store/manage SSL/TLS certificates. Access Control → Access Policies (older model). Azure RBAC (modern, preferred). Integration → works with App Service, Functions, AKS, VMs, SQL DB, etc. Logging → audit who accessed secrets via Azure Monitor / Diagnostic Logs. 🟢 Why Use Key Vault? Security → secrets are encrypted with HSM (Hardware Security Modules). Compliance → meet industry standards (PCI-DSS, ISO, GDPR). Automation → aut...