Authentication vs Authorization in Depth: Sessions, JWTs, RBAC, ABAC, and Access Control
Authentication and authorization are often discussed together because they happen close to each other in a request flow. In a business application, separating them is not academic: a logged-in user can still be outside the tenant, missing the required permission, or unable to change a record because its current state forbids the operation.
Authentication answers: who is this user?
Authorization answers: what is this user allowed to do?
Confusing them creates fragile systems: routes that only check login status, admin panels hidden only in the UI, JWTs treated as permission databases, or tenant data exposed because the code verified identity but forgot the resource boundary.
This is Part 1 of the Production Backend Systems series. It focuses on the identity and access-control layer behind real applications: sessions, JWTs, RBAC, ABAC, multi-tenant checks, frontend permissions, and backend enforcement.
Quick Answer
Authentication verifies identity. Authorization checks permissions.
Request arrives
↓
Authentication: identify the user
↓
Authorization: check whether that user can perform this action on this resource
↓
Business logic runs only if both pass
Example:
Authentication: This request belongs to user 42.
Authorization: User 42 can update invoice 991 because they are an accountant in the same tenant.
A production backend should enforce both. The frontend can hide buttons and improve UX, but the backend must be the source of truth for access control.
Authentication vs Authorization
Authentication is about proving identity. Common examples:
- Email and password login
- OAuth login with Google, GitHub, or Microsoft
- Magic links
- One-time passwords
- Passkeys and WebAuthn
- API keys for machine clients
Authorization is about deciding access after identity is known. Common examples:
- Can this user view this page?
- Can this user create an invoice?
- Can this manager approve this purchase order?
- Can this tenant admin invite users only inside their tenant?
- Can this API key read analytics but not write billing settings?
Authentication usually happens early in the request. Authorization often happens closer to the resource because permissions depend on the action and the specific object being accessed.
GET /tenants/acme/invoices/991
Authentication:
Which user made this request?
Authorization:
Is this user allowed to read invoice 991 inside tenant acme?
Identity, Principal, Subject, Role, and Permission
Access-control discussions become clearer when the words are precise.
| Term | Meaning |
|---|---|
| Identity | The real-world or system entity being represented |
| Principal / Subject | The authenticated actor in the system, such as a user, service account, or API client |
| Role | A named grouping of responsibilities, such as admin, manager, or accountant |
| Permission | A specific allowed action, such as invoice.read or user.invite |
| Resource | The object being accessed, such as an invoice, tenant, project, or report |
| Policy | The rule that decides whether access is allowed |
Roles are not permissions by themselves. A role is a convenient way to group permissions. The backend should usually evaluate permissions or policies, not just role names spread across random route handlers.
Bad pattern:
if (user.role === 'admin') {
updateInvoice(invoiceId, data)
}
Better pattern:
authorize(user, 'invoice.update', invoice)
updateInvoice(invoice.id, data)
The second version makes the action and resource explicit. It is easier to test, audit, and extend.
Sessions vs JWTs
Sessions and JWTs are both ways to keep a user authenticated after login. They differ in where the authentication state lives.
Session-Based Authentication
With server-side sessions, the browser stores a session ID, usually in a secure cookie. The server stores session data in a database, Redis, memory store, or another session backend.
Browser cookie:
session_id=abc123
Server session store:
abc123 → userId: 42, expiresAt: ...
Advantages:
- Easy to revoke immediately
- Small cookie payload
- Permissions can be reloaded from the database
- Good fit for traditional web apps and dashboards
Tradeoffs:
- Requires server-side session storage
- Needs shared storage across multiple server instances
- Cross-domain and mobile usage can require extra design
JWT-Based Authentication
A JWT is a signed token that contains claims. The server verifies the signature and reads the claims without necessarily looking up a session record.
JWT claims:
sub: user_42
tenantId: tenant_acme
exp: 1784462400
Advantages:
- Stateless verification
- Useful across services
- Common for APIs, mobile apps, and service-to-service flows
- Can carry simple identity claims
Tradeoffs:
- Harder to revoke immediately unless you maintain revocation state
- Can become stale if roles or permissions change
- Large tokens can bloat requests
- Dangerous when developers put too much authorization state inside the token
JWTs are not automatically more secure than sessions. They are just a different transport and verification model. Security depends on expiry, storage, signing, audience checks, issuer checks, transport security, and backend authorization.
Access Tokens and Refresh Tokens
Modern token systems often use two token types.
Access token: short-lived token used to call APIs.
Refresh token: longer-lived token used to obtain new access tokens.
Login succeeds
↓
Issue access token: expires in 10 minutes
Issue refresh token: expires in days/weeks, stored carefully
↓
API request uses access token
↓
When access token expires, refresh token requests a new one
The access token should be short-lived because it is sent often. The refresh token should be protected more carefully because it can create more access tokens.
Common mistakes:
- Long-lived access tokens with no revocation story
- Refresh tokens stored in unsafe browser storage
- No rotation or reuse detection
- No device/session list for users to revoke active logins
- Treating token presence as permission to access every resource
Even with perfect token handling, authorization checks still need to happen on the backend.
RBAC Explained
RBAC (Role-Based Access Control) assigns permissions through roles.
User → Role → Permissions
Suriya → Accountant → invoice.read, invoice.create, invoice.update
RBAC works well when responsibilities are stable and easy to name.
Example roles:
- Owner
- Admin
- Manager
- Accountant
- Support agent
- Viewer
Example permissions:
invoice.readinvoice.createinvoice.updateinvoice.deleteuser.invitereport.export
RBAC is simple to explain, easy to show in admin screens, and practical for many business applications.
The problem appears when role names start carrying too much hidden meaning.
manager can approve purchase orders below 50,000
manager can view only their department
manager can edit only draft records
regional manager can approve different limits
At that point, the system is no longer pure RBAC. It needs attributes and policies.
ABAC Explained
ABAC (Attribute-Based Access Control) evaluates access using attributes of the user, resource, action, and environment.
Allow invoice.update when:
user.tenantId === invoice.tenantId
AND user.department === invoice.department
AND invoice.status === 'draft'
ABAC handles conditions that roles alone cannot express cleanly.
Useful attributes:
- User tenant
- User department
- User region
- Resource owner
- Resource status
- Approval amount
- Time of day
- Subscription plan
- Feature flag state
Example:
can(user, 'purchaseOrder.approve', purchaseOrder) {
return user.tenantId === purchaseOrder.tenantId &&
user.approvalLimit >= purchaseOrder.amount &&
purchaseOrder.status === 'pending'
}
ABAC is powerful, but it can become hard to reason about if policies are scattered everywhere. The goal is not to make every route a custom policy. The goal is to centralize policy decisions so they are explicit and testable.
RBAC vs ABAC
RBAC and ABAC are not enemies. Most production systems use both.
| Model | Best for | Weakness |
|---|---|---|
| RBAC | Stable job responsibilities and admin-friendly permission management | Becomes awkward for contextual rules |
| ABAC | Resource-specific, tenant-specific, ownership, state, and limit-based decisions | Can become complex without structure |
A practical model:
RBAC decides baseline capability:
Accountant has invoice.update
ABAC decides context:
Only invoices in same tenant, only draft invoices, only assigned department
Example:
authorize(user, 'invoice.update', invoice)
// Internally:
// 1. Does the user's role include invoice.update?
// 2. Is the invoice in the same tenant?
// 3. Is the invoice editable in its current status?
This keeps permission management understandable while still protecting real business boundaries.
Multi-Tenant Authorization
Multi-tenant systems add one rule that should almost never be optional:
Every tenant-owned resource must be scoped to the tenant.
Authentication tells you the user is valid. It does not automatically prove they can access a resource in another tenant.
Bad pattern:
const invoice = await db.invoice.findById(invoiceId)
authorize(user, 'invoice.read', invoice)
If the lookup is not tenant-scoped, a bug in authorize can expose cross-tenant data.
Better pattern:
const invoice = await db.invoice.findFirst({
where: {
id: invoiceId,
tenantId: user.tenantId,
},
})
if (!invoice) throw notFound()
authorize(user, 'invoice.read', invoice)
This gives you two layers:
- Data access is tenant-scoped
- Policy access is permission-scoped
In sensitive systems, it is better for unauthorized cross-tenant records to look like they do not exist. Returning 404 instead of 403 can avoid leaking whether another tenant's record ID is valid.
Frontend Permission Checks vs Backend Enforcement
Frontend permission checks are useful for UX. They are not security boundaries.
The frontend can:
- Hide buttons
- Disable menu items
- Avoid showing impossible workflows
- Display role-specific navigation
- Prevent obvious accidental actions
The backend must:
- Verify identity
- Check permissions
- Scope data access
- Validate resource state
- Enforce tenant boundaries
- Write audit logs for sensitive actions
If a user cannot click a button in the UI but can still call the API manually, the system is not secure.
Frontend check: should this button be visible?
Backend check: is this action allowed?
Both are valuable. Only one is authoritative.
Where Authorization Should Live
Authorization should be close enough to business logic that it has the resource context, but centralized enough that rules are not duplicated across every handler.
Weak pattern:
// route A
if (user.role !== 'admin') throw forbidden()
// route B
if (!['admin', 'manager'].includes(user.role)) throw forbidden()
// route C
if (user.role === 'viewer') throw forbidden()
Better pattern:
authorize(user, 'invoice.update', invoice)
authorize(user, 'report.export', report)
authorize(user, 'user.invite', tenant)
The route handler stays readable, while policy logic lives in one place.
A simple structure:
routes/controllers
parse request, load resource, call use case
services/use cases
run business workflow
authorization/policies
answer can user do action on resource?
data access
enforce tenant scoping and query constraints
There is no single perfect architecture, but duplicated role checks spread across routes usually become a maintenance problem.
Common Access-Control Mistakes
| Mistake | Why it is dangerous |
|---|---|
| Checking only whether the user is logged in | Authenticated users may still be unauthorized |
| Trusting frontend-hidden buttons | Users can call APIs directly |
| Putting permissions only in JWTs | Permissions become stale and hard to revoke |
| Not scoping queries by tenant | Cross-tenant data exposure |
| Checking role names everywhere | Rules become duplicated and inconsistent |
| Ignoring resource state | Users can modify locked, approved, or archived records |
| No audit logs | Sensitive actions become hard to investigate |
Using admin as a bypass everywhere | One compromised admin account becomes catastrophic |
The most damaging authorization bugs are usually not complex cryptographic failures. They are ordinary business-rule mistakes: wrong tenant, wrong owner, wrong status, wrong role, or missing backend check.
Production Checklist
Use this checklist before shipping an access-controlled feature.
Authentication
- Are passwords hashed with a modern password hashing algorithm?
- Are cookies
HttpOnly,Secure, and configured with appropriateSameSitepolicy? - Do sessions or tokens expire?
- Can users revoke active sessions/devices?
- Are refresh tokens rotated if token auth is used?
- Are issuer, audience, expiry, and signature verified for JWTs?
Authorization
- Does every protected API check authorization on the backend?
- Are data queries tenant-scoped where required?
- Are resource ownership and status checked?
- Are role/permission definitions centralized?
- Are policy tests written for allow and deny cases?
- Are sensitive actions audit logged?
- Does the API return safe errors that do not leak cross-tenant records?
Frontend
- Does navigation reflect permissions?
- Are disabled actions explained clearly?
- Does the frontend handle
401and403responses gracefully? - Does it avoid storing sensitive tokens in unsafe places?
The Boundary Between Identity and Permission
The short explanation:
Authentication verifies who you are. Authorization decides what you can access.
The production questions are:
- Authentication state is carried through sessions, cookies, API keys, or tokens
- JWTs are not permission systems by themselves
- Authorization must consider action, resource, tenant, owner, and state
- Frontend checks improve UX but backend checks enforce security
- RBAC handles broad responsibility, ABAC handles contextual policy
- Multi-tenant systems must scope database queries, not just UI routes
FAQ
Is JWT authentication better than session authentication?
Not automatically. JWTs are useful for stateless APIs and distributed systems, but sessions are easier to revoke and often simpler for web apps. The better choice depends on the application, clients, infrastructure, and revocation needs.
Should permissions be stored inside a JWT?
Small, stable claims can be stored in a JWT, but detailed permissions can become stale. If permissions change frequently or depend on resource state, load or evaluate them on the backend during authorization.
Is hiding a button enough to protect an action?
No. Hiding buttons improves user experience, but the API must still enforce authorization. Users can send requests without using your UI.
What is the difference between RBAC and ABAC?
RBAC grants access based on roles. ABAC grants access based on attributes of the user, action, resource, and environment. Many real systems combine both.
Should unauthorized resources return 403 or 404?
Use 403 when the user is allowed to know the resource exists but cannot perform the action. Use 404 when revealing the resource existence would leak information, especially across tenant boundaries.
Where should authorization logic live?
Keep it centralized enough to avoid duplicated role checks, but close enough to resource loading that policies can use resource data. A common pattern is route handler loads resource, then calls authorize(user, action, resource).
Glossary
| Term | Simple meaning |
|---|---|
| Authentication | Verifying the identity of the requester |
| Authorization | Deciding whether the requester can perform an action |
| Session | Server-side authentication state referenced by a session ID |
| JWT | Signed token containing claims |
| Access token | Short-lived token used to call APIs |
| Refresh token | Longer-lived token used to obtain new access tokens |
| RBAC | Role-Based Access Control |
| ABAC | Attribute-Based Access Control |
| Tenant | A customer/account boundary in a multi-tenant system |
| Policy | A rule that decides whether access is allowed |
Final Mental Model
Authentication gets you a principal. Authorization decides what that principal can do.
Identity verified
↓
Principal created
↓
Resource loaded with tenant scope
↓
Policy checks action + resource + attributes
↓
Allowed request reaches business logic
If you remember only one rule, remember this:
Login is not permission. Every sensitive backend action needs an authorization decision.
Series: Production Backend Systems
Part 1 of 3