Software Architecture

Authentication & Session Design — Server Session vs JWT

Authentication & Session Design — Server Session vs JWT

About this article

This article is the Software Architecture category’s final installment (7th) in the Architecture Crash Course for the Generative-AI Era series, covering server-side authentication and session.

The question: “how do we hold the session post-login?” Server session vs JWT, OAuth 2.0/OIDC, safe Cookie attributes — landing on the practical axis: “same-domain = Cookie, cross-domain = JWT.”

CoveredNot covered (-> other articles)
Server session, JWT, OAuth, server-side Cookie configAuth strength (MFA / Passkey / IDaaS), authorization (IAM), browser defense (XSS / CSP)

Before you read this

This article is mostly about development: programs and APIs. If IT vocabulary is unfamiliar, reading the primer "Programs and APIs" first makes it far easier to follow. You can also look anything up in the glossary as you read.

What is authentication and session design in the first place

Authentication and session design is “deciding the mechanism for remembering logged-in users.”

Imagine entering an amusement park. You show your ticket at the gate for identity verification (authentication) just once, then inside the park you just show your wristband (session) to ride attractions. Whether the wristband is managed server-side or carried by the visitor — this choice is the difference between server-session and JWT approaches.

Authentication and authorization are separate things, and mixing them up is where much of the trouble starts.

Implementing login often confuses authentication (AuthN) and authorization (AuthZ). They serve completely different purposes and must be separated at design time.

Authentication confirms “who you are” — verifying identity via password or Passkey. Authorization decides “what you can do” — determining whether logged-in users can “see the admin panel” or “edit this data”, etc.

TermSubstanceEnglish (abbreviation)
AuthenticationConfirm who you areAuthentication (AuthN)
AuthorizationDecide what you can doAuthorization (AuthZ)

“Logged in, so they can do admin operations” is the classic confusion. Design AuthN and AuthZ as separate things.

Why authentication and session design matters

A mistake in session management is a fatal security hole. If you store JWT in the wrong place, XSS steals it; if you forget Cookie attribute settings, CSRF exploits it — session design mistakes directly lead to unauthorized access.

Microservices multiply the complexity of authentication. With microservices, every service must verify the user independently. Deciding “who verifies, and how is session state shared” at design time is essential; bolting it on later touches every endpoint.

The two main session-management approaches

The mechanism letting users continue using APIs without re-login post-authentication is session management. It splits broadly into “keeping state on the server” and “keeping state on the client.”

ApproachState locationTrait
Server session (Cookie)Server (Redis, etc.)Immediate revocation, mature track record
JWT (token)ClientStateless, easy to scale

Neither is superior; the default split: “same-domain ordinary web app = server session”, microservice-crossing or external-integration APIs = JWT.

Server sessions — the default within one domain

Server session: on successful login, the server issues a random session ID and stores it in the client’s Cookie. Subsequent requests carry the Cookie; the server looks up Redis or similar to retrieve user info from the session ID.

The traditional approach with strengths of immediate revocation (delete from Redis to invalidate), localizing leak impact, and mature libraries. Downsides: a separate session store to operate, and on horizontal scaling all servers need to share the store.

StrengthsWeaknesses
Immediate revocation (logout is simple)Session store required
Mature track record and librariesStateful (server-side state)
Easier to localize leak damageSharing required at scale

For same-domain general web apps and SPAs, server session is sufficient and safe.

JWT — the tool for crossing domains

JWT packages user info itself into a signed token handed to the client. Servers authenticate by verifying the signature each time, so they hold no state — stateless is the headline trait.

In multi-microservice configurations, each service can authenticate independently. Watchpoints: immediate revocation is hard (token valid until expiry), private-key leak enables impersonation of all users, payload is base64-decodable by anyone.

Header.Payload.Signature
─────  ────────  ─────────
algo  user info  tamper-detection
                 signature

JWT brings its own set of traps.

When adopting JWT, understand the pitfalls. The “difficulty of revocation” is JWT’s fundamental nature; workarounds need separate mechanisms.

  • Cannot be revoked: JWT is valid until expiry, so leaks can’t be invalidated immediately. Solution: “short-lived Access Token + Refresh Token in httpOnly Cookie” is the default.
  • Private-key leak is fatal: a leaked signing secret lets attackers forge tokens for any user.
  • localStorage storage is XSS-vulnerable: XSS (Cross-Site Scripting — embedding malicious scripts) easily steals tokens. Save in httpOnly Cookies for safety.
  • Don’t put secrets in the payload: base64 isn’t encryption. Anyone can decode it.

