Skip to main content
ANVISoftware Solutions
Lesson 1 of 14Intermediate16 min

Authentication vs Authorization

By the end of this lesson

Separate proving identity from granting permission.

Two questions get asked on nearly every request your application handles. Who is this? And are they allowed to do the thing they are asking for?

Authentication answers the first. Authorization answers the second. They run at different moments, fail in different ways, and belong in different parts of your code. Treating them as one step is behind a large share of access control bugs, because a system that knows who you are will happily let you do anything unless something separate says otherwise.

This lesson assumes you have used a login form and can read a small amount of C#. Everything else is defined as it appears.

Five terms used throughout the rest of this course:

Authentication
Establishing which account a request belongs to, by checking something only that account holder should be able to present — a password, a token, a certificate. Often shortened to authn.
Authorization
Deciding whether the established account may perform this particular operation, on this particular data. Often shortened to authz.
Claim
A single statement about the authenticated user, carried with the request: an account id, an email address, a role, a department. Your code reads claims from the authenticated identity. It never asks the client what its claims are.
Principal
The object representing the authenticated user for the current request, holding its claims. In ASP.NET Core this is HttpContext.User, also available as a ClaimsPrincipal parameter on your handlers.
Anonymous request
A request with no established identity. Worth naming separately, because 'we do not know who you are' and 'we know who you are and the answer is no' are different situations that need different responses.

The differences that matter in practice:

 AuthenticationAuthorization
Question answeredWho is making this request?May they do this, to this?
When it runsOnce per request, as the credential is checkedEvery time a protected operation is reached
What it depends onA credential the client presentsThe user's claims, plus the data being touched
Status code on failure401 — no usable identity was established403 — identity established, permission refused
What the user should do nextSign in, or present a valid tokenNothing. Asking again will not change the answer
Where it lives in your codeMiddleware and identity configurationPolicies, handlers, and the code that loads the data
Program.cs — the two concerns configured separately
C#
var builder = WebApplication.CreateBuilder(args);

// 1. Authentication: how a request's identity gets established.
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer();

// 2. Authorization: what an established identity is allowed to do.
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanApproveOrders", policy =>
        policy.RequireAuthenticatedUser()
              .RequireRole("OrdersManager"));

    // Closed by default. An endpoint with no requirements of its own
    // still needs an authenticated user, unless it says [AllowAnonymous].
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
});

var app = builder.Build();

app.UseAuthentication();   // works out who you are
app.UseAuthorization();    // decides whether you may proceed

app.MapGet("/orders/{id:int}", GetOrder);

app.MapPost("/orders/{id:int}/approve", ApproveOrder)
   .RequireAuthorization("CanApproveOrders");

app.Run();
  • The two registrations are deliberately separate. Replacing tokens with cookies later changes the first block and leaves the second untouched, because the policy talks about a role rather than about how the role was proved.
  • UseAuthentication runs before UseAuthorization, and the order is not cosmetic. The authorization step has nothing to decide with until the principal exists.
  • FallbackPolicy applies to any endpoint that states no requirements of its own. It flips the default from open to closed, so a forgotten attribute produces a refusal rather than an unprotected endpoint. Making the safe case the lazy case is worth more than any amount of care.
  • RequireAuthorization names the policy for the approve endpoint. The handler itself does not repeat the role check, which keeps one rule in one place.

Authorization is a per-request decision, and that is the part most often got wrong. Checking permissions once at login and remembering the answer is tempting, because it looks like a saving. It fails for two reasons.

The first is that the answer changes. An employee moves team, a role is revoked, an account is disabled. A decision cached for the life of a session keeps working after the permission behind it has gone.

The second is more fundamental: the decision depends on the operation, and often on the specific record. A session that may read its own orders is not a session that may read every order. A check that ran at login, before any order was named, cannot know which order is about to be loaded. It was never in a position to answer the question.

The front end hiding a button is not a check either. It is a courtesy to the user. Your endpoint is reachable without your front end, by anything that can make an HTTP request, and it has to refuse on its own.

