The client credentials flow is the correct OAuth 2.0 choice when one application must call an API as itself, with no signed-in user. In Microsoft Entra ID, the caller receives an app-only access token and the API authorizes it through an application permission represented by an app role.

This guide refreshes the original October 2024 .NET 8 demonstration. The original repository, screenshots, and video remain useful historical evidence, but the demo was not rebuilt or re-run in 2026. The production guidance below therefore separates the original client-secret example from the safer credential options recommended for current deployments.

What the client credentials flow proves

A successful token proves the identity of a confidential client application. It does not represent a person, and it does not carry delegated user permissions. That distinction determines how the API must authorize the request:

  • The token should contain an application role in its roles claim.
  • The API must require the expected role, not merely accept any valid token.
  • The caller requests the resource’s .default scope, which means the application permissions already configured for that resource.
  • An administrator grants consent for the application permission before the caller can use it.

Use this flow for background workers, scheduled jobs, daemons, integration services, and service-to-service API calls. If a user must be represented, use a delegated flow instead.

Flow and trust boundary

Client credentials flow between a confidential client, Microsoft Entra ID, and a protected API
The original flow diagram: the caller authenticates to Microsoft Entra ID, receives an app-only token, and presents it to the API.
  1. The calling application authenticates to the Microsoft identity platform using a credential.
  2. It requests a token for the API using api://YOUR_API_CLIENT_ID/.default.
  3. Microsoft Entra ID issues a token containing the application roles granted to that caller.
  4. The API validates the issuer, audience, signature, lifetime, and required role.

The credential is the most sensitive part of the caller. A leaked secret allows another process to impersonate the application until that credential expires or is revoked. Do not place secrets in source control, screenshots, container images, or application settings committed with the code.

Register the API and define an application role

Create an app registration for the protected API. Under Expose an API, set an Application ID URI such as api://YOUR_API_CLIENT_ID. Then add an app role intended for applications:

  • Display name: Access the API as an application
  • Allowed member types: Applications
  • Value: access_as_app
  • Description: Allows the calling service to access the protected API

Keep roles narrow. A single broad role such as “full access” makes later reviews and incident response harder. Define separate roles when callers have genuinely different responsibilities.

Register the caller and grant application permission

Create a second app registration for the worker or console client. In API permissions, add a permission for the protected API, choose Application permissions, and select access_as_app. An administrator must then grant tenant-wide consent.

This is not the same as a delegated permission. Application permissions run without a user and can be powerful, so record who approved them, why they are required, and when they should be reviewed.

Choose the credential before writing code

The original demo uses a client secret because it is easy to show locally. That remains acceptable for a short-lived development exercise when the value is stored outside the repository. For production, prefer a credential that reduces secret handling:

  • Managed identity: best when the workload runs on a supported Azure resource.
  • Workload identity federation: useful for supported external workloads and CI/CD systems because no long-lived secret must be stored.
  • Certificate: preferable to a client secret when managed identity or federation is unavailable.
  • Client secret: use only when the stronger options are impractical; keep it short-lived and rotate it.

For local development, store a temporary secret with .NET user secrets rather than in appsettings.json:

dotnet user-secrets init
dotnet user-secrets set "EntraId:ClientSecret" "LOCAL_DEVELOPMENT_SECRET"

Configure the protected .NET 8 API

Use placeholders in configuration and supply environment-specific values through your deployment platform. The API client ID is not a secret, but keeping environments separate prevents accidental cross-environment trust.

{
  "EntraId": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "YOUR_TENANT_ID",
    "ClientId": "YOUR_API_CLIENT_ID",
    "Audience": "api://YOUR_API_CLIENT_ID"
  }
}

Add Microsoft.Identity.Web and configure bearer-token validation plus a policy that requires the application role:

dotnet add package Microsoft.Identity.Web
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;

var builder = WebApplication.CreateBuilder(args);

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

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AccessAsApplication", policy =>
        policy.RequireRole("access_as_app"));
});

builder.Services.AddControllers();

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

Protect the endpoint with the policy. This rejects a valid token that lacks the required application role:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/employees")]
public sealed class EmployeesController : ControllerBase
{
    [HttpGet]
    [Authorize(Policy = "AccessAsApplication")]
    public IActionResult Get() => Ok(new[]
    {
        new { Id = 1, Name = "Ada" },
        new { Id = 2, Name = "Grace" }
    });
}

Acquire an app-only token with MSAL.NET

The following client-secret version matches the historical learning path. Treat it as a local-development example, not the recommended production credential.

dotnet add package Microsoft.Identity.Client
using Microsoft.Identity.Client;
using System.Net.Http.Headers;

var tenantId = configuration["EntraId:TenantId"]
    ?? throw new InvalidOperationException("Tenant ID is missing.");
var clientId = configuration["EntraId:ClientId"]
    ?? throw new InvalidOperationException("Client ID is missing.");
var apiClientId = configuration["EntraId:ApiClientId"]
    ?? throw new InvalidOperationException("API client ID is missing.");
var clientSecret = configuration["EntraId:ClientSecret"]
    ?? throw new InvalidOperationException("Client secret is missing.");

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

var token = await clientApp
    .AcquireTokenForClient(new[] { $"api://{apiClientId}/.default" })
    .ExecuteAsync();

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

var response = await http.GetAsync("https://localhost:7001/api/employees");
Console.WriteLine($"{(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine(await response.Content.ReadAsStringAsync());

In a production workload, replace WithClientSecret with the credential mechanism chosen for that environment. The API contract and role authorization remain the same.

Roles, scopes, and the .default request

One of the easiest mistakes is checking the wrong claim. Delegated user tokens normally describe permissions in scp. App-only tokens describe application permissions in roles. A client credentials API should therefore require the role value it exposed and that an administrator granted.

The .default suffix does not ask for every permission in the tenant. It asks Microsoft Entra ID to issue the application permissions already configured and consented for that resource. If the role is missing, check the permission type, admin consent, resource identifier, and token claims before changing code.

Verify success and failure paths

A successful HTTP 200 response is only the first test. Before release, verify each boundary deliberately:

  • No token: the API returns 401.
  • Wrong audience or tenant: the API returns 401.
  • Valid token without access_as_app: the API returns 403.
  • Correct role: the API returns the expected result.
  • Expired or revoked credential: token acquisition fails and monitoring reports it.
  • Rotation: the new credential works before the old credential is removed.

Log correlation IDs, status codes, tenant and client identifiers, and the authorization outcome. Never log access tokens, client secrets, assertion payloads, or certificate private keys.

Original .NET 8 demonstration

The original October 2024 repository contains the Web API and console client used for this article. It remains available as a historical companion: Client Credentials Flow in Entra ID on GitHub.

Original .NET 8 demonstration recorded in October 2024. It shows the working process at that time and was not re-run in 2026.
Original .NET 8 client credentials flow demonstration result
The original console client calling the protected API successfully.

Production checklist

  • Use an app-only flow only when no user identity is required.
  • Expose a narrow application role and require it in the API.
  • Grant application permission explicitly and record admin consent.
  • Prefer managed identity, workload identity federation, or a certificate over a secret.
  • Keep every credential out of source control and rotate it safely.
  • Validate issuer, audience, signature, lifetime, and role.
  • Test 401, 403, credential-expiry, and rotation scenarios.

The durable design is simple: authenticate the workload, authorize the exact app role, minimize credential exposure, and test failure paths as carefully as the success path.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO