Knowing who sent a request and deciding what they may do are separate decisions. Authentication establishes a principal under a trusted scheme. Authorization checks that principal against a policy and, when necessary, a specific resource. A valid token does not automatically permit every endpoint, and a user-supplied owner identifier does not prove ownership.
This guide uses .NET 10 and ASP.NET Core 10. The main console fixture evaluates real authorization policies using explicitly constructed test principals. Those principals are test inputs, not authenticated credentials. The separate API configuration shows where real JWT validation belongs. You will distinguish 401 from 403 and implement a coded resource-ownership check rather than trusting a route parameter.
Authentication must validate evidence
A bearer-token handler validates tokens according to its configured trust contract. Important checks include signature, issuer, audience and lifetime. Merely decoding a JWT's payload does not establish any of these facts. Never accept a caller's arbitrary header or query parameter as a verified user identity.
An API should obtain trusted issuer and audience settings from its configuration and use the supported handler. Real identity issuance belongs to an appropriate identity provider or authentication system, not a tutorial endpoint that manufactures tokens from any supplied username. Use HTTPS for credentials in transit and avoid logging the token. Claims are trustworthy only after the relevant authentication boundary validates their source and meaning.
Authorization policies make permissions explicit
A policy can require an authenticated user and specific claims or requirements. Apply it to the endpoint or endpoint group that needs protection. A permissive anonymous route should be an intentional exception, especially when the application uses a fallback policy. Adding authentication services alone does not automatically secure every route.
Claims describe facts under an issuer's contract. A claim named permission with value orders.read can be useful if the trusted issuer actually defines it. An OAuth scope claim often contains a space-separated list, so a simple exact-value requirement may not implement the intended scope semantics. Parse and evaluate that contract deliberately. A role is another authorization concept, but broad roles do not replace ownership checks on individual records.
Configure the real API boundary
For an actual minimal API, use a Web SDK project targeting net10.0 and reference Microsoft.AspNetCore.Authentication.JwtBearer version 10.0.0 or an appropriate supported compatible patch. The following complete Program.cs requires Auth:Authority and Auth:Audience settings for a real trusted provider. Missing settings fail clearly instead of enabling a permissive fallback.
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
string authority = builder.Configuration["Auth:Authority"]
?? throw new InvalidOperationException("Auth authority is required.");
string audience = builder.Configuration["Auth:Audience"]
?? throw new InvalidOperationException("Auth audience is required.");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = authority;
options.Audience = audience;
options.RequireHttpsMetadata = true;
options.MapInboundClaims = false;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ReadOrders", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "orders.read"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/orders", () => Results.Ok(new[] { "order-7" }))
.RequireAuthorization("ReadOrders");
app.Run();Without an acceptable bearer credential: HTTP 401.
With a valid authenticated token lacking orders.read: HTTP 403.
With a valid token satisfying ReadOrders: HTTP 200 and ["order-7"].These outcomes depend on the configured issuer, its claim contract and valid credentials. They are not tokens supplied by the console fixture. The handler performs the authentication work; the endpoint applies the permission policy afterward.
Resource ownership belongs on the server
An endpoint-level permission can allow access to the feature while a resource handler checks the particular order. Load the record under a safe query, identify its owner from trusted stored data, and compare it with a stable validated subject identifier. Do not accept an owner ID from the request body as evidence that the caller owns the record.
Decide whether a missing or unauthorized resource should return 404 or 403 under the application's disclosure policy. Both cases still require correct internal authorization. Keep the authorization and mutation within a coherent operation so a later change does not bypass the checked ownership assumption. Database constraints and concurrency rules can matter alongside the permission check.
Test the boundaries independently
Policy tests verify permissions under controlled principals. Authentication integration tests verify the configured handler's acceptance and rejection of credentials. Endpoint tests verify that protected routes actually apply their policy. Resource tests verify ownership, missing records and cross-user access. Each layer catches a different failure. Keep the protected-route inventory reviewable as endpoints are added, and verify that anonymous exceptions are intentional.
For cookie-based browser authentication, review CSRF protection for state-changing requests because browsers attach cookies automatically. Bearer APIs have different transport behavior but still need safe CORS and credential handling. Do not treat CORS as authorization. Keep logs limited to safe outcome and correlation fields, and never disable token validation merely to make a failing test pass.
Official references
C# ASP.NET Core Authentication and Authorization Examples
Each example shows the code, the output it produces and what each part does.
Evaluate a policy without an external identity service
Use this project file and complete program. It tests the authorization layer only. The nonempty authentication type marks an identity as authenticated for the fixture; it does not perform token validation.
The first principal is anonymous. The second is authenticated under the test fixture but lacks permission. The third satisfies both requirements. This isolates policy logic from network and identity-provider behavior.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorizationCore(options =>
{
options.AddPolicy("ReadOrders", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "orders.read"));
});
using var provider = services.BuildServiceProvider();
var authorization = provider.GetRequiredService<IAuthorizationService>();
ClaimsPrincipal[] users =
{
new(new ClaimsIdentity()),
new(new ClaimsIdentity(Array.Empty<Claim>(), "fixture")),
new(new ClaimsIdentity(
new[] { new Claim("permission", "orders.read") }, "fixture"))
};
foreach (ClaimsPrincipal user in users)
{
var result = await authorization.AuthorizeAsync(
user, resource: null, policyName: "ReadOrders");
Console.WriteLine(result.Succeeded ? "Allowed" : "Denied");
}Denied
Denied
AllowedC# ASP.NET Core Authentication and Authorization in Real Projects
Worked exercise: authorize the stored owner
The task is to allow only the authenticated subject that owns an order. The complete fixture registers a resource handler and tests two principals against the same stored owner. The subject claim is a trusted input in a real application and an explicit constructed value only in this isolated test.
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddLogging();
services.AddAuthorizationCore();
services.AddSingleton<IAuthorizationHandler, OwnerHandler>();
using var provider = services.BuildServiceProvider();
var authorization = provider.GetRequiredService<IAuthorizationService>();
var order = new Order(7, "user-a");
foreach (string subject in new[] { "user-a", "user-b" })
{
var user = new ClaimsPrincipal(new ClaimsIdentity(
new[] { new Claim("sub", subject) }, "fixture"));
var result = await authorization.AuthorizeAsync(
user, order, new[] { new OwnerRequirement() });
Console.WriteLine(result.Succeeded ? "Owner allowed" : "Owner denied");
}
public sealed record Order(int Id, string OwnerSubject);
public sealed class OwnerRequirement : IAuthorizationRequirement { }
public sealed class OwnerHandler :
AuthorizationHandler<OwnerRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OwnerRequirement requirement, Order resource)
{
if (context.User.Identity?.IsAuthenticated == true &&
string.Equals(context.User.FindFirst("sub")?.Value,
resource.OwnerSubject, StringComparison.Ordinal))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}Owner allowed
Owner deniedA third test should use an anonymous principal with the same subject text and verify denial. Matching a string is insufficient without the authenticated identity condition.
C# ASP.NET Core Authentication and Authorization FAQ
What is the difference between 401 and 403?
A 401 challenge generally indicates that acceptable authentication is missing or failed. A 403 forbid generally indicates an authenticated principal that lacks authorization for the operation. The exact handler and resource-disclosure policy influence responses. Test both paths with the actual endpoint configuration, and avoid returning sensitive details about why a particular credential or resource was rejected.
Is decoding a JWT enough to authenticate a caller?
No. Decoding reveals supplied claims but does not validate the signature, issuer, audience or lifetime. Use a supported authentication handler under a trusted provider configuration. A token payload can be crafted by an attacker unless the validation boundary proves its authenticity. Never turn decoded fields directly into a trusted principal merely because their names look familiar.
Why does the console fixture construct principals?
It isolates authorization logic so tests can cover anonymous, permitted and denied cases deterministically. The fixture does not establish a real user's identity and must not be exposed as a login mechanism. A real request principal comes from the configured authentication handler. Keeping these layers separate makes tests useful without weakening the production trust boundary.
Does a read permission allow every user's order?
Not necessarily. A feature-level permission can allow the operation category while resource authorization restricts the specific record. Use trusted stored ownership data and a validated stable subject. A caller-supplied owner field is not evidence. Test cross-user access explicitly, and preserve the check through the actual read or mutation operation rather than treating it as an unrelated preliminary step.
C# ASP.NET Core Authentication and Authorization Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
An order update includes OwnerSubject in its request body. What should the server use for ownership authorization?
C# ASP.NET Core Authentication and Authorization Quiz
2 questions to check what you learned.
Which responsibility belongs to authentication?
- Deciding whether user-a owns order-7.
- Establishing a principal from validated trusted evidence.
- Granting every resource operation once a token has a valid signature.
- Treating every decoded claim as verified.
A valid authenticated token lacks a required permission. Which outcome is generally appropriate?
- HTTP 200 with unrestricted data.
- Mint a new token inside the endpoint.
- A forbid response, commonly HTTP 403.
- Skip the policy because the signature is valid.