caduh

Session Cookies vs JWTs vs PASETO

11 min read

A practical guide to choosing session cookies, JWTs, or PASETO for web apps, SPAs, mobile apps, and APIs without cargo-culting stateless auth.

Session Cookies vs JWTs vs PASETO

What to use for web apps, SPAs, mobile, and APIs

Goal: help you pick the right session/token approach for first-party web apps, SPAs, mobile apps, and backend APIs without turning auth into a distributed systems problem you didn’t mean to have.


TL;DR

  • For a first-party web app, default to a server-backed session cookie.
  • For a SPA, the safest default is usually a BFF (backend-for-frontend) that gives the browser an HTTP-only session cookie.
  • For mobile apps, use short-lived bearer tokens plus a refresh token stored in the OS secure storage. Use JWT if you need OAuth/OIDC or vendor interoperability.
  • For service-to-service APIs, use JWT when you need standards, gateways, or third-party compatibility. Use PASETO only when you control both ends and want a simpler self-issued token format.
  • PASETO is not “better JWT everywhere.” It is a different token format with different trade-offs.
  • JWT is not a session strategy by itself. It is just a token format. You still need storage, expiry, rotation, revocation, and audience checks.
  • Avoid storing long-lived auth tokens in localStorage unless you have accepted the XSS trade-off on purpose.

1) Start Here: These Solve Different Problems

A lot of auth confusion comes from comparing things that live at different layers.

Session cookies

A session cookie is usually just a browser-friendly way to carry a session identifier. The browser stores the cookie and automatically sends it back to your server. The real session state usually lives server-side in memory, Redis, or a database.

Think:

  • browser transport: cookie
  • source of truth: server
  • revocation: easy

JWT

A JWT is a compact token format for carrying claims. It is commonly signed, sometimes encrypted, and widely used in OAuth and OpenID Connect ecosystems.

Think:

  • transport: usually bearer token
  • source of truth: often inside the token itself
  • revocation: harder unless you add state back

PASETO

PASETO is another token format. It removes some of JWT’s flexibility and replaces it with stricter versioned constructions and two purposes:

  • public = signed with asymmetric keys
  • local = encrypted with a shared symmetric key

Think:

  • transport: usually bearer token
  • source of truth: often inside the token itself
  • interoperability: usually lower than JWT, but simpler when you control the whole system

The key mental model

  • Cookie vs bearer token = how the client sends auth
  • Session vs stateless token = where the auth state lives
  • JWT vs PASETO = token format choice

These are related, but not the same decision.


2) The Default Picks

| Scenario | Default pick | Why | |---|---|---| | Server-rendered web app | Session cookie | Easy revocation, browser-native, least exposure to JavaScript | | SPA with your own backend | BFF + session cookie | Keeps long-lived tokens out of browser JS | | SPA calling third-party APIs directly | JWT access token | Fits OAuth/OIDC and delegated API access | | Native mobile app | JWT or PASETO bearer token | Better fit than cookies; store refresh token in OS secure storage | | Internal service-to-service APIs | JWT or PASETO public | JWT for interoperability; PASETO if self-issued and fully controlled | | Public API / partner integrations | JWT or opaque OAuth token | Better ecosystem support, libraries, and gateway compatibility |

If you need one rule of thumb:

Web browser + first-party app? Choose session cookies.
Bearer tokens crossing service boundaries? Choose JWT unless you have a strong reason to use PASETO.


3) Session Cookies: The Best Default for First-Party Web Apps

If you run a classic web app, an admin dashboard, a customer portal, or a same-origin app with a backend, session cookies are usually the boring, correct answer.

3.1 Why they work so well

  • HTTP-only cookies are not readable by JavaScript.
  • Browsers understand cookies natively.
  • You can store only a random session ID in the cookie and keep all real session data on the server.
  • Logout and revocation are simple: delete or invalidate the server-side session.
  • You can rotate session IDs without redesigning every client.

3.2 What you should set

