Back to Blog
Security12 min readJun 2026

Authentication vs Authorization: The Two Most-Confused Words in Security

Authentication asks who are you; authorization asks what are you allowed to do. Mix them up and you ship security holes. Here is the mental model, the request flow, and where each check belongs.

SecurityAuthenticationAuthorizationOAuth
SB

Sri Balaji

Founder · TheSimplifiedTech

On this page

Same prefix, completely different jobs

You add a login page, users sign in, and you breathe out. The app feels secure. Then a junior engineer signs in with their own valid account, changes the URL from /orders/42 to /orders/43, and sees someone else's invoice. Nothing was "hacked", every request had a valid session. The login worked perfectly. The problem was never login at all.

That gap is the difference between authentication and authorization. They share a prefix, they sit next to each other in the request, and they get used interchangeably in standups, which is exactly why people ship the bug above. Once you can keep them apart in your head, a huge class of security mistakes simply stops happening.

Who this is for

Developers who can build a login form but get fuzzy on what happens after sign-in. If you have ever wondered "is this a 401 or a 403?", written an `if (user.isAdmin)` check in a React component, or copy-pasted JWT verification without quite knowing what it proves, this is for you. No prior security background needed.

Two questions, in order

Authentication (authn) answers "who are you?", it proves identity. Authorization (authz) answers "what are you allowed to do?", it grants or denies access to a specific action or resource. Authn always runs first; authz depends on its answer.

Notice the ordering. You cannot decide what someone is allowed to do until you know who they are. But knowing who they are tells you nothing, on its own, about what they may do. Those are two separate decisions, made at two separate moments, often by two separate parts of your system.

Showing your passport at the security checkpointAuthentication, the system confirms you are who you claim to be
Your boarding pass says seat 14C on flight BA42 todayAuthorization, you may board THIS flight, in THIS seat, right now
A valid passport does not let you board any plane you wantBeing authenticated does not mean you are authorized for an action
The lounge agent re-checks your pass at the gateAuthorization is re-checked at each protected resource, not just once
The airport makes the split obvious, two checkpoints, two different questions.

Your passport is real whether you are flying to Tokyo or standing in a parking lot, identity is stable. The boarding pass is scoped: one flight, one seat, one day. That scoping is the whole point of authorization, and it is why "they logged in" is never a complete answer to "should they be able to do this?".

Where each check lives in a request

Every protected request runs the same gauntlet. The client presents a credential, the server verifies it to establish identity, then a separate step asks whether that identity may perform this exact action on this exact resource. Only if both pass does your business logic ever run.

token / cookieidentitypermittedno/invalid creds (401)not allowed (403)
Client

sends credential

Authenticate

Who are you?

Authorize

Allowed for this?

Handler

Business logic

Identity / Policy

users, roles, scopes

401 / 403

rejected early

One request, two gates: authenticate (who), then authorize (allowed for this action?), then the handler runs.

  1. 1

    Client sends a credential

    A request arrives carrying proof of identity, a session cookie, a bearer JWT in the Authorization header, or an API key. No credential at all means the request is anonymous.

  2. 2

    Authenticate: who are you?

    The server validates the credential, verifies the JWT signature and expiry, or looks up the session ID in its store. Success yields a trusted identity (a user id, maybe roles/scopes). Failure returns 401 Unauthorized and stops here.

  3. 3

    Authorize: allowed for THIS?

    Now that identity is known, a policy check asks whether this user may perform this specific action on this specific resource, not "are they logged in" but "may user 42 delete order 43?". Failure returns 403 Forbidden.

  4. 4

    Handler runs

    Only after both gates pass does the actual business logic execute. By the time your handler runs, identity is proven and the action is permitted, the handler should not have to re-litigate either.

Pro tip

