How to protect a .NET Web API with tokens

Protect a .NET Web API with tokens

June 9, 2026

From concept to implementation — using OpenIddict introspection

You have a .NET Web API. You want to make sure that only authenticated clients — applications or users with a valid token — can call it. How do you set that up correctly, and what does "correctly" actually mean in a production context?

This article walks through the full setup: how token validation works, why introspection is the right approach when using OpenIddict, and what the actual code looks like in a minimal API project.

The flow in plain terms

When a client wants to call your API, it first obtains a token from an authorization server — a dedicated service responsible for authenticating users and issuing tokens. The client then includes that token in every API request, inside the Authorization header:

copy

GET /api/orders HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
        

Your API receives the request and needs to answer one question: is this token valid? If yes, the request proceeds. If not, the API returns a 401 Unauthorized response.

How the API answers that question is where the implementation choices matter. If you want to understand the authorization server side of this flow in depth, the OpenIddict mini-series covers it in detail.

Local validation vs introspection

There are two ways an API can validate a token. The first is local validation: the API fetches the authorization server's public key and verifies the token's signature locally, without any network call. This is fast, but it has a blind spot — if a token has been revoked before it expires, the API has no way to know.

The second approach is token introspection. For each request, the API calls the authorization server's introspection endpoint and asks: "is this token still active?" The server checks its database and responds with the token's current status and its claims. This adds a network call per request, but it means revoked tokens are detected immediately — which is the correct behavior in a production system where security incidents can happen.

When your authorization server is OpenIddict — or IdentitySuite, which is built on it — introspection is the recommended approach. OpenIddict's validation package integrates directly with the introspection endpoint and handles all the plumbing for you.

💡 Performance consideration

The introspection call adds latency to every request. In practice, this is mitigated by keeping the authorization server close to your API (same network, same datacenter) and by using short token lifetimes — which reduce the window during which a compromised token could be used anyway.

Step 0 — register your API as a resource

Before configuring your API, you need to register it as an API resource in your authorization server. This tells the server which applications are allowed to introspect tokens, and which scopes are associated with your API.

In IdentitySuite, this is done through the admin UI — no code required. You can find the full walkthrough in the IdentitySuite documentation. Once your API resource is registered, you will have a client ID and secret to use in the next steps.

Step 1 — add the NuGet package

OpenIddict's validation package for ASP.NET Core is all you need on the API side:

copy

dotnet add package OpenIddict.AspNetCore
        

Step 2 — configure appsettings.json

Add a section for the authorization server settings. The Authority is the base URL of your authorization server — IdentitySuite in this case. The ClientId and ClientSecret are the credentials of the API resource you registered in Step 0:

copy

{
  "AuthorizationServer": {
    "Authority": "https://your-identitysuite-instance.com",
    "ClientId": "your-api-resource-id",
    "ClientSecret": "your-api-resource-secret"
  }
}
        

Step 3 — configure Program.cs

Register OpenIddict's validation services and point them at your authorization server. The UseIntrospection call is what switches from local JWT validation to server-side introspection:

copy

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenIddict()
    .AddValidation(options =>
    {
        options.SetIssuer(builder.Configuration["AuthorizationServer:Authority"]!);
        options.AddAudiences(builder.Configuration["AuthorizationServer:ClientId"]!);

        options.UseIntrospection()
               .SetClientId(builder.Configuration["AuthorizationServer:ClientId"]!)
               .SetClientSecret(builder.Configuration["AuthorizationServer:ClientSecret"]!);

        options.UseSystemNetHttp();
        options.UseAspNetCore();
    });

builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapGet("/api/orders", () => Results.Ok("protected data"))
   .RequireAuthorization();

app.Run();
        
⚠️ Order matters

UseAuthentication() must always come before UseAuthorization() in the middleware pipeline. Inverting the order means authorization runs before the identity is established — and every request will be rejected.

Step 4 — reading claims from the token

Once authentication is set up, the claims from the introspected token are available through HttpContext.User in any endpoint. Here is how to read the subject and a custom claim:

copy

app.MapGet("/api/profile", (HttpContext context) =>
{
    var userId = context.User.FindFirst(OpenIddictConstants.Claims.Subject)?.Value;
    var email  = context.User.FindFirst(OpenIddictConstants.Claims.Email)?.Value;

    return Results.Ok(new { userId, email });
})
.RequireAuthorization();
        

The claims available in HttpContext.User are exactly those returned by the introspection endpoint — which in turn reflects what was included in the token when it was issued. In IdentitySuite, you control which claims are included per client from the admin UI.

Step 5 — testing the protection

A quick way to verify the setup is working correctly is to call the endpoint with and without a token. Without a token, you should get a 401 Unauthorized:

copy

curl -i https://localhost:5001/api/orders
# HTTP/1.1 401 Unauthorized
        

With a valid token obtained from your authorization server:

copy

curl -i https://localhost:5001/api/orders \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
# HTTP/1.1 200 OK
        

If you get a 401 even with a token, the most common causes are a mismatched audience, an incorrect authority URL, or wrong API resource credentials. The OpenIddict validation middleware logs detailed error information — checking the application logs is usually the fastest way to diagnose the issue.

The authorization server side — IdentitySuite

The API configuration above assumes you have an OpenIddict-compatible authorization server running. Setting one up correctly from scratch — certificates, token lifetimes, client registrations, scope definitions — requires significant work, as covered in the OpenIddict configuration pitfalls article.

IdentitySuite is an all-inclusive solution built on OpenIddict and ASP.NET Core Identity that handles all of that out of the box. Your authorization server is up and running with three method calls, and everything else — client registration, API resources, token configuration — is managed through the admin UI without touching code. The API configuration shown in this article works with IdentitySuite without any additional setup.

Share this article

Found this helpful? Share it with your team

Logo

About IdentitySuite

IdentitySuite simplifies enterprise authentication for .NET developers. Built on proven technologies like ASP.NET Core Identity and Openiddict, we eliminate the complexity of OAuth 2.0 and OpenID Connect implementation while maintaining enterprise-grade security standards.