Protect a .NET Web API with tokens
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:
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:
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:
{
"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:
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:
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:
curl -i https://localhost:5001/api/orders
# HTTP/1.1 401 Unauthorized
With a valid token obtained from your authorization server:
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.
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.