Authentication, Authorization, and Session State
LESSON
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:
- Credentials: proof used at login, such as password plus a second factor, SSO, or another login method.
- Principal: the authenticated actor, such as
staff_user_id=17. - Session evidence: a cookie, session ID, or bearer token sent on later requests.
- Claims: facts about the principal, such as user ID, roles, tenant, or expiry.
- Policy: the rule used to decide allow or deny.
- Resource: the thing being acted on, such as product
prod_9. - Context: request-time facts, such as route, method, request ID, IP risk, time, or tenant.
- Audit event: a record of actor, action, resource, decision, and evidence safe enough for later review.
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:
- Treating "logged in" as enough for admin actions.
- Checking a broad role but missing resource ownership.
- Trusting stale role claims in a long-lived token.
- Logging session IDs or bearer tokens.
- Applying authorization in the UI but not the API.
- Forgetting to audit sensitive allow and deny decisions.
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:
- no session evidence,
- invalid or expired session evidence,
- authenticated actor without permission,
- authenticated actor with permission on the wrong resource,
- authenticated actor with permission on the right resource,
- stale role or revoked-session case for sensitive actions.
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:
- a valid user gets
403, - a missing session gets
401, - a stale token still carries an old role,
- a customer can access another customer's resource,
- an audit log cannot explain who did what.
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:
- The session cookie is authentication evidence, but it must be validated before it proves an actor.
- The actor is the staff user behind
sess_support_8. - The action is granting
catalog_admin. - The resource is user
17and their role set. - The policy question is whether this actor may grant this specific role.
- A freshness risk is that the actor's own roles may have changed after the session or token was issued.
- The audit event should include actor ID, target user ID, action, role change intent, decision, request ID, and timestamp.
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
- [REFERENCE] OWASP Authorization Cheat Sheet
- Link: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- Focus: Use this for the principle that authorization should be checked on every request and at the right granularity.
- [REFERENCE] OWASP Session Management Cheat Sheet
- Link: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Focus: Read for session identifiers, expiration, renewal, transport, and storage risks.
- [DOC] MDN: HTTP Authentication
- Link: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Authentication
- Focus: Connect credentials, challenges, and
401behavior to HTTP.
- [REFERENCE] OAuth 2.0 Security Best Current Practice
- Link: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- Focus: Skim for bearer-token risks, token handling, and modern OAuth security guidance.
Key Takeaways
- Authentication proves identity; authorization decides whether that identity may perform one action on one resource.
- Session state is evidence carried between requests, not permission by itself.
- Roles help, but sensitive actions usually need actor, action, resource, and context.
- Session and token designs trade off scale, revocation, freshness, and theft impact.
- Safe audit evidence helps operators investigate decisions without exposing secrets.
← Back to Backend Development Foundations