// Security · 01
Authentication vs Authorization
Authentication says who you are; authorization says what you may do.
How it fits together
- 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 readExpandCollapse
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.