Good baseline:

  • HttpOnly
  • Secure
  • SameSite=Lax or SameSite=Strict
  • Path=/
  • Prefer a __Host- prefix if possible

Example:

Set-Cookie: __Host-SID=3l5gS8...; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800

3.3 What you still need

Cookies are not magic. You still need:

  • CSRF protection for state-changing requests
  • short idle and absolute session lifetimes
  • session rotation on login / privilege change
  • server-side invalidation on logout or suspicious activity

3.4 When cookies become awkward

  • your frontend and API live on different sites with tricky cross-site rules
  • you need third-party API access delegated by the user
  • you are building a native mobile app or public API client ecosystem
  • you need token inspection by gateways or other services outside the browser session model

4) JWT: Great for Standards and Service Boundaries

JWT wins when you need interop, standards, and claims that travel with the request.

4.1 Where JWT shines

  • OAuth / OIDC ecosystems
  • API gateways and service meshes
  • machine-to-machine auth
  • mobile apps calling APIs
  • multi-service systems where resource servers validate tokens independently

4.2 Why teams like it

A signed JWT can often be validated locally without calling the issuer on every request. That makes it attractive for distributed APIs.

Typical claims:

  • iss — issuer
  • sub — subject
  • aud — audience
  • exp — expiry
  • iat — issued at
  • jti — unique token ID
  • scopes / roles / permissions

4.3 What JWT does not give you for free

  • immediate revocation
  • safe browser storage
  • sensible audience boundaries
  • small tokens
  • claim freshness

Once a JWT is issued, every service that trusts it may continue to trust it until expiry unless you add extra machinery:

  • short TTLs
  • revocation lists
  • token introspection
  • key rotation
  • session versioning

4.4 JWT is a bad excuse for fake statelessness

A lot of systems choose JWT because “stateless scales”, then immediately add:

  • refresh token tables
  • revocation tables
  • device/session tracking
  • token blacklists
  • re-auth requirements

At that point, they have reintroduced state anyway, just with more complexity.

4.5 Browser warning

If your JWT lives in JavaScript-accessible storage, XSS can often turn into token theft. That is why browser apps frequently end up safer with a BFF and session cookies instead of raw bearer tokens in the frontend.


5) PASETO: Good for Self-Issued Tokens, Not for Every Stack

PASETO exists because JWT’s algorithm flexibility and ecosystem history created a lot of footguns. PASETO simplifies that by using explicit versions and purposes instead of a free-form alg choice.

5.1 The two PASETO modes

  • v4.public — signed tokens using asymmetric cryptography
  • v4.local — encrypted tokens using a shared symmetric key

5.2 Where PASETO fits well

  • you control issuer and verifier
  • your auth is internal to your own product
  • you want simpler crypto choices
  • you do not need broad OAuth/OIDC or gateway compatibility

5.3 Where PASETO is awkward

  • third-party identity providers
  • OAuth/OIDC vendor ecosystems
  • gateways, proxies, and middleware that expect JWT or opaque OAuth tokens
  • teams that rely on wide library/tool compatibility across languages and vendors

5.4 Important local vs public rule

Many teams see “encrypted token” and reach for local. Be careful.

With local, every verifier that has the shared key can typically also mint valid tokens. That is fine when one tightly controlled service owns both issuance and verification. It is a bad fit when many independent services should verify but must not mint.

In distributed systems, public is usually the safer default because verifiers only need the public key.

5.5 PASETO is usually a product-level choice, not an ecosystem choice

Use PASETO when the token never needs to leave your controlled world. If the token must work cleanly with identity providers, API gateways, or external standards, JWT usually wins.


6) What to Use by App Type

6.1 Web apps (server-rendered or same-origin app + backend)

Use session cookies.

This is the best default for:

  • admin portals
  • SaaS dashboards
  • internal tools
  • e-commerce accounts
  • apps where browser and backend are part of the same system

