Authentication vs Authorization: Sessions, JWTs, OAuth and OIDC Explained

The difference between authentication and authorization, and how sessions, JWTs, OAuth 2.0, OpenID Connect, RBAC and ABAC work — with security best practices for web apps.

By Danyal Ahmed - Data Scientist · · 5 min read

Authentication and authorization are often mashed together as "auth", but they answer different questions and fail in different ways. Mixing them up leads to some of the most common and damaging security bugs in web applications.

Key takeaways

  • Authentication (AuthN): Who are you? Verifying identity.
  • Authorization (AuthZ): What are you allowed to do? Checking permissions.
  • Sessions (server-side state + cookie) are simple and secure defaults for web apps. JWTs suit stateless APIs and distributed systems but make revocation harder.
  • OAuth 2.0 is for delegated authorization; OpenID Connect (OIDC) adds authentication on top.
  • Most breaches involve authorization failures: checking that a user is logged in, but not that they own the resource.

Authentication: proving identity

Common factors:

  • Something you know: a password or PIN.
  • Something you have: a phone authenticator app, hardware security key or passkey on a device.
  • Something you are: biometrics, usually unlocking a device credential.

Multi-factor authentication (MFA) combines factors. Passkeys (WebAuthn) use public-key cryptography bound to a device and origin, which makes them resistant to phishing. They're the strongest mainstream option today.

Password handling essentials

  • Hash with a slow, salted algorithm designed for passwords (Argon2id, bcrypt or scrypt). Never store plain text or use fast hashes like SHA-256 alone.
  • Rate-limit login attempts and consider breached-password checks.
  • Use generic error messages ("Invalid email or password") to avoid account enumeration.

Keeping users logged in: sessions vs tokens

Server-side sessions

After login, the server creates a session record and sends a random session ID in a cookie.

httpSet-Cookie: sid=9f8a...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=86400
  • HttpOnly: JavaScript can't read it, which limits damage from XSS.
  • Secure: only sent over HTTPS.
  • SameSite: reduces CSRF risk.

Pros: easy revocation (delete the session), small cookie, battle-tested. Cons: requires a session store (Redis or a database) shared across servers.

JSON Web Tokens (JWT)

A JWT is a signed token containing claims:

textheader.payload.signature
{"sub": "user_42", "role": "admin", "exp": 1767225600}

The server verifies the signature and trusts the claims without a database lookup.

Pros: stateless verification; works well across services and domains. Cons: hard to revoke before expiry; claims can go stale (a removed admin keeps admin until the token expires); size overhead.

JWT best practices

  • Keep access tokens short-lived (minutes) and use refresh tokens, stored securely and rotated, for longevity.
  • Always verify the signature and exp, iss and aud. Reject the none algorithm, and pin expected algorithms.
  • Don't put sensitive data in the payload. It's encoded, not encrypted.
  • In browsers, avoid storing tokens in localStorage where XSS can read them. Prefer HttpOnly cookies.
Sessions JWT
State Server-side store Stateless (in token)
Revocation Immediate Hard (short expiry / denylist)
Scaling Needs shared store Easy verification anywhere
Best for Traditional web apps, BFFs Service-to-service, APIs, federated systems

For most web apps, a session cookie via a backend-for-frontend is the simplest secure choice.

OAuth 2.0: delegated authorization

OAuth lets a user grant an application limited access to their resources on another service without sharing their password. "Allow this app to read your calendar" is OAuth.

Roles:

  • Resource owner: the user.
  • Client: the app requesting access.
  • Authorization server: issues tokens after user consent.
  • Resource server: the API holding the data.

The recommended flow for web and mobile apps is Authorization Code with PKCE:

text1. App redirects user to authorization server (with code_challenge)
2. User logs in and consents
3. Authorization server redirects back with a short-lived code
4. App exchanges code (+ code_verifier) for tokens at the token endpoint
5. App calls the API with the access token

For server-to-server access with no user, use the client credentials flow. Avoid the deprecated implicit and password grants.

Important: an OAuth access token says "this client may access these scopes". It doesn't, by itself, tell the client who the user is.

OpenID Connect: authentication on top of OAuth

OIDC adds an ID token (a JWT) describing the authenticated user, plus standard endpoints for user info and discovery. "Sign in with Google/Microsoft/Apple" is OIDC.

  • Use OIDC when you want users to log in with an external identity provider (SSO).
  • Use OAuth scopes when you need to access APIs on the user's behalf.

For B2B products, supporting SAML or OIDC SSO and SCIM provisioning is often a requirement for enterprise customers.

Authorization models

Role-based access control (RBAC)

Users get roles; roles have permissions.

textadmin  → invoices:read, invoices:write, users:manage
member → invoices:read

Simple and understandable. It can explode into dozens of roles as special cases accumulate.

Attribute-based access control (ABAC)

Decisions use attributes of the user, resource and context: "Managers can approve expenses under $5,000 in their own department during business hours." Flexible, but harder to reason about and audit.

Relationship-based access control (ReBAC)

Permissions derive from relationships: "Users can edit documents in folders shared with their team." Good for collaboration products with sharing and hierarchies.

Many real systems combine RBAC for coarse permissions with ownership or relationship checks for individual resources.

The #1 authorization bug

Broken object-level authorization: the API checks that the user is logged in, but not that they can access this specific object.

ts// ❌ Any logged-in user can read any invoice by changing the ID
app.get("/invoices/:id", requireLogin, async (req, res) => {
  res.json(await db.invoices.findById(req.params.id));
});

// ✅ Scope every query to the caller's tenant/ownership
app.get("/invoices/:id", requireLogin, async (req, res) => {
  const invoice = await db.invoices.findOne({
    id: req.params.id,
    organizationId: req.user.organizationId,
  });
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});

Make scoping the default: repository methods that require a tenant or user context, integration tests for cross-tenant access, and code review checklists. See API design best practices. The same principle applies to AI features that call tools.

Security checklist

  • Passwords hashed with Argon2id or bcrypt; MFA or passkeys available
  • Session cookies are HttpOnly, Secure, with an appropriate SameSite value
  • Short-lived access tokens; refresh tokens rotated and revocable
  • JWT verification checks signature, algorithm, exp, iss, aud
  • OAuth uses Authorization Code + PKCE
  • Authorization enforced server-side on every request and object
  • Tenant scoping built into data access
  • Login and token endpoints rate-limited
  • Auth events logged (logins, failures, permission changes)

FAQ

Should I build auth myself or use a provider?

For most teams, an established identity provider or a mature open-source library is safer and faster than building from scratch. You still own authorization logic, which is specific to your domain.

Are JWTs more secure than sessions?

Not inherently. Both are secure when implemented correctly. Sessions make revocation easy; JWTs make stateless verification easy. Choose based on architecture, then follow the best practices for that choice.

What is the difference between OAuth and SSO?

OAuth is a framework for delegated authorization. Single sign-on is a user experience, logging in once to access multiple apps, and is typically implemented with OIDC or SAML.

Where should I store tokens in a single-page app?

Ideally, don't expose long-lived tokens to JavaScript at all. Use a backend-for-frontend that keeps tokens server-side and gives the browser an HttpOnly session cookie. If tokens must live in the browser, keep them short-lived and in memory.