“Access Token (short-lived) + Refresh Token (long-lived, httpOnly)” is the modern JWT operational default.

OAuth 2.0 / OIDC

OAuth 2.0 is the protocol for “delegating authorization.” Widely used as a mechanism for handing API-access permissions to third-party apps, like “give this app read access to Google Drive.”

OpenID Connect (OIDC) is an extension standardizing authentication on top of OAuth — features like “Sign in with Google” are realized via OIDC.

These are standardized protocols, so the modern mainstream is delegating to authentication services (Auth0 / Firebase Auth / Cognito / Okta) rather than implementing in-house. In-house implementation has high vulnerability risk; including MFA and Passkey support, the operational load is large.

ProtocolRole
OAuth 2.0Delegate API-access permissions to third-party apps
OpenID Connect (OIDC)OAuth-based extension for authentication

Examples: Google / Apple / GitHub login, internal SSO (Okta, Auth0).

For authentication, delegation to external services is the standard. Avoid in-house implementation for safety.

The main flows divide as follows.

OAuth has multiple flows by use. For web and mobile apps, “Authorization Code + PKCE is the de facto standard; new adoption of other flows is rare.

The decision axis: “who protects what.” The Authorization Code flow receives the authorization code via the browser and exchanges it for tokens server-side — a two-step mechanism. Tokens themselves don’t end up in browser history or URLs, lowering leak risk.

Combining with PKCE means even if the authorization code is intercepted mid-flight, attackers can’t exchange it for tokens. This makes it safe for SPAs and mobile — “clients that can’t hold secrets.”

OAuth 2.0 + PKCE Authorization Code Flow De facto standard for SPA/mobile. Safe method where tokens aren't exposed in browsers User / Browser App Server Authorization Server Google / Auth0 / Okta 1 Login Button Pressed 2 Authorization Request + code_challenge (PKCE) 3 Login Screen Displayed → User Authenticates 4 Authorization Code Returned (Not Left in URL) 5 Authorization Code + code_verifier → Token Exchange 6 Access Token + Refresh Token 7 Session Issued via HttpOnly Cookie Role of PKCE Prevents token exchange even if authorization code is intercepted Deprecated Flows Don't use Implicit Flow / Password Grant For new implementations, PKCE Authorization Code is the only choice. Delegating auth to external services is standard Store JWT in HttpOnly Cookie, not localStorage. This is the required baseline from 2024 onward
FlowUse
Authorization Code + PKCE (Proof Key for Code Exchange — code-interception protection)SPA / mobile / web app (standard)
Client CredentialsServer-to-server (no user)
Device CodeTVs, CLIs — input-difficult devices
Implicit FlowDeprecated (security issues)
Password GrantDeprecated (passing password to a third party by design)

For new builds, PKCE-paired Authorization Code only. Don’t use Implicit / Password Grant from old sources.

How to choose — decide it from the domain structure

A traditional web application on one domain. Server session (Cookie). The most mature approach; logout is also simple. Default to this without a special reason.

An SPA plus API on the same domain. Server session (Cookie). “SPA = JWT” is a misconception. httpOnly Cookies are sufficiently safe.

Microservices, authenticating across services. JWT + Refresh Token. Stateless property raises inter-service independence.

Federating external logins — Google, GitHub and the rest. OpenID Connect (OIDC). Delegate to Auth0 or Firebase Auth — practical.

Internal corporate systems where SSO is required. OIDC + SSO platform (Okta / Auth0 / Entra ID). Permission changes from HR moves can be unified.

Whichever you choose, the cookie configuration decides how safe it actually is.

Whether using server session or putting JWT in Cookies, Cookie-attribute settings drive safety. These prevent the majority of security incidents.

AttributeRole
HttpOnlyNot readable from JavaScript (XSS protection)
SecureSent only over HTTPS
SameSite=Lax or StrictPrevent CSRF
Domain / PathLimit send scope to minimum
Max-Age / ExpiresMake expiry explicit

“HttpOnly + Secure + SameSite=Lax” is the minimum line for web apps. Skipping is unacceptable.

Implementation in Express / Hono / Next.js typically looks like:

// Recommended: pass via Cookie (XSS-protected)
res.cookie("session", token, {
  httpOnly: true,           // not readable from JS (XSS protection)
  secure: true,             // HTTPS only
  sameSite: "lax",          // CSRF protection
  maxAge: 60 * 60 * 1000,   // 1 hour
  path: "/",
});

