Authentication, Authorization, and Session State

LESSON

Backend Development Foundations

010 25 min beginner

Authentication, Authorization, and Session State

By the end of this lesson, you will be able to...

  • Distinguish authentication, authorization, and session state in one backend request.

  • Trace how a login becomes identity evidence on later requests.

  • Recognize bugs caused by stale roles, missing resource checks, and unsafe token handling.

Idea in one sentence: Authentication proves who the caller is, authorization decides what that caller may do, and session state carries identity evidence between requests with trade-offs around revocation, scale, and freshness.

Core Insight

The previous lesson designed JSON errors that help clients understand failures such as 401 and 403.

Now we need to explain what those failures mean.

Imagine the orders API has a staff dashboard:

POST   /admin/products
DELETE /admin/products/:id
GET    /admin/orders/:id

A staff member signs in. Later, they call:

DELETE /admin/products/prod_9

During review, the team finds a bug: a logged-in support user can delete products, even though only catalog admins should do that.

The login worked. The caller was real. The session was valid.

The failure was different:

The backend proved who the caller was.
It did not prove that this caller may delete this product.

The naive idea is:

If the user is logged in, the request is allowed.

That is not enough.

A better model separates three questions:

Authentication: Who is this caller, and how did they prove it?
Authorization: May this caller perform this action on this resource?
Session state: What evidence carries identity between requests?

These questions often run near each other in a framework pipeline. They are not the same decision.

The Small Situation

Use one request:

DELETE /admin/products/prod_9
Cookie: session_id=sess_abc123

The product rule is narrow:

Only an active staff user with catalog_admin permission may deactivate a product.
Physical deletion is not allowed if the product appears in shipped order history.

The visible pieces are:

Plain meaning:

Authentication gets an identity into the request.

In this scenario:

The session cookie may let the backend recover staff_user_id=17.

Technical name:

That identity proof step is authentication.

Plain meaning:

Authorization decides whether an authenticated actor may do one specific thing.

In this scenario:

staff_user_id=17 may view orders but may not delete products.

Technical name:

That permission decision is authorization.

A Worked Request Trace

Trace a successful catalog admin request:

Input:
  DELETE /admin/products/prod_9
  Cookie: session_id=sess_abc123

Step 1: read session evidence
  request includes sess_abc123
  at this point, it is only a string from the client

Step 2: validate the session
  session exists
  session is signed or stored correctly
  session is not expired
  session is not revoked

Intermediate state:
  backend can attach principal staff_user_id=17

Step 3: load current actor facts
  staff user is active
  roles or permissions are loaded
  tenant or organization boundary is known if relevant

Step 4: identify action and resource
  action = deactivate_product
  resource = product prod_9

Step 5: evaluate policy
  actor has catalog_admin permission
  product exists
  product can be deactivated under business rules

Intermediate state:
  decision = allow

Step 6: run handler
  handler changes product state
  handler records audit event

Output:
  safe response and audit trail

Now compare the broken path:

Input:
  DELETE /admin/products/prod_9
  Cookie: session_id=sess_support_8

Step 1: validate session
  session is valid

Step 2: attach principal
  staff_user_id=21
  role=support_agent

Step 3: run handler
  product is deleted

Naive failure:
  the request was authenticated
  but no authorization decision checked delete_product on prod_9

This bug can hide in testing because unauthenticated users are blocked. The missing case is an authenticated user without the right permission.

So far, the important distinction is:

Authentication says "we know who this is."
Authorization says "this known actor may do this action here."

Check: A logged-in support user can delete products, but only catalog admins should do that. Which boundary failed?

Think first, then reveal.

Answer: Authorization. Authentication proved the caller was a real support user. Authorization should have denied the delete_product action for that actor and resource.

Session State Carries Evidence

HTTP requests are separate. The user does not send their password with every click.

After login, the backend gives the client some session evidence. Later requests present that evidence.

Common shapes:

server-side session
  client stores opaque session_id
  server stores session data

signed or encrypted cookie
  client stores protected session data
  server verifies the cookie

bearer token
  client sends token in Authorization header
  server validates or introspects token

With a server-side session:

client cookie:
  session_id=sess_abc123

server store:
  sess_abc123 -> user_id=17, expires_at, revoked=false

This makes central revocation easier. Mark the session revoked, and later requests fail. The cost is that requests depend on a session store.

With a self-contained signed token:

token contains:
  user_id=17
  roles=["catalog_admin"]
  expires_at=...

The server may validate it without a session database lookup. That can scale well. The cost is freshness. If the user loses catalog_admin, the old token may still say they have it until the token expires or the backend checks freshness elsewhere.

With bearer tokens, remember the plain rule:

Whoever presents the token can act as that token until it expires or is rejected.

That means raw tokens should not appear in ordinary logs, error messages, analytics events, screenshots, or support tickets.

The trade-off is visible:

server-side session -> easier revocation, more shared state
self-contained token -> easier stateless validation, harder immediate revocation
short lifetime -> less stale access, more refresh complexity
long lifetime -> smoother UX, more risk if stolen or stale

Check: A user loses catalog_admin, but their token still contains roles=["catalog_admin"] for six hours. What is the main problem?

Think first, then reveal.

Answer: Policy freshness. The token may be valid cryptographically while its authorization claims are stale. The system needs shorter expiry, role lookup, revocation, token versioning, or another freshness check for sensitive actions.

Authorization Needs Resource Context

Roles are useful. They are not the whole policy.

Consider:

GET /customers/cus_42/orders
DELETE /admin/products/prod_9
PATCH /admin/users/17/roles

Each request needs more than "is the caller logged in?"

For customer orders:

actor = customer cus_42
action = read_orders
resource = customer cus_42 orders
decision = allow

But if the same actor changes the URL:

GET /customers/cus_99/orders

the policy should ask whether this actor may read orders for cus_99. If not, deny or hide the resource according to the API's rule.

For product deletion:

actor = staff_user_id=17
action = deactivate_product
resource = product prod_9
context = admin dashboard request
decision = allow or deny

For role changes:

actor = staff_user_id=21
action = grant_catalog_admin
resource = staff_user_id=17 role set
context = request_id, organization, current actor permissions
decision = usually deny unless actor has a very specific permission

A good authorization check has this shape:

actor -> action -> resource -> context -> decision

Putting if user.is_admin inside many handlers is fragile. One handler checks only a broad role. Another forgets ownership. A third uses old policy vocabulary. Consistent policy helpers or a central policy layer make the boundary easier to review and test.

Check: A customer changes /customers/cus_42/orders to /customers/cus_99/orders and sees another customer's orders. Which check failed?

Think first, then reveal.

Answer: Resource-level authorization. The backend authenticated a real customer, but it did not verify that this actor may read orders for cus_99.

Errors, Audits, and Signals

Authentication and authorization should connect to the error contract from the previous lesson.

Common mapping:

missing or invalid identity evidence -> 401-style authentication response
known caller without permission       -> 403-style authorization response
resource hidden by policy             -> sometimes 404 by project rule

The exact response policy is a project decision. The important part is consistency. Clients should know whether to sign in, stop, refresh visible state, or ask for a different permission.

Operators need different evidence. Do not expose sensitive detail to the client, but do record enough safe audit context:

request_id
actor id
action
resource id
decision allow/deny
policy name or reason category
timestamp
session/token family, not raw secret

Authentication and authorization bugs usually come from boundary confusion:

Useful operational signals include authentication failures, expired sessions, revoked sessions, authorization denies by action, suspicious repeated denies, token validation failures, and audit events for sensitive actions.

A Small Boundary Table

When a backend request feels confusing, name the boundary before changing code.

Use this table:

Boundary          Input it trusts carefully          Output it produces
authentication    credential or session evidence     principal or no principal
session state     cookie, session id, token           reusable identity evidence
authorization     actor, action, resource, context    allow or deny decision
audit             safe request and decision facts     reviewable event

The rows should happen in a deliberate order.

Authentication should not quietly grant broad permission. Its output is an actor the backend can reason about. If the request has no valid identity evidence, the handler should not have to guess who the caller is.

Session state should not be treated as a permanent source of truth. It is evidence that must be validated. For low-risk pages, the backend may accept cached identity facts. For sensitive changes, it may reload fresh roles, check token version, require a recent login, or ask a policy service.

Authorization should sit close to the protected action. It needs the real resource, not just the route name. A check that runs before loading prod_9 may know the caller is a catalog admin, but it cannot know whether this particular product can be deactivated under business rules.

Audit should record the decision without leaking secrets. A useful audit event says that actor 17 attempted deactivate_product on prod_9 and the policy allowed or denied it. It should not store raw bearer tokens, passwords, full session cookies, or unnecessary personal data.

This table is also a testing checklist. Good backend tests should include:

That checklist keeps the next lesson grounded. Testing backend behavior is not only about happy paths. It is about proving that each boundary fails closed when its input is missing, stale, or not enough for the requested action.

Trade-offs and Limits

This model improves clarity because each boundary has a job:

authentication -> establish identity
authorization  -> decide permission
session state   -> carry evidence between requests
audit           -> preserve safe decision evidence

It costs design discipline. Every protected action needs a policy decision. Every session style has revocation and freshness trade-offs. Every log line must avoid secrets while still preserving enough investigation context.

This lesson does not solve full identity architecture. It does not choose OAuth, SAML, cookies, JWTs, passkeys, or a specific authorization framework for you. It gives the beginner backend model you need before choosing those tools.

You can see the boundary when:

Those are not one generic "auth problem." They are different boundary failures.

Practice

Review this request:

PATCH /admin/users/17/roles
Cookie: session_id=sess_support_8
Content-Type: application/json

{"add": ["catalog_admin"]}

A good answer should mention:

Model answer:

First validate the session cookie and load the actor. Being logged in is not enough. The backend must ask whether this actor may grant catalog_admin to user 17. That is a sensitive authorization decision, not normal profile editing. If the actor's permissions were embedded in a long-lived token, they may be stale, so the backend may need a current permission lookup or token freshness check. Whether the decision is allow or deny, record safe audit evidence without logging the raw session ID.

Connections

The previous lesson used 401 and 403 as API error categories. This lesson explains the backend decisions behind those responses.

The next lesson turns these boundaries into test questions: which unit, integration, or contract test would catch a missing policy call, stale token assumption, or wrong error contract?

Resources

Key Takeaways

PREVIOUS JSON APIs, Validation, and Error Contracts NEXT Testing Backends: Unit, Integration, and Contract Tests