DEV_NET_CORE
GET_STARTED
.NETAuthentication, authorization, and web security

Authentication vs Authorization in C#

Overview

Authentication and authorization are two core security concepts in C# web applications, especially in ASP.NET Core APIs and web apps.

Authentication answers the question: Who are you?

Authorization answers the question: What are you allowed to do?

Authentication verifies the identity of a user, service, or client application. Authorization decides whether that authenticated identity has permission to access a resource or perform an operation.

For example, when a user signs in with a username and password, cookie, JWT bearer token, OpenID Connect provider, or external identity provider, the application is performing authentication. When the same user tries to access an admin endpoint, update another user's data, approve a payment, or call a protected API, the application performs authorization.

This topic matters because almost every production C# web application needs secure identity and access control. A developer must understand the difference between proving identity and granting access. Confusing the two can cause serious security issues, such as allowing authenticated users to access data they should not see.

In ASP.NET Core, authentication is handled by authentication middleware, authentication schemes, and authentication handlers. Authorization is handled by [Authorize], policies, roles, claims, requirements, handlers, and authorization middleware.

This topic is important for interviews because it tests practical web security understanding. Interviewers often ask about:

  • The difference between authentication and authorization.
  • What [Authorize] actually does.
  • How JWT bearer authentication works.
  • What claims, roles, and policies are.
  • Why middleware order matters.
  • The difference between 401 Unauthorized and 403 Forbidden.
  • How to secure APIs using cookies, JWTs, or external identity providers.
  • How to avoid common security mistakes in ASP.NET Core.

A strong answer should explain both concepts clearly and show how they work together in a real ASP.NET Core application.

Core Concepts

Authentication

Authentication is the process of verifying an identity.

In an ASP.NET Core application, authentication usually produces a ClaimsPrincipal that represents the current user or caller.

A ClaimsPrincipal contains one or more identities, and each identity contains claims.

Example claims:

Code
sub: 12345
name: Minh
email: [email protected]
role: Admin
tenant_id: tenant-001
permission: orders.read

Authentication does not automatically mean the user can access everything. It only means the application has identified who the caller is.

Common authentication mechanisms in C# and ASP.NET Core include:

  • Cookie authentication.
  • JWT bearer authentication.
  • OpenID Connect authentication.
  • OAuth 2.0 access tokens.
  • ASP.NET Core Identity.
  • Windows authentication.
  • API key authentication through custom middleware or handlers.
  • External identity providers such as Microsoft Entra ID, Auth0, Okta, or Google.

Example JWT bearer authentication registration:

Code
using Microsoft.AspNetCore.Authentication.JwtBearer;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://identity.example.com";
        options.Audience = "orders-api";
    });

builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapControllers();

app.Run();

In this example, authentication validates the incoming bearer token and builds the authenticated user identity.

Authorization

Authorization is the process of deciding whether an authenticated identity can access a resource or perform an action.

Authorization usually depends on information from authentication, such as:

  • User ID.
  • Roles.
  • Claims.
  • Permissions.
  • Tenant ID.
  • Department.
  • Subscription level.
  • Resource ownership.

Example authorization using [Authorize]:

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

[ApiController]
[Route("api/orders")]
[Authorize]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetOrders()
    {
        return Ok(new[] { "Order 1", "Order 2" });
    }
}

This requires the caller to be authenticated. If the caller is not authenticated, the request is rejected.

Authorization can also be more specific:

Code
[Authorize(Roles = "Admin")]
[HttpDelete("{id:int}")]
public IActionResult DeleteOrder(int id)
{
    return NoContent();
}

This requires the authenticated user to be in the Admin role.

Authentication vs Authorization

Authentication and authorization are related but different.

ConceptQuestion AnsweredExample
AuthenticationWho are you?User signs in and receives a cookie or JWT
AuthorizationWhat can you access?User must have Admin role to delete an order

A user can be authenticated but not authorized.

Example:

  • Minh signs in successfully.
  • Minh is authenticated.
  • Minh tries to access /api/admin/users.
  • Minh is not an admin.
  • The application returns 403 Forbidden.

A request can also be unauthenticated.

Example:

  • No token or cookie is sent.
  • The application cannot identify the caller.
  • The application returns 401 Unauthorized.

The key point is that authentication comes first, and authorization uses the authenticated identity to make access decisions.

ClaimsPrincipal, ClaimsIdentity, and Claims