The two failure codes map cleanly: **401 Unauthorized** means "I don't know who you are" (authn failed, go log in). **403 Forbidden** means "I know exactly who you are, and you still can't do this" (authz failed, logging in again won't help). Despite its name, 401 is the authentication error.

Sessions vs tokens: how identity is carried

Authentication does not stop after login, proof of identity has to ride along on every subsequent request, because HTTP is stateless. There are two dominant ways to carry it, and they make opposite trade-offs.

Sessions are server-side. After login the server creates a session record (in memory, Redis, or a database) and hands the client an opaque session ID in a cookie. Every request sends the cookie; the server looks the ID up to recover the identity. The token itself carries no information, it is just a key into server state. Revoking access is trivial: delete the row.

Tokens (typically JWTs) are self-contained. After login the server signs a token that embeds the claims, user id, roles, expiry, and the client sends it on each request. The server verifies the signature and trusts the contents without a lookup, which scales beautifully across many services. The catch: a signed token is valid until it expires, so you cannot easily revoke one mid-flight. That is why access tokens should be short-lived and paired with a refresh token.

DimensionServer sessionsJWT / tokens
Where state livesServer (store / DB)Inside the token itself
Lookup per requestYes, fetch the sessionNo, verify the signature
RevocationInstant (delete the record)Hard until expiry; needs a denylist
Scales across servicesNeeds shared session storeNaturally, any service can verify
Best lifetimeLong-ish, revocableShort access + refresh token
Carried inCookie (HttpOnly)Authorization: Bearer header
Sessions vs tokens, the trade-off is statefulness vs revocability.

OAuth and OIDC, without the jargon

Two more terms get tangled into the authn/authz knot, so let us untangle them in one breath. OAuth 2.0 is an authorization framework, it lets a user grant one app limited access to their data in another app without sharing a password. When you click "Connect your Google Calendar", OAuth is what issues an access token scoped to just your calendar.

OIDC (OpenID Connect) is an authentication layer built on top of OAuth. It adds an id_token, a JWT that proves who the user is, so apps can use "Sign in with Google" for login, not just data access. Rule of thumb: OAuth = delegated authorization (access to resources); OIDC = federated authentication (proof of identity). Most "Sign in with X" buttons are OIDC; most "Allow X to access your Y" consent screens are OAuth scopes.

The scopes you see on those consent screens (read:calendar, email, profile) are authorization data riding inside an authentication flow. Your app should treat scopes as one input to its own policy decisions, not as the final word on what a user may do inside your system.

In code: verify identity, then check permission

Here is the whole idea in one Python handler. The first block proves identity from a JWT (authentication). The second block, a completely separate decision, asks whether that proven identity may act on this particular resource (authorization). Keep them visibly distinct.

orders_api.py
python
import jwt  # PyJWT
from fastapi import HTTPException, Request

JWT_SECRET = "...load-from-env..."

# --- AUTHENTICATION: who are you? ---
def authenticate(request: Request) -> dict:
    header = request.headers.get("Authorization", "")
    if not header.startswith("Bearer "):
        # No credential at all -> we don't know who you are.
        raise HTTPException(status_code=401, detail="Missing token")
    token = header.removeprefix("Bearer ")
    try:
        # Verifies signature AND expiry. Returns the claims if valid.
        claims = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")
    return claims  # e.g. {"sub": "42", "roles": ["customer"]}


# --- AUTHORIZATION: are you allowed to do THIS? ---
def authorize_order_access(user: dict, order) -> None:
    is_owner = order.customer_id == user["sub"]
    is_admin = "admin" in user.get("roles", [])
    if not (is_owner or is_admin):
        # We know exactly who you are -- you still can't have this.
        raise HTTPException(status_code=403, detail="Forbidden")


async def get_order(request: Request, order_id: str):
    user = authenticate(request)              # 1. prove identity
    order = await db.orders.find(order_id)
    if order is None:
        raise HTTPException(status_code=404)
    authorize_order_access(user, order)        # 2. check permission
    return order                               # 3. only now: business logic

The authorization check is resource-scoped: it compares the resource's owner against the caller. That is what stops the /orders/43 attack from the intro, a valid token authenticates the attacker, but they are not the owner, so authorize_order_access returns 403. The bug at the top of this article is exactly an app that ran step 1 and skipped step 2.

This pattern of "is the caller the owner, or do they have a role that overrides it?" is RBAC (role-based access control). When rules get richer, "managers can refund orders under €500 in their own region", you graduate to ABAC (attribute-based access control), where the decision is a function of attributes of the user, the resource, and the context. Same place in the request; just a more expressive policy.

Common mistakes that ship security holes

  1. Assuming authentication implies authorization. "They're logged in" is not "they may do this." Every protected action needs its own permission check against the specific resource, the single most common real-world breach (broken object-level authorization, the /orders/43 bug).
  2. Enforcing authorization only in the UI. Hiding the Delete button for non-admins is UX, not security. The endpoint is still reachable with curl. Every check that hides a button in the frontend must be re-enforced on the server, where it cannot be bypassed.
  3. Long-lived or non-revocable tokens. A JWT with a 30-day expiry and no denylist means a leaked token is a 30-day master key. Keep access tokens short (minutes), use refresh tokens, and have a revocation path for the cases that matter.
  4. Trusting claims you didn't verify. Decoding a JWT is not verifying it. Always check the signature and expiry (and issuer/audience) before trusting a single claim, an unverified "roles": ["admin"] is just attacker-supplied JSON.
  5. Checking permissions far from the resource. Authorization decided at the gateway with stale role data can drift from reality. Decide as close to the protected resource as you can, with current data.

Takeaways

The whole article in seven lines

  • Authentication = who are you. Authorization = what may you do. Authn runs first; authz depends on it.
  • 401 = I don't know you (authn failed). 403 = I know you and you still can't (authz failed).
  • Sessions store state on the server (easy revoke); tokens carry state in the JWT (easy scale, hard revoke).
  • OAuth = delegated authorization (resource access). OIDC = authentication on top of OAuth (proof of identity).
  • Authorization must be resource-scoped: not "are you logged in" but "may YOU touch THIS".
  • Always enforce authorization on the server, UI hiding is not security.
  • Short-lived tokens, verify signatures, decide permissions close to the resource.

Where to go next

Authn and authz are the spine of every secure system, but they live inside a bigger picture. Two natural next steps: see how these checks fit into a hardened API surface, and how identity stops being a one-time gate and becomes a continuous decision.

Want to go deeper?

This article covers concepts taught hands-on in the Cloud Engineer and DevOps career paths, with real terminal labs, production scenarios, and structured lessons.