// Not recommended: localStorage (one XSS = leak)
localStorage.setItem("token", token);

When using JWT, store in HttpOnly Cookies rather than localStorage. Refresh Tokens go in a separate Cookie or server-side KV store. Since 2024 security guidelines, “JWT + localStorage” is effectively forbidden.

Numeric gates for session and token lifetimes

Note: industry rates as of April 2026. Periodic refresh required.

Session design decided “casually” always produces vulnerabilities; set specific numerical baselines at the start. Industry defaults:

SettingRecommendedReason
Access Token (JWT) expiry15 minutesMinimize leak damage
Refresh Token (httpOnly Cookie) expiry14-30 daysBalance with user convenience
Session Cookie expiry30 minutes - 24 hours (depends on business)By business form
Session ID entropy128+ bitsUnguessable length
JWT signing algorithmRS256 / EdDSAHS256 is symmetric; key distribution care
JWT none algorithmAbsolutely forbidden2018 library vulnerabilities widespread
Refresh Token RotationEnableTheft detection invalidates all sessions
CSRF tokenSameSite=Lax + CSRF tokenDefense in depth

JWT algorithm: use RS256 (RSA signing) or EdDSA, and never allow the none algorithm. 2018 saw multiple JWT-library vulnerabilities permitting none.

Refresh Tokens issue new values on each use (Rotation); reuse of old values invalidates the entire session — modern default.

Three scenarios

If you are building solo or at a startup

Delegating to Clerk, Auth.js or Supabase Auth is the only choice worth making. Writing session management, token rotation and OAuth integration yourself is the single biggest time sink in solo development. As for the method, an httpOnly cookie server session is the default — the important thing is not to fall for the “it is an SPA, so JWT misunderstanding.

Personal / Startup: Ship in One Month Is Correcten.senkohome.com/arch-intro-case-startup/

If you are a small or mid-size SaaS

Use Auth0 or Cognito, and split by structure: server sessions where everything is on one domain, and JWT with refresh token rotation once services are separated. Hold the numeric gates — a 15-minute access token, a 14- to 30-day refresh token with rotation enabled — and for B2B, check SAML and OIDC support when you select the IdP.

Small-Mid SaaS - Lean on Managed and Run with Few Peopleen.senkohome.com/arch-intro-case-saas/

If you are a large enterprise

Integrating with an SSO platform (Okta or Entra ID) over OIDC is the premise. Doing so centralises the permission changes that follow staff moves, and structurally prevents accounts of departed employees being left in place. Conversely, giving each internal system its own independent authentication database is a breeding ground for security incidents and should be avoided absolutely.

Large-Enterprise Core: Design That Holds Up for Yearsen.senkohome.com/arch-intro-case-enterprise/

AI decision axes — What the AI can write, and where it drops the ball

Session implementations AI writes correctly, and where it drops

AI accurately writes the entire OAuth 2.0 Authorization Code + PKCE flow. code_verifier generation faithful to RFC 7636, code_challenge calculation, redirect to authorization endpoint, POST to token endpoint — this sequence is almost never wrong.

The problem is operational settings. Specific areas commonly missed in AI-generated session code:

  • Cookie attribute SameSite=Lax not set (CSRF protection ineffective)
  • Refresh Token expiration and Rotation implementation omitted
  • Logout API to invalidate server-side Refresh Token not written
  • HttpOnly not set, leaving tokens JavaScript-accessible

These aren’t areas AI “can’t write” — they’re areas it omits unless explicitly instructed. Design that assumes catching these via code review and security-test checklists is necessary.

AI utilization patterns when using IDaaS SDKs

When using auth SDKs like Auth.js (formerly NextAuth), Clerk, or Firebase Auth, AI generation accuracy becomes very high. The reason is simple: these SDKs have rich documentation with abundant samples in training data.

When delegating auth implementation to AI, having it write SDK setup and middleware configuration is safer and more accurate than having it write custom session management. For Auth.js, it can generate end-to-end: providers array config, JWT customization in callbacks, and protected-path specification in middleware.