Good shape:

  • random session ID in cookie
  • server-side session store
  • CSRF protection
  • short idle timeout
  • session rotation after login

6.2 SPAs

For SPAs, the answer depends on architecture.

Best default: SPA + BFF

The browser talks to your backend, and your backend holds the real tokens or session state. The browser gets an HTTP-only session cookie.

Why this is strong:

  • less token exposure to JavaScript
  • easier logout/revocation
  • easier integration with same-origin APIs
  • fewer token-handling mistakes in the browser

Direct browser-to-API SPA

Use this when you truly need it, especially for delegated OAuth APIs.

Good shape:

  • Authorization Code + PKCE
  • short-lived access token
  • refresh token handled very carefully, or avoided in browser JS if possible
  • strict CSP and XSS hygiene
  • narrow scopes and audiences

6.3 Mobile apps

Use bearer tokens, not cookie sessions, as the default.

Recommended shape:

  • short-lived access token
  • refresh token in OS secure storage
  • backend validates iss, aud, exp, and token type
  • device/session tracking on the server

Choose format like this:

  • JWT if you need OAuth/OIDC, external IdPs, or broad library support
  • PASETO public if you self-issue tokens and control both sides

6.4 Backend APIs / service-to-service

Cookies do not belong here.

Use:

  • JWT if you need gateways, standard claims, external systems, or vendor tooling
  • PASETO public if it is internal-only and you control the whole trust boundary
  • opaque token + introspection if central revocation matters more than self-contained claims

6.5 Public APIs / partner ecosystems

JWT or opaque OAuth token usually wins.

Why:

  • better standards alignment
  • easier integration with existing auth servers
  • more compatible docs, SDKs, and API gateways

PASETO can work technically, but it is rarely the path of least resistance when other organisations have to integrate with you.


7) Browser Reality: XSS vs CSRF Is the Real Trade-Off

This is the part many auth debates skip.

Session cookies

  • Pros: can be HttpOnly; browser sends them automatically
  • Cons: automatic sending means CSRF matters

Bearer tokens in JS

  • Pros: explicit Authorization: Bearer ... control; not auto-sent by the browser
  • Cons: if JavaScript can read them, XSS can often steal them

The practical answer

  • For first-party browser apps, many teams should prefer cookies + CSRF defenses over putting long-lived bearer tokens in browser storage.
  • HttpOnly helps against token theft, but it does not stop XSS from doing other bad things.
  • SameSite=Lax is a sensible baseline; Strict is stronger but may break some flows.

8) Decision Matrix

| Requirement | Session Cookie | JWT | PASETO | |---|---|---|---| | Best for browser-first first-party app | Yes | Sometimes | Rarely | | Easy logout / revocation | Yes | No, not by default | No, not by default | | Works well with OAuth / OIDC vendors | No | Yes | No | | Easy local validation across many services | No | Yes | Yes | | Safe from JS access with HttpOnly | Yes | No, unless hidden behind BFF | No, unless hidden behind BFF | | Compact ecosystem support | Browser-native | Excellent | Smaller | | Best for mobile app bearer auth | Rarely | Yes | Yes | | Best for internal self-issued tokens | Sometimes | Yes | Yes | | Best for third-party partners | No | Yes | Rarely |


9) Copy-Paste Patterns

9.1 Session cookie pattern

// POST /login
const sessionId = crypto.randomUUID();
await redis.set(`sess:${sessionId}`, JSON.stringify({
  userId,
  roles: ["admin"],
  createdAt: Date.now(),
}), { EX: 60 * 30 });

res.setHeader("Set-Cookie", [
  `__Host-SID=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800`
]);
res.status(204).end();

9.2 JWT pattern

const token = signJwt({
  iss: "https://auth.example.com",
  sub: user.id,
  aud: "api.example.com",
  scope: "orders.read orders.write",
  exp: Math.floor(Date.now() / 1000) + 900,
  jti: crypto.randomUUID(),
}, privateKey, { kid: "2026-03" });

const claims = verifyJwt(token, publicKey, {
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

9.3 PASETO public pattern

const token = signPasetoV4Public({
  iss: "https://auth.example.com",
  sub: user.id,
  aud: "api.example.com",
  exp: nowPlusMinutes(15),
  scope: "orders.read",
}, privateKey);

const claims = verifyPasetoV4Public(token, publicKey, {
  issuer: "https://auth.example.com",
  audience: "api.example.com",
});

9.4 BFF pattern for SPAs

Browser <-> Your BFF: session cookie
BFF <-> Auth server / APIs: bearer tokens

That lets your backend deal with refresh tokens, token exchange, and revocation, while the browser gets the simpler and safer cookie model.


10) Common Mistakes

Mistake 1: “JWT scales better, so use it for everything”

JWT is useful, but a first-party web app usually gets simpler logout, safer browser handling, and fewer surprises with session cookies.

Mistake 2: Storing long-lived tokens in localStorage

This is often the wrong trade-off for browser apps. If XSS lands, the attacker may steal the token.

Mistake 3: Using PASETO because “JWT is insecure”

JWT is widely used and can be deployed safely. PASETO removes some sharp edges, but it also gives up ecosystem compatibility.

Mistake 4: Using PASETO local everywhere

Shared symmetric verification means any holder of the key may also mint tokens. That is not what you want in many multi-service systems.

Mistake 5: Forgetting revocation design

Stateless-looking auth still needs state somewhere:

  • logout
  • compromised device handling
  • refresh token rotation
  • suspicious session termination

Mistake 6: Treating token contents as secret

Signed JWTs are not encrypted by default. Anyone holding the token can often read its claims. Keep sensitive data out of them.


11) What “Good” Looks Like

Good for a web app

  • random session ID cookie
  • Redis or DB-backed session store
  • HttpOnly, Secure, SameSite
  • CSRF defense
  • session rotation and logout invalidation

Good for a SPA

  • BFF if possible
  • otherwise OAuth code flow + PKCE
  • short-lived access tokens
  • no long-lived secrets in browser JS

Good for mobile

  • bearer tokens only
  • refresh token in platform secure storage
  • device/session visibility in backend
  • remote revoke support

Good for internal APIs

  • short-lived access tokens
  • strict aud and iss checks
  • key rotation
  • logs contain token IDs, not tokens
  • clear decision: JWT for interop, PASETO for controlled internal-only flows

12) The 2 AM Runbook

  1. Who is the client?

    • browser -> prefer session cookie
    • mobile -> bearer token
    • backend service -> bearer token
  2. Do we need standards / external IdP / API gateway support?

    • yes -> JWT
    • no -> PASETO is an option
  3. Does the browser need to hold the token directly?

    • no -> use BFF + cookie
    • yes -> keep access tokens short-lived and accept the XSS storage trade-off explicitly
  4. Do we need immediate logout or revoke?

    • yes -> server session or opaque token with introspection is easier
    • maybe -> JWT/PASETO can work with short TTLs plus server-side controls
  5. Are many services verifying the token but only one should mint it?

    • yes -> JWT or PASETO public
    • no -> PASETO local may be acceptable in a tightly controlled design
  6. What usually broke?

    • cookie missing Secure / SameSite
    • CSRF defenses missing
    • JWT aud mismatch
    • clock skew on exp / nbf
    • refresh token not rotated or revoked
    • PASETO shared-key sprawl

Appendix: Fast Rules of Thumb

  • Web app you own end-to-end? Session cookie.
  • SPA? Prefer BFF + session cookie.
  • Mobile? JWT unless you fully control both ends and want PASETO.
  • OAuth / OIDC / partner ecosystem? JWT.
  • Internal-only self-issued bearer tokens? PASETO is worth considering.
  • Need easy revoke and session admin? Server-backed sessions beat stateless tokens.

Pick the boring default first. Most auth incidents do not come from choosing the wrong acronym; they come from choosing a complex model you never needed.