ASP.NET Core represents the current user using HttpContext.User, which is a ClaimsPrincipal.

Example:

Code
[Authorize]
[HttpGet("me")]
public IActionResult GetCurrentUser()
{
    var userId = User.FindFirst("sub")?.Value;
    var email = User.FindFirst("email")?.Value;
    var roles = User.Claims
        .Where(c => c.Type == "role")
        .Select(c => c.Value)
        .ToList();

    return Ok(new
    {
        UserId = userId,
        Email = email,
        Roles = roles
    });
}

Important terms:

TermMeaning
ClaimsPrincipalRepresents the current user or caller
ClaimsIdentityRepresents one identity inside a principal
ClaimA key-value statement about the user
RoleA type of claim commonly used for role-based access
PolicyA named authorization rule
RequirementA condition inside a policy
HandlerCode that evaluates a requirement

Claims are not always permissions. A claim only states something about the user or token. The application must decide how to interpret it.

For example, a claim department: finance does not automatically grant access. You need authorization rules that use that claim.

Authentication Schemes

An authentication scheme is a named authentication configuration. It tells ASP.NET Core which handler should authenticate a request.

Common schemes include:

  • Cookies
  • Bearer
  • OpenID Connect schemes
  • Custom schemes

Example using cookies:

Code
using Microsoft.AspNetCore.Authentication.Cookies;

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/access-denied";
    });

Example using JWT bearer:

Code
using Microsoft.AspNetCore.Authentication.JwtBearer;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://identity.example.com";
        options.Audience = "orders-api";
    });

If an app has one authentication scheme, it is often configured as the default scheme. If an app has multiple schemes, you may need to specify which one to use.

Example:

Code
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("api-data")]
public IActionResult GetApiData()
{
    return Ok();
}

Multiple schemes are common when the same app supports both browser cookie authentication and API bearer token authentication.

Authentication Middleware and Authorization Middleware

ASP.NET Core uses middleware to process requests.

The common order is:

Code
app.UseRouting();

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

app.MapControllers();

The order matters.

UseAuthentication() reads the incoming credential, such as a cookie or bearer token, validates it, and sets HttpContext.User.

UseAuthorization() checks endpoint authorization requirements, such as [Authorize], roles, or policies.

If UseAuthorization() runs before UseAuthentication(), authorization may not see the authenticated user correctly.

Best practice:

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

Authentication should run before authorization.

[Authorize] and [AllowAnonymous]

[Authorize] requires authorization for a controller, action, Razor Page, or endpoint.

Example:

Code
[Authorize]
[ApiController]
[Route("api/profile")]
public class ProfileController : ControllerBase
{
    [HttpGet]
    public IActionResult GetProfile()
    {
        return Ok();
    }
}

[AllowAnonymous] allows anonymous access even when a controller or global policy requires authentication.

Example:

Code
[Authorize]
[ApiController]
[Route("api/account")]
public class AccountController : ControllerBase
{
    [AllowAnonymous]
    [HttpPost("login")]
    public IActionResult Login()
    {
        return Ok();
    }

    [HttpGet("me")]
    public IActionResult Me()
    {
        return Ok();
    }
}

In this example:

  • /api/account/login allows anonymous access.
  • /api/account/me requires an authenticated user.

Common mistake:

Code
[Authorize]
public class AccountController : ControllerBase
{
    [HttpPost("login")]
    public IActionResult Login()
    {
        return Ok();
    }
}

If you forget [AllowAnonymous], the login endpoint may require authentication, which makes no sense.

Role-Based Authorization

Role-based authorization grants access based on roles.

Example:

Code
[Authorize(Roles = "Admin")]
[HttpGet("admin-report")]
public IActionResult GetAdminReport()
{
    return Ok();
}

Multiple roles can be allowed:

Code
[Authorize(Roles = "Admin,Manager")]
[HttpGet("management-report")]
public IActionResult GetManagementReport()
{
    return Ok();
}

This means the user must be in either the Admin role or the Manager role.

Role-based authorization is simple and useful for broad access control. However, it can become hard to maintain when permissions become fine-grained.

For example, this is less flexible:

Code
[Authorize(Roles = "Admin")]

This may be better for larger systems:

Code
[Authorize(Policy = "CanApproveOrders")]

The policy name describes the permission rather than a role name.

Claims-Based Authorization

Claims-based authorization uses claims to make access decisions.

Example policy:

Code
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("FinanceOnly", policy =>
    {
        policy.RequireClaim("department", "finance");
    });
});

Controller action:

Code
[Authorize(Policy = "FinanceOnly")]
[HttpGet("finance-report")]
public IActionResult GetFinanceReport()
{
    return Ok();
}

This requires the user to have a department claim with value finance.

Claims-based authorization is more flexible than simple role checks because it can use identity information such as:

  • Department.
  • Tenant.
  • Region.
  • Permission.
  • Subscription level.
  • Employment type.
  • Security clearance.

However, claims must be trusted. Claims from a JWT should come from a trusted identity provider and must be validated.

Policy-Based Authorization

Policy-based authorization is the recommended approach for complex authorization rules in ASP.NET Core.

A policy is a named rule.

Example:

Code
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanDeleteOrders", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireRole("Admin");
        policy.RequireClaim("permission", "orders.delete");
    });
});

Usage:

Code
[Authorize(Policy = "CanDeleteOrders")]
[HttpDelete("{id:int}")]
public IActionResult DeleteOrder(int id)
{
    return NoContent();
}

Policy-based authorization is useful because it:

  • Centralizes authorization rules.
  • Improves readability.
  • Avoids repeating role and claim logic across actions.
  • Supports custom requirements and handlers.
  • Works well for enterprise applications.

Custom Authorization Requirements and Handlers

For complex authorization, you can create custom requirements and handlers.

Example requirement:

Code
using Microsoft.AspNetCore.Authorization;

public sealed class MinimumAgeRequirement : IAuthorizationRequirement
{
    public MinimumAgeRequirement(int minimumAge)
    {
        MinimumAge = minimumAge;
    }

    public int MinimumAge { get; }
}

Example handler:

Code
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

public sealed class MinimumAgeHandler
    : AuthorizationHandler<MinimumAgeRequirement>
{
    protected override Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        MinimumAgeRequirement requirement)
    {
        var dateOfBirthValue = context.User.FindFirst("date_of_birth")?.Value;

        if (!DateTime.TryParse(dateOfBirthValue, out var dateOfBirth))
        {
            return Task.CompletedTask;
        }

        var age = DateTime.Today.Year - dateOfBirth.Year;

        if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
        {
            age--;
        }

        if (age >= requirement.MinimumAge)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

Register the handler and policy:

Code
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast18", policy =>
    {
        policy.Requirements.Add(new MinimumAgeRequirement(18));
    });
});

Use the policy:

Code
[Authorize(Policy = "AtLeast18")]
[HttpGet("restricted-content")]
public IActionResult GetRestrictedContent()
{
    return Ok();
}

This is useful when authorization logic cannot be expressed with simple role or claim checks.

Resource-Based Authorization

Sometimes authorization depends on the specific resource being accessed.

Example:

  • A user can edit their own order.
  • An admin can edit any order.
  • A manager can approve only orders from their department.
  • A tenant user can access only resources from their tenant.

Attribute-based authorization may not have enough context because the resource must be loaded first.

Example:

Code
[Authorize]
[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateOrder(
    int id,
    UpdateOrderRequest request,
    IAuthorizationService authorizationService,
    CancellationToken cancellationToken)
{
    var order = await _orderRepository.GetByIdAsync(id, cancellationToken);

    if (order is null)
    {
        return NotFound();
    }

    var authorizationResult = await authorizationService.AuthorizeAsync(
        User,
        order,
        "CanUpdateOrder");

    if (!authorizationResult.Succeeded)
    {
        return Forbid();
    }

    order.Update(request.Description);

    await _orderRepository.SaveChangesAsync(cancellationToken);

    return NoContent();
}

Resource-based authorization is more secure than checking only whether the user is authenticated.

A common security mistake is checking only this:

Code
[Authorize]
public async Task<IActionResult> GetOrder(int id)
{
    var order = await _orderRepository.GetByIdAsync(id);
    return Ok(order);
}

This verifies that the user is logged in but does not verify that the user owns or is allowed to access the order.

JWT Bearer Authentication

JWT bearer authentication is common for APIs.

The client sends an access token in the Authorization header:

Code
Authorization: Bearer eyJhbGciOi...

ASP.NET Core validates the token and creates a ClaimsPrincipal.

A secure API should validate:

  • Token signature.
  • Issuer.
  • Audience.
  • Expiration.
  • Relevant claims.
  • Token type and intended use.

Example:

Code
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.example.com";
        options.Audience = "orders-api";

        options.TokenValidationParameters.ValidateIssuer = true;
        options.TokenValidationParameters.ValidateAudience = true;
        options.TokenValidationParameters.ValidateLifetime = true;
    });

