Skip to content

// Security · 01

Authentication vs Authorization

Who are you? (401)leads totwo checksWhat may you do? (403)

Authentication says who you are; authorization says what you may do.

How it fits together

Authentication vs Authorization structure«client»ClientGET /orders/42Bearer tokenHTTP 401Orders APIauthn: who is it?authz: may they?Orders tableOrderId, CustomerIdHTTP 4031 GET /orders/422 no valid token3 load order4 not your order
uses / callsfails / error path· numbered steps replay in order
  • Client uses / calls Orders API (1 GET /orders/42)
  • Orders API fails / error path HTTP 401 (2 no valid token)
  • Orders API uses / calls Orders table (3 load order)
  • Orders API fails / error path HTTP 403 (4 not your order)

Spot it

  • Logged-in users see other users' orders
  • Anonymous calls return 500 instead of 401
  • Admin page checked only in the UI

It costs

Every protected request pays for a policy check, and the rule must live on the server.

In code

using System.Security.Claims;​app.MapGet("/orders/{id}", (string id, ClaimsPrincipal user, IOrderStore orders) =>{    if (user.Identity?.IsAuthenticated != true)        return Results.Unauthorized();​    var order = orders.Find(id);    if (order is null)        return Results.NotFound();​    var isOwner = user.FindFirst(ClaimTypes.NameIdentifier)?.Value == order.CustomerId;    var isSupport = user.IsInRole("support");    return isOwner || isSupport        ? Results.Ok(order)        : Results.StatusCode(StatusCodes.Status403Forbidden);});
Go deeperThe full explanation and what to ask for · 1 min readExpand

Authentication establishes identity: the caller presents a credential (a password, a session, a signed token) and the server verifies it. Authorization then asks whether that identity may perform this action on this resource. A 401 means the server does not know who you are, and the response should say how to authenticate. A 403 means it knows you and the answer is no. Returning the wrong one confuses clients and retries.

Check authorization where the resource lives, on every request. A hidden button or a route guard in a single-page app is a convenience, not a control: anyone can call the API directly. The check needs the actual object (its owner, its tenant), so a role alone is often too coarse. Keep ownership, tenant and role rules in one policy rather than scattered across handlers.

Decide what an unauthorised caller may learn. Returning 404 for another user's order hides that it exists; 403 is more honest and easier to debug. Pick one rule per resource and apply it everywhere, and deny by default when no rule matches. When NOT to build a policy layer: three roles and two resources need one well-named check, not an engine.

Ask for it

  • authentication (authn)
  • authorization (authz)
  • 401 Unauthorized
  • 403 Forbidden
  • policy / claims check
  • resource ownership

Add an ownership check to GET /orders/{id}: return 401 with no valid bearer token, 404 when the order does not exist, and 403 when the sub claim is not the order CustomerId and the caller lacks the support role. Put the rule in one policy method and test all four outcomes.