Design Desk

Security

Authentication vs Authorization

Checking that someone is logged in and checking that they're allowed to see this specific thing are two different questions, and a codebase that only ever asks the first one has a very quiet bug in it.

SecurityFoundation6 min read

After this lesson, you should be able to:

  • Distinguish authentication, who is this, from authorization, is this specific user allowed to do this specific thing.
  • Diagnose broken object-level authorization: a route that checks login but never checks resource ownership.
  • Explain why authentication centralizes easily in one middleware while object-level authorization has to be checked per resource.
  • Recognize that authorization must be evaluated at request time against current permissions, not cached from login.

At a Glance

  • Authentication answers "who is this," checked once, at login. Authorization answers "is this specific person allowed to do this specific thing," and has to be checked again for every resource and every action, not once.
  • A route that requires a valid session is authenticated. That says nothing about whether the person behind that session is allowed to see the particular record they just asked for.
  • The hard part isn't building a login system, that's a solved, well-tested problem. It's remembering to check ownership at the object level, on every endpoint that touches a specific resource, not just at the route level.

01 The Problem

A SaaS app's GET /invoices/:id endpoint sits behind middleware that checks for a valid session and rejects anyone not logged in with a 401. The route is, by every reasonable definition, protected. The handler then fetches the invoice by the ID in the URL and returns it. A logged-in user, looking at their own invoice at /invoices/4471, changes the number in the address bar to /invoices/4472, out of curiosity or by accident, and gets back somebody else's invoice, a different company's line items and totals, because nothing in the handler ever asked whether this specific user actually owns invoice 4472. It asked whether they were logged in at all, once, and stopped there.

This isn't a login bug. Authentication worked exactly as designed, the user really is who their session says they are. The bug is that "logged in" and "allowed to see this specific record" got treated as the same check, and they aren't.

02 What It Is

Authentication answers who is this. It happens once, typically at login: verify a password or token, issue a session or a signed token representing "this request comes from user X," and every subsequent request just checks that the session or token is still valid. It's a single yes/no gate, checked the same way for every request.

Authorization answers whether this specific, authenticated user is allowed to do this specific thing to this specific resource. It isn't a single gate, it has to be evaluated per resource and per action: this user, reading this invoice; this user, deleting this comment; this user, editing this project. A user can pass authentication and still fail authorization for a particular resource, and that's supposed to happen constantly, it's the entire point of having authorization at all.

Authentication is easy to get right because it's checked in exactly one place, usually a shared middleware, once per request, the same logic no matter what the request is for. Authorization is easy to get right at the route level, does this endpoint require login at all, and easy to get wrong at the object level, does this specific user own this specific thing they're asking for, because that check has to be written again, correctly, in every handler that fetches something by ID, and it's invisible when it's missing. A route that "requires auth" looks exactly the same in the code whether or not it also checks object ownership, right up until someone changes a number in a URL.

In Plain Terms

A building's front door checks your badge before letting you into the lobby. That's the only check most people ever notice, so it's tempting to assume anyone standing in the hallway on the fourth floor is supposed to be there. A locked office door within that same building is a second, separate check, specific to that room, and a badge that opens the front door was never claiming to open every door behind it. Getting past the lobby answers who you are. It says nothing about which rooms you're allowed into.

03 How It Works

Rendering diagram…

The route requires a valid session, and this request has one, so it passes authentication. Nothing after that point checks whether this specific user owns invoice 4472, so any logged-in user, regardless of which company they belong to, gets the same result.

Rendering diagram…

Authentication answers who's asking. A second, separate check, evaluated for this specific object, answers whether they're allowed to have it. Both have to pass, and they're not the same question with the same answer.

04 Trade-offs

 Route-level authorization onlyObject-level authorization
What it actually checksIs this user allowed to hit this endpoint at allIs this user allowed to access this specific resource the endpoint returns
Where it's implementedOnce, in shared middleware, easy to get rightIn every handler that fetches a resource by ID, easy to forget
Failure modeN/A on its own; it's a real check, just an incomplete oneMissing it silently returns another user's data instead of erroring
How it's caughtImmediately, in any manual test, since it blocks unauthenticated access visiblyRarely caught by normal testing, since the tester usually tests with their own, correctly-owned data

Watch out

It's tempting to see a route behind auth middleware and consider the resources it returns "protected." Authenticated and authorized are not the same word doing double duty. A route can require a valid session and still hand out any record whose ID you can guess or increment, because the middleware only ever asked "is someone logged in," never "does this specific someone own this specific thing." This is common enough to have its own name, broken object-level authorization, and it shows up in security disclosures constantly, because it's invisible in code review unless you're specifically looking for the missing ownership check, not the missing auth check.

05 Where You'll Actually See This

Neither case study implements authentication or authorization at all, worth naming rather than glossing over. Both assume a single trusted caller, the merchant, the client, rather than modeling multiple accounts or tenants who'd need to be kept separate from each other. That's a deliberate scope narrowing: the moment either system needs to serve multiple merchants or multiple users who shouldn't see each other's data, this article's distinction is the first thing that has to be designed correctly, before anything else about the system matters.

API gateways are a real place this splits cleanly in practice. A gateway can centralize authentication for every service behind it, one place validating who's calling, because that check doesn't depend on which resource the request is for. It can't centralize object-level authorization the same way, because the gateway has no idea which invoice belongs to which caller, only the service holding that record does. That's not a gap in the gateway, it's the actual boundary between the two checks this article is about.

The authentication half, "is this a valid session," is a check against something specific: a session store or a signature, not a vague notion of being logged in. JWT & Sessions is what that check actually is under the hood, and which one a system picked shapes how fast that first gate can react when access needs to be revoked, well before authorization's per-resource question even comes up.

OAuth and OpenID Connect is this same split given actual protocol names: OIDC answers authentication, OAuth answers authorization, and the two get confused for exactly the reason this article warns about, they share a mechanism and look identical from the outside.

How to reason about it

When asked how you'd secure an endpoint, don't stop at "require authentication." Say how you'd check that the authenticated user is actually allowed to access the specific resource being requested, by ID, not just that they're logged in. An answer that only covers the route level has described half the problem. The half that actually shows up in breach disclosures is the object-level check nobody wrote.

06 Quick Check

A user's session correctly identifies them and their role at login. A week later, an admin revokes that user's access to a specific project, but doesn't log them out. The user, still logged in with the same valid session, requests that project's data. What should happen, and why?