Common mistake:

Code
// Bad idea for production
options.TokenValidationParameters.ValidateIssuerSigningKey = false;

Do not weaken token validation in production.

JWTs are commonly used for stateless API authentication, mobile apps, SPAs, and service-to-service calls. However, token storage and refresh flows must be designed carefully.

Cookie authentication is common for server-rendered web apps and some browser-based applications.

After sign-in, the server creates an authentication cookie. The browser sends the cookie with later requests.

Example registration:

Code
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.Name = "__Host-AppAuth";
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/access-denied";
        options.SlidingExpiration = true;
    });

Cookies are convenient for browser apps because the browser automatically sends them. However, cookie-based authentication must handle:

  • CSRF protection.
  • Secure cookie settings.
  • SameSite behavior.
  • Expiration.
  • Sign-out.
  • Data protection key management across multiple servers.

JWTs and cookies both authenticate users, but they are used differently.

MechanismCommon Use Case
Cookie authenticationServer-rendered web apps
JWT bearer authenticationAPIs, mobile apps, SPAs, service-to-service calls
OpenID ConnectSign-in with an external identity provider
OAuth 2.0 access tokenAPI authorization

OpenID Connect and OAuth 2.0

OpenID Connect and OAuth 2.0 are related but not the same.

OpenID Connect is about signing in users and getting identity information. It answers authentication questions.

OAuth 2.0 is about delegated access to resources. It is commonly used to obtain access tokens for APIs.

In practical ASP.NET Core development:

  • A web app may use OpenID Connect to sign in a user.
  • An API may use JWT bearer authentication to validate access tokens.
  • Authorization policies decide what the user or client can access.

A common interview mistake is saying OAuth is simply a login protocol. In modern systems, OpenID Connect is typically used for login, while OAuth 2.0 is used for delegated authorization.

401 Unauthorized vs 403 Forbidden

401 Unauthorized means the request is not authenticated or the authentication failed.

Examples:

  • No token was provided.
  • Token is expired.
  • Token signature is invalid.
  • Cookie is missing.
  • The app cannot identify the caller.

403 Forbidden means the caller is authenticated but not allowed to access the resource.

Examples:

  • User is signed in but lacks the required role.
  • User has a valid token but missing required permission.
  • User tries to access another tenant's data.
  • User is authenticated but fails a policy check.

In ASP.NET Core terms:

  • Challenge() often results in 401.
  • Forbid() often results in 403.

Example:

Code
if (!User.Identity?.IsAuthenticated ?? true)
{
    return Challenge();
}

if (!User.IsInRole("Admin"))
{
    return Forbid();
}

In APIs, returning the correct status code helps clients know whether they need to sign in again or show an access denied message.

Challenge vs Forbid

Authentication handlers respond to two important actions:

  • Challenge.
  • Forbid.

A challenge asks the client to authenticate. This commonly maps to 401 Unauthorized for APIs or redirects to login for cookie-based web apps.

A forbid tells the client that authentication succeeded, but access is denied. This commonly maps to 403 Forbidden for APIs or redirects to an access denied page for cookie-based web apps.

Example:

Code
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult AdminOnly()
{
    return Ok();
}

If the user is not signed in, the app challenges the user.

If the user is signed in but not an admin, the app forbids access.

Global Authorization

Some APIs require authentication by default.

You can configure a fallback policy:

Code
builder.Services.AddAuthorization(options =>
{
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

This means endpoints require authenticated users unless explicitly marked as anonymous.

Example:

Code
app.MapGet("/health", () => Results.Ok("Healthy"))
   .AllowAnonymous();

app.MapGet("/profile", () => Results.Ok("Profile"))
   .RequireAuthorization();

For controller APIs, you can also use [Authorize] at the controller or base controller level.

A secure default is useful because developers are less likely to accidentally expose endpoints. However, public endpoints like login, registration, health checks, documentation, and webhook callbacks must be considered carefully.

Minimal APIs and Authorization

Minimal APIs use endpoint methods instead of controller attributes, but the same security concepts apply.

Example:

Code
app.MapGet("/api/orders", () =>
{
    return Results.Ok(new[] { "Order 1", "Order 2" });
})
.RequireAuthorization();

Policy example:

Code
app.MapDelete("/api/orders/{id:int}", (int id) =>
{
    return Results.NoContent();
})
.RequireAuthorization("CanDeleteOrders");

Allow anonymous:

Code
app.MapPost("/api/account/login", () =>
{
    return Results.Ok();
})
.AllowAnonymous();

Minimal APIs still use authentication and authorization middleware:

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

The syntax is different, but the concepts are the same.

Authentication and Authorization in Clean Architecture

In Clean Architecture, authentication and authorization should be separated from business logic.

Typical responsibilities:

LayerResponsibility
API layerReads authenticated user and applies endpoint authorization
Application layerPerforms use case authorization when needed
Domain layerEnforces domain invariants
Infrastructure layerIntegrates with identity providers, token validation, persistence

Example:

Code
public interface ICurrentUser
{
    string? UserId { get; }
    bool IsAuthenticated { get; }
    bool HasPermission(string permission);
}

Implementation in the API or infrastructure layer:

Code
public sealed class CurrentUser : ICurrentUser
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public CurrentUser(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string? UserId =>
        _httpContextAccessor.HttpContext?.User.FindFirst("sub")?.Value;

    public bool IsAuthenticated =>
        _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated == true;

    public bool HasPermission(string permission)
    {
        return _httpContextAccessor.HttpContext?.User
            .HasClaim("permission", permission) == true;
    }
}

This allows application handlers to access current-user information without depending directly on MVC controller classes.

However, avoid spreading authorization logic randomly across the codebase. Centralize policies and use clear authorization services where possible.

Authentication Is Not Enough for Multi-Tenant Security

In multi-tenant applications, authentication only identifies the caller. It does not automatically protect tenant data.

Bad example:

Code
[Authorize]
[HttpGet("{orderId:int}")]
public async Task<IActionResult> GetOrder(int orderId)
{
    var order = await _dbContext.Orders.FindAsync(orderId);
    return Ok(order);
}

This endpoint checks that the user is logged in, but it does not verify tenant access.

Better example:

Code
[Authorize]
[HttpGet("{orderId:int}")]
public async Task<IActionResult> GetOrder(int orderId)
{
    var tenantId = User.FindFirst("tenant_id")?.Value;

    var order = await _dbContext.Orders
        .Where(o => o.Id == orderId && o.TenantId == tenantId)
        .SingleOrDefaultAsync();

    return order is null ? NotFound() : Ok(order);
}

This checks resource access using tenant information.

A good interview answer should mention that authorization often includes row-level or resource-level checks, not just endpoint-level [Authorize].

Common Security Mistakes

Common mistakes include:

  • Confusing authentication with authorization.
  • Assuming logged-in users can access all data.
  • Using [Authorize] but not checking resource ownership.
  • Putting sensitive permissions only in frontend code.
  • Trusting unvalidated JWTs.
  • Disabling issuer, audience, signature, or lifetime validation.
  • Using roles for every permission in a large system.
  • Forgetting middleware order.
  • Forgetting [AllowAnonymous] on login endpoints.
  • Returning 401 when the user is authenticated but not allowed.
  • Returning 403 when the user is not authenticated.
  • Storing JWTs insecurely in browser storage without considering XSS risk.
  • Not protecting cookie-based flows against CSRF.
  • Exposing too much information in error messages.
  • Hard-coding authorization rules throughout controllers.
  • Not testing authorization paths.

Best Practices

Use authentication to establish identity and authorization to enforce access rules.

Validate tokens fully in production.

Prefer policy-based authorization for complex systems.

Use role-based authorization for simple broad access control.

Use claims and permissions for fine-grained access control.

Use resource-based authorization when access depends on the specific entity being accessed.

Always use UseAuthentication() before UseAuthorization().

Use [AllowAnonymous] intentionally for public endpoints.

Return correct status codes:

  • 401 for unauthenticated or invalid authentication.
  • 403 for authenticated but not allowed.

Do not rely on frontend checks for security. Backend authorization is required.

Keep authentication configuration in infrastructure or startup code, and keep business access decisions explicit and testable.

Add automated tests for:

  • Anonymous access.
  • Authenticated access.
  • Missing roles.
  • Missing permissions.
  • Cross-tenant access.
  • Resource ownership rules.
  • Expired or invalid tokens.

Interview Practice

PreviousParameter BindingNext UpCookie behavior, CSRF, and browser-based security concerns