SDK-Based Authentication Architecture with Auth.js (NextAuth) Delegate auth to SDK, don't implement yourself. A standard config with high AI generation accuracy Next.js Application Auth.js Configuration providers: [Google, GitHub] callbacks: { jwt, session } secret: env.AUTH_SECRET Middleware Protected Path Specification /dashboard /api/* Unauthenticated → Redirect Session Management via HttpOnly Cookie (Secure / SameSite=Lax) External Auth Providers Google OIDC GitHub OAuth Auth0 / Okta / Clerk Don't write authentication logic yourself SDK ensures secure implementation OIDC 4 Review Points for AI-Generated Code: localStorage prohibited / 4 Cookie attributes / Revocation API / PKCE support

Reviewing AI-generated authentication code deserves its own checklist.

When reviewing AI-generated session-related code, always check these perspectives:

  • Token storage location — not in localStorage? (single XSS leaks all tokens)
  • Cookie attribute quad-set — are HttpOnly / Secure / SameSite / Path all explicit?
  • Revocation path exists — is there an API call to invalidate Refresh Token on logout?
  • PKCE support — does the OAuth flow generate code_verifier and include code_challenge in the authorize request?

These are not found by functional tests. Even when the auth flow works, tests pass with weak security settings. Combination with static analysis and security scanners is assumed.

Pitfalls and forbidden moves

The typical ways a session implementation goes wrong all lead straight to account takeover and impersonation. Here are the six most dangerous.

Forbidden moveWhy it is bad → what to do instead
Storing a JWT in localStorageXSS steals it trivially → keep it in an httpOnly cookie
Not setting the cookie attributesXSS, eavesdropping and CSRF all become possible → state HttpOnly, Secure and SameSite explicitly
Avoiding cookie sessions because “it is an SPA, so JWT”on one domain a cookie is both safer and simpler → choose from the domain structure
No revocation design for the JWTyou cannot invalidate anything when it leaks → short-lived access token plus refresh token rotation
Adopting Implicit Flow or Password Grant for something newboth have been discouraged since 2020 → use Authorization Code with PKCE
A logout that only deletes the cookiethe refresh token survives on the server → implement a revocation API

For the hard parts of the authentication methods themselves — password storage, MFA, passkeys — see the separate “security architecture” category.

Author’s note — the “JWT is newer and cooler” illusion

A misconception spread during the late-2010s SPA boom: “You’re doing SPA, so JWT in localStorage is modern.” Even for ordinary same-domain web apps, jumping to JWT and avoiding Cookie sessions kept happening.

There are stories of building JWT + localStorage configurations around 2017 with the same assumption, only to be told by a junior reviewer “are you allowed to keep this in localStorage?” during XSS review.

The result: a single XSS leaks all users’ JWTs, and JWT’s nature means no immediate revocation — attackers run wild until expiry. Slack’s 2022 incident of stolen employee tokens via GitHub used standard token authentication; the code worked correctly. Even so, storage location and revocation operations were the weak points exploited — a classic example.

JWT shines for “places where stateless is required” — microservice-crossing, external integration. For ordinary same-domain web apps, it’s overkill. If a server session (Cookie) suffices, that’s safer and simpler. Postponing this judgment leads to a rewrite later.

“Newer = correct” doesn’t apply to authentication. Mature methods continue being chosen for reasons.

What you must decide — what’s your project’s answer?

Articulate your project’s answer in 1-2 sentences for each:

  • Session approach (server session / JWT / hybrid)
  • Cookie attributes (HttpOnly / Secure / SameSite)
  • Access Token expiry / Refresh Token Rotation
  • JWT signing algorithm (RS256 / EdDSA, none forbidden)
  • OAuth flow (Authorization Code + PKCE essentially only)
  • Logout processing (revocation API implementation)
  • Session ID entropy (128+ bits)

Authentication methods (MFA, Passkey, IDaaS selection, password policy) are decision items for the auth-design article in the “Security Architecture” category.

Write your answers down as an ADR. A concrete guide to writing them is here.

[DevOps Architecture] Documentationen.senkohome.com/arch-intro-devops-docs/

Summary

This article covered authentication and session design — server session vs JWT, OAuth/OIDC, Cookie attributes.

Same-domain = Cookie, cross-domain = JWT, external integration = OIDC. Cookie attributes explicit, JWT short-lived + Refresh Token Rotation, OAuth = PKCE only. The realistic answer for session operation at the software-architecture level.

This concludes the “Software Architecture” category’s 8 articles. The next category is “Application Architecture” — class design, domain logic, naming conventions, error handling.

Back to series TOC -> ‘Architecture Crash Course for the Generative-AI Era’: How to Read This Book

I hope you’ll read the next article as well.