Authentication vs Authorization in Depth: Sessions, JWTs, RBAC, ABAC, and Access Control

13 min read

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.

TermMeaning
IdentityThe real-world or system entity being represented
Principal / SubjectThe authenticated actor in the system, such as a user, service account, or API client
RoleA named grouping of responsibilities, such as admin, manager, or accountant
PermissionA specific allowed action, such as invoice.read or user.invite
ResourceThe object being accessed, such as an invoice, tenant, project, or report
PolicyThe 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.read
  • invoice.create
  • invoice.update
  • invoice.delete
  • user.invite
  • report.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.

ModelBest forWeakness
RBACStable job responsibilities and admin-friendly permission managementBecomes awkward for contextual rules
ABACResource-specific, tenant-specific, ownership, state, and limit-based decisionsCan 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

MistakeWhy it is dangerous
Checking only whether the user is logged inAuthenticated users may still be unauthorized
Trusting frontend-hidden buttonsUsers can call APIs directly
Putting permissions only in JWTsPermissions become stale and hard to revoke
Not scoping queries by tenantCross-tenant data exposure
Checking role names everywhereRules become duplicated and inconsistent
Ignoring resource stateUsers can modify locked, approved, or archived records
No audit logsSensitive actions become hard to investigate
Using admin as a bypass everywhereOne 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 appropriate SameSite policy?
  • 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 401 and 403 responses 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

TermSimple meaning
AuthenticationVerifying the identity of the requester
AuthorizationDeciding whether the requester can perform an action
SessionServer-side authentication state referenced by a session ID
JWTSigned token containing claims
Access tokenShort-lived token used to call APIs
Refresh tokenLonger-lived token used to obtain new access tokens
RBACRole-Based Access Control
ABACAttribute-Based Access Control
TenantA customer/account boundary in a multi-tenant system
PolicyA 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

About the author

Suriyaprakash Somu is a full-stack developer from Erode, Tamil Nadu, building production-ready business applications with React, Node.js, Fastify, PostgreSQL and MySQL. He focuses on Access Control, schema-based forms, and reliable backend workflows.