ASP.NET Core Authentication vs. Authorization: A Practical Guide

The short version
When an ASP.NET Core API receives a request, it needs answers to two questions:
- Who is making this request? Authentication establishes an identity.
- May that identity do this? Authorization checks access to an endpoint, operation, or resource.
A useful mental model is a concert venue. Authentication checks the ticket holder’s identity. Authorization checks whether that ticket grants access to the floor, backstage, or a particular seat. Showing a valid ticket gets you into the venue; it does not grant access to every area.
These steps are connected, but they are not interchangeable. A successfully authenticated user can still be forbidden from an action. Conversely, authorization needs an identity or a policy that explicitly permits anonymous access.
This guide builds that model up from the basics, compares the two concepts, registers common services through dependency injection, and follows a request through a Clean Architecture application. It also explains what role-based access control (RBAC) and JWT do—and what they do not do.
Authentication: establishing identity
Authentication is the process of verifying a caller’s identity and creating a ClaimsPrincipal that represents it for the request. A principal can contain one or more identities, and each identity can carry claims such as a user ID, email address, tenant ID, or role.
ASP.NET Core performs authentication through registered schemes. A scheme names a handler and its configuration. Common examples include:
- Cookie authentication: the browser sends a protected cookie on later requests. Common for server-rendered web apps.
- JWT bearer authentication: an API client sends a bearer token, commonly in the
Authorizationheader. Common for APIs and service-to-service calls. - OpenID Connect: an interactive sign-in protocol often used to authenticate people through an identity provider. It commonly works with cookies in a web app.
The scheme handler examines the request, validates its credential, and creates an authentication ticket and principal if it succeeds. Configuring a scheme alone does not automatically make every endpoint private; authorization rules determine which endpoints require an authenticated user.
Authorization: deciding access
Authorization is the decision about whether an identity may perform an operation or access a resource. ASP.NET Core evaluates the current ClaimsPrincipal against authorization requirements.
Authorization can be expressed in several related ways:
- Authenticated-user checks: the user must be signed in.
- Role checks: the user must belong to a role such as
AdminorEditor. - Claim checks: the user must have a claim with a required type and possibly value.
- Policy checks: a named set of one or more requirements, evaluated by handlers.
- Resource-based checks: a decision uses both the user and a particular object, such as whether a user owns a document.
Policies are often the clearest way to name reusable access rules. Roles are useful for broad group membership; claims can describe attributes or capabilities; policies combine requirements and can use custom handlers for more involved decisions.
Authentication vs. authorization
| Question | Authentication | Authorization |
|---|---|---|
| Main question | Who is the caller? | What may the caller do? |
| Input | Cookie, bearer token, or another credential | The authenticated principal, policy, and sometimes a resource |
| ASP.NET Core mechanism | Authentication scheme and handler | Requirements, policies, and authorization handlers |
| Typical outcome | A ClaimsPrincipal or an authentication challenge |
Access granted or denied; an unauthenticated caller may be challenged |
| Example | Validate a JWT and read its subject | Require the Invoice.Approve permission to approve an invoice |
A 401 Unauthorized response generally means the request lacks valid authentication for the endpoint. A 403 Forbidden response generally means the caller was authenticated but failed an authorization check. Exact responses can vary with the app’s handlers and whether it serves browser pages or APIs, but this is the useful distinction to remember.
Example: signed in, but not allowed
A user has a valid identity and can read their own profile. They do not have the Admin role, so they cannot open the administration endpoint. Authentication succeeded; authorization denied the operation.
What is ASP.NET Core Identity?
ASP.NET Core Identity is a membership system for managing application users and related sign-in data. Depending on how it is configured, it provides user and role managers, password hashing and validation, sign-in services, and support for features such as claims, tokens, and lockout. A persistence store—often backed by Entity Framework Core—stores user and role records.
Identity is not itself a replacement for the ASP.NET Core authorization system. It helps create and manage users and sign-in credentials. The authentication scheme turns a credential into a principal, and authorization evaluates that principal against the app’s rules.
Identity can be used with cookie sign-in for a web app. An API can use a standards-based identity provider that issues access tokens, then validate those tokens with JWT bearer authentication. Identity and JWT are not the same thing: Identity is a user-management framework, while JWT is a token format commonly used to carry claims between systems.
Registering authentication and authorization in dependency injection
ASP.NET Core’s dependency injection container receives registrations through builder.Services in Program.cs. The following example shows two common configurations. Choose the one that matches the app’s sign-in model; many apps use one primary model per request path.
Option A: Identity with cookie authentication
For a web app that stores users in an EF Core database, a typical setup looks like this. ApplicationUser is a project class extending IdentityUser; ApplicationDbContext is an EF Core context configured for Identity.
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services
.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequiredLength = 12;
options.Lockout.MaxFailedAccessAttempts = 5;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanManageProducts", policy =>
policy.RequireRole("Admin", "CatalogManager"));
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.Run();
AddIdentity<TUser, TRole> registers Identity’s user and role services and configures its authentication support, which normally includes application cookies. AddEntityFrameworkStores connects Identity to the EF Core context. AddDefaultTokenProviders registers token providers used by features such as password reset and email confirmation; it does not mean the app is issuing JWT access tokens.
The middleware order matters: authentication should run before authorization so that authorization can inspect the principal. In a typical endpoint-routing app, both run after routing and before endpoints execute.
To require the registered policy on a controller action:
[Authorize(Policy = "CanManageProducts")]
public IActionResult CreateProduct() => View();
RequireRole("Admin", "CatalogManager") means a member of either role satisfies that one role requirement. Multiple separate requirements in a policy are generally all required.
Option B: JWT bearer authentication for an API
For an API that accepts access tokens issued by a trusted identity provider, register JWT bearer validation and authorization policies. The issuer should be responsible for signing tokens; avoid inventing a custom token issuer unless the architecture specifically requires one.
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Authentication:Authority"];
options.Audience = builder.Configuration["Authentication:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true
};
});
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanReadReports", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "reports.read"));
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Set Authority and Audience from configuration for the real identity provider and API. Depending on the provider and deployment, metadata discovery can supply signing keys and issuer details. If you validate an issuer’s signing key directly instead, configure the expected key and issuer carefully. Never put signing secrets into source control. Use HTTPS and a secure configuration store.
Protect an API endpoint by requiring authentication and the policy:
[ApiController]
[Route("api/reports")]
public sealed class ReportsController : ControllerBase
{
[HttpGet]
[Authorize(Policy = "CanReadReports")]
public IActionResult GetReports() => Ok();
}
AddJwtBearer validates a token and builds the principal. It does not decide that every token holder can read reports. The authorization policy performs that check. Avoid accepting tokens from arbitrary issuers, and validate signature, issuer, audience, and lifetime according to the trusted provider’s configuration.
Registering Identity roles with a smaller setup
If the app uses AddIdentityCore instead of the full AddIdentity setup, role services are not included automatically. Add them explicitly:
builder.Services
.AddIdentityCore<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddSignInManager();
The app still needs an appropriate authentication scheme, such as cookies or bearer tokens, and authorization services. Use a complete Identity setup that matches the UI and sign-in flow instead of mixing registrations blindly.
Protecting endpoints and resources
A quick role check can be placed on a controller or action:
[Authorize(Roles = "Admin")]
public IActionResult AdminDashboard() => View();
A named policy is often easier to reuse and evolve:
builder.Services.AddAuthorizationBuilder()
.AddPolicy("CanApproveInvoices", policy =>
policy.RequireAuthenticatedUser()
.RequireClaim("permission", "invoice.approve"));
Then apply it with [Authorize(Policy = "CanApproveInvoices")] or on a minimal API endpoint with .RequireAuthorization("CanApproveInvoices").
Some decisions depend on the object being accessed. For example, a user may edit their own draft but not another user’s draft. A simple role attribute cannot express ownership by itself. Load the resource and call IAuthorizationService with a resource-specific requirement, or structure a policy handler to evaluate the resource and current user. Apply those checks close to the operation that has the necessary resource context, and enforce them on the server even if the UI also hides unavailable actions.
What is RBAC?
Role-based access control (RBAC) assigns users to roles, then grants permissions to those roles. An app might define Admin, Editor, and Reader roles. Users receive role membership, and policies or attributes check that membership.
In ASP.NET Core, roles are exposed through ClaimsPrincipal.IsInRole and can be checked with [Authorize(Roles = "Admin")] or RequireRole("Admin"). If Identity manages the roles, its role services and persistence store handle membership. If the app uses AddIdentityCore, add role services with .AddRoles<IdentityRole>().
RBAC works well when permissions map naturally to stable job functions. It can become awkward when the rules depend on ownership, tenant, record state, or a combination of attributes. In those cases, keep roles for broad categories and use policies, claims, or resource-based authorization for specific decisions.
Roles vs. claims vs. policies
- A role answers: “Which group does this user belong to?”
- A claim is a name/value statement about the subject, such as
tenant_id=42orpermission=invoice.approve. - A policy is an app-defined rule that evaluates requirements, often using claims or roles.
Claims are data, not automatically permissions. The application decides how to interpret a claim and whether it is trustworthy. For example, department=Finance does not grant access unless a registered policy uses it to do so.
How JWT fits in
JWT stands for JSON Web Token. A JWT is a compact, signed token format that can carry claims. In a bearer-token request, the client commonly sends it as:
Authorization: Bearer eyJ...
The JWT bearer handler checks the token using configured validation rules, including its signature and expected issuer, audience, and lifetime. If validation succeeds, the handler turns its claims into a ClaimsPrincipal. Authorization then checks roles, claims, and policies against that principal.
The sequence is:
- A user or client authenticates with an identity provider.
- The identity provider issues an access token for the API.
- The client sends the token to the API over HTTPS.
- ASP.NET Core validates the token and creates a principal.
- Authorization evaluates the endpoint’s requirements.
- The endpoint runs only if the checks pass.
A JWT is usually signed, not encrypted. Do not put passwords, secrets, or sensitive personal information in its payload; a token holder can often decode the payload. Keep access tokens short-lived and protect them in the client. A token can become stale before expiry if a user’s role or permission changes, so consider short lifetimes and the identity provider’s revocation or refresh-token strategy. Do not treat “it is a JWT” as proof that a token is trustworthy: issuer, audience, signature, and lifetime validation matter.
Clean Architecture flow
Clean Architecture separates business rules from delivery mechanisms and external details. A common arrangement has these layers:
- API / Presentation: HTTP endpoints, authentication middleware, request/response mapping, and coarse endpoint access checks.
- Application: use cases and application-level permission checks needed to protect business operations.
- Domain: core entities and business invariants; it should not depend on ASP.NET Core or JWT libraries.
- Infrastructure: database access, Identity stores, external identity-provider integration, and implementations of application interfaces.
A typical request flows like this:
Client
│ cookie or bearer token
▼
ASP.NET Core API
├─ Authentication handler validates credential → ClaimsPrincipal
├─ Authorization policy checks endpoint access
▼
Application use case
├─ Checks operation-level permission when needed
▼
Domain rules
▼
Infrastructure persists or retrieves data
The API layer is the natural place to configure AddAuthentication, AddJwtBearer or cookie authentication, AddAuthorization, and the middleware. Those are delivery and framework details. The application layer can depend on an abstraction such as ICurrentUser or IAuthorizationService when a use case needs the caller’s identity or must enforce an operation permission. Infrastructure implements application-owned interfaces for user lookup or persistence.
Avoid making the Domain layer depend on HttpContext, ClaimsPrincipal, JWT packages, or Identity EF stores. Pass the minimum caller context a use case needs through an application abstraction. Also avoid relying solely on endpoint attributes for rules that are essential to a business operation: the same use case might later be called from a queue consumer or another API. Keep the decision near the use case when it must apply to every entry point.
A small current-user abstraction
An application project might define an interface without referencing ASP.NET Core:
public interface ICurrentUser
{
string? UserId { get; }
bool IsAuthenticated { get; }
}
The API project can implement it using the current request principal, and register that implementation in DI:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, HttpContextCurrentUser>();
HttpContextCurrentUser belongs at the API boundary because it adapts HTTP claims to an application-facing interface. The application project owns the interface; Infrastructure or API composition wires the implementation. Do not register HttpContextCurrentUser in the Domain layer.
Common mistakes to avoid
- Confusing login with access: a successful sign-in does not grant every permission.
- Using roles for every rule: record ownership and contextual decisions often need resource-based checks.
- Trusting claims from an unvalidated token: only use claims after the configured handler validates the credential.
- Skipping audience or issuer validation: tokens meant for another service or issuer should not be accepted automatically.
- Putting secrets in JWT payloads: signed tokens can be readable by their holders.
- Putting framework security setup in the Domain: keep ASP.NET Core handlers and
HttpContextat the outer boundary. - Checking only in the UI: enforce authorization on server endpoints and business operations.
- Ignoring middleware order: run authentication before authorization.
- Forgetting default access rules: configure endpoints or a fallback policy intentionally so a new endpoint does not accidentally expose sensitive data.
A practical way to choose
For a traditional server-rendered web app, cookie authentication—often managed with ASP.NET Core Identity—is a common fit. For an API, validate access tokens from a trusted identity provider with JWT bearer authentication. For coarse access categories, roles may be enough. For reusable permissions, combine claims and policy-based authorization. For ownership or data-dependent rules, use resource-based authorization.
Many production systems combine these: Identity or an external provider manages users, a cookie or JWT scheme authenticates requests, and authorization policies decide access. Start with the smallest understandable rules, then introduce custom requirements and handlers when the access decision needs them.
Conclusion
Remember the order: authenticate the request, then authorize the action. Authentication schemes such as cookies and JWT bearer establish a principal. Identity can manage users and sign-in data. Roles, claims, policies, and resource checks use that principal to decide what the caller may do.
In a Clean Architecture design, configure HTTP authentication at the API boundary, keep domain rules independent of web frameworks, and enforce important business permissions at the use case that owns the operation. With that separation, your security rules stay easier to understand as the application grows.