OrdersEndpoints.cs — checking the user against the specific record
C#
private static async Task<IResult> GetOrder(
    int id,
    ClaimsPrincipal user,
    OrdersDbContext db,
    CancellationToken ct)
{
    var order = await db.Orders
        .AsNoTracking()
        .FirstOrDefaultAsync(o => o.Id == id, ct);

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

    var employeeId = user.FindFirst("employee_id")?.Value;

    var isOwner = order.PlacedByEmployeeId.ToString() == employeeId;
    var isManager = user.IsInRole("OrdersManager");

    if (!isOwner && !isManager)
    {
        // 403: we know who you are. The answer is no.
        return Results.Forbid();
    }

    return Results.Ok(OrderResponse.From(order));
}
  • The fallback policy already guaranteed an authenticated user before this method ran. That gave us a name, not a decision about this order.
  • The ownership rule needs the record, so it cannot run in middleware. The check sits next to the data it depends on. ASP.NET Core calls this resource-based authorization, and IAuthorizationService lets you move the rule into a reusable handler once more than one endpoint needs it.
  • Results.Forbid produces 403. Results.Challenge produces 401. Returning 401 here would tell a signed-in employee to sign in again, which is misleading advice and hides a permission problem behind a login screen.
  • Reading the employee id from the principal, not from the request, is the whole point of the claim. An id supplied by the caller is a statement by the caller about itself.
  • One trade-off to decide deliberately: returning NotFound before the permission check tells a caller which order ids exist. Inside one company that is usually acceptable. Where it is not, return NotFound for refusals too, and accept that debugging gets harder.

Summary

  • Authentication establishes which account a request belongs to; authorization decides what that account may do
  • Keep them separate in code so changing the login mechanism does not touch the permission rules
  • 401 means no usable identity was established; 403 means the identity is known and permission was refused
  • Authorization is a per-request decision, because it depends on the operation and often the specific record
  • Make the default closed, and read permissions from the authenticated principal rather than from the request

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

401 or 403?

Decide the right status code for each of these, and say why:

1. A request arrives at the orders API with no token at all. 2. A request arrives with a token that expired an hour ago. 3. A request arrives with a valid token for an employee who is not an orders manager, asking to approve an order.

Show solution

The first two are 401. In both cases no usable identity was established — a missing credential and an expired credential leave the server in the same position. The useful thing the client can do is obtain a valid one, and 401 is the instruction to do that.

The third is 403. The identity is established and current; the request is refused on permission. Sending 401 would prompt a fresh sign-in that produces exactly the same refusal, which wastes the user's time and hides the real cause from whoever reads the logs.

The line between them is which of the two questions failed. If authentication failed, 401. If authentication succeeded and authorization failed, 403.

Try it yourself

Add the ownership rule

An endpoint returns any order to any signed-in employee. The business rule is that an employee sees their own orders, and an orders manager sees all of them.

Write the check. Then answer this: could the rule have been expressed as an authorization policy in Program.cs instead, and why or why not?

Show solution

It cannot be a policy on its own. A policy is evaluated from the principal and the endpoint, before your handler runs, so at that moment no order has been loaded. The rule needs the order's PlacedByEmployeeId, which means the decision has to happen after the record is fetched.

That is the difference between a role rule and a resource rule. Roles are properties of the user and can be checked early. Ownership is a relationship between the user and one record, and can only be checked once both are in hand.

The tidy middle ground is resource-based authorization: keep the rule in an AuthorizationHandler so it is written and tested once, and call it from the handler with the loaded order. The check still happens late; the logic is no longer copied into every endpoint.

C#
var employeeId = user.FindFirst("employee_id")?.Value;

var isOwner = order.PlacedByEmployeeId.ToString() == employeeId;
var isManager = user.IsInRole("OrdersManager");

if (!isOwner && !isManager)
{
    return Results.Forbid();
}

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

A signed-in employee requests an order belonging to another team and is refused. Which status code fits?
Why does authorization have to be checked on every request rather than once at login?

Saved in this browser only.