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.”
| Covered | Not covered (-> other articles) |
|---|---|
| Server session, JWT, OAuth, server-side Cookie config | Auth strength (MFA / Passkey / IDaaS), authorization (IAM), browser defense (XSS / CSP) |
What is authentication and session design in the first place
Authentication and session design is, in a nutshell, “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.
Why authentication and session design matters
Session-management mistakes create fatal security holes
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 authentication complexity
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.
Authentication vs authorization
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.
| Term | Substance | English (abbreviation) |
|---|---|---|
| Authentication | Confirm who you are | Authentication (AuthN) |
| Authorization | Decide what you can do | Authorization (AuthZ) |
“Logged in, so they can do admin operations” is the classic confusion. Design AuthN and AuthZ as separate things.
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.”
| Approach | State location | Trait |
|---|---|---|
| Server session (Cookie) | Server (Redis, etc.) | Immediate revocation, mature track record |
| JWT (token) | Client | Stateless, easy to scale |
Neither is superior; the default split: “same-domain ordinary web app = server session”, microservice-crossing or external-integration APIs = JWT.
Server-session approach
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.
| Strengths | Weaknesses |
|---|---|
| Immediate revocation (logout is simple) | Session store required |
| Mature track record and libraries | Stateful (server-side state) |
| Easier to localize leak damage | Sharing required at scale |
For same-domain general web apps and SPAs, server session is sufficient and safe.
JWT (JSON Web Token) approach
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 pitfalls
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.
| Protocol | Role |
|---|---|
| OAuth 2.0 | Delegate 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.
Major OAuth flows
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.”
sequenceDiagram
participant U as User
participant C as Client<br/>(SPA/mobile)
participant A as Auth server
participant R as Resource server<br/>(API)
U->>C: Login
C->>C: Generate code_verifier<br/>code_challenge = SHA256
C->>A: Auth request<br/>+ code_challenge
A->>U: Login screen
U->>A: Credentials
A->>C: Auth code (short-lived)
C->>A: Token exchange<br/>code + code_verifier
A->>A: Verify SHA256(code_verifier)<br/>matches code_challenge
A->>C: Access Token + Refresh Token
C->>R: API call<br/>+ Access Token
R->>C: API response
| Flow | Use |
|---|---|
| Authorization Code + PKCE (Proof Key for Code Exchange — code-interception protection) | SPA / mobile / web app (standard) |
| Client Credentials | Server-to-server (no user) |
| Device Code | TVs, CLIs — input-difficult devices |
| Deprecated (security issues) | |
| Deprecated (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.
Selection by case
Traditional web app (same domain)
Server session (Cookie). The most mature approach; logout is also simple. Default to this without a special reason.
SPA + API (same domain)
Server session (Cookie). “SPA = JWT” is a misconception. httpOnly Cookies are sufficiently safe.
Microservices / cross-service authentication
JWT + Refresh Token. Stateless property raises inter-service independence.
External login federation (Google, GitHub, etc.)
OpenID Connect (OIDC). Delegate to Auth0 or Firebase Auth — practical.
Internal corporate systems (SSO required)
OIDC + SSO platform (Okta / Auth0 / Entra ID). Permission changes from HR moves can be unified.
Cookie configuration
Whether using server session or putting JWT in Cookies, Cookie-attribute settings drive safety. These prevent the majority of security incidents.
| Attribute | Role |
|---|---|
| HttpOnly | Not readable from JavaScript (XSS protection) |
| Secure | Sent only over HTTPS |
| SameSite=Lax or Strict | Prevent CSRF (Cross-Site Request Forgery — fake requests exploiting login state) |
| Domain / Path | Limit send scope to minimum |
| Max-Age / Expires | Make 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.
Session / token expiry numeric gates
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:
| Setting | Recommended | Reason |
|---|---|---|
| Access Token (JWT) expiry | 15 minutes | Minimize leak damage |
| Refresh Token (httpOnly Cookie) expiry | 14-30 days | Balance with user convenience |
| Session Cookie expiry | 30 minutes - 24 hours (depends on business) | By business form |
| Session ID entropy | 128+ bits | Unguessable length |
| JWT signing algorithm | RS256 / EdDSA | HS256 is symmetric; key distribution care |
JWT none algorithm | Absolutely forbidden | 2018 library vulnerabilities widespread |
| Refresh Token Rotation | Enable | Theft detection invalidates all sessions |
| CSRF token | SameSite=Lax + CSRF token | Defense 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.
Session / token operational traps
Common session-implementation failure patterns. All directly enable account takeover and impersonation.
| Forbidden move | Why |
|---|---|
| JWT in localStorage | Easily stolen via XSS. httpOnly Cookie is the rule |
| Cookie without HttpOnly / Secure / SameSite | XSS, eavesdropping, CSRF all succeed |
| ”SPA = JWT” misconception, avoiding Cookie sessions | Same-domain Cookie session is safer and simpler |
| JWT without revocation design | Leaks can’t be invalidated. Short-lived Access + httpOnly Refresh is the default |
Allowing JWT none algorithm | Forgery without signature verification possible. 2018 library vulnerabilities widespread |
| Adopting OAuth Implicit Flow / Password Grant for new builds | Deprecated since 2020. Authorization Code + PKCE is standard |
| No Refresh Token Rotation | Theft cannot be detected. Reissuing on each use is modern default |
| Weak Session ID entropy | Guessing-based takeover. 128+ bit randomness required |
| Default Cookie attributes | Without explicit settings, neither Secure nor HttpOnly applies. Always set explicitly |
| Logout only deletes Cookie | Server-side Refresh Token survives. Revocation API required |
| Giving up on JWT revocation and not implementing Rotation | Short-lived Access Token + Refresh Token Rotation effectively enables immediate revocation; it’s a design issue, not JWT’s fate |
| Treating authentication and authorization as the same design | ”Logged in = full permissions” is textbook confusion; AuthN and AuthZ require separate design |
Authentication-method-specific traps (password storage, MFA, Passkey) are in the “Security Architecture” category. This article is limited to session-tech traps.
AI decision axes
| AI-era favorable | AI-era unfavorable |
|---|---|
| OAuth / OIDC standard-flow implementation | Custom token design |
| Short-lived Access + httpOnly Refresh | Long-lived, localStorage |
| Cookie attributes explicit in code (HttpOnly, etc.) | Default-reliance, implicit settings |
| SDK calls (Auth.js, Clerk SDK) | DIY session management |
- Decide approach by domain structure — same-domain Cookie, cross-domain JWT.
- Make Cookie attributes explicit — HttpOnly + Secure + SameSite=Lax is the minimum line.
- JWT: short-lived Access + Refresh Token Rotation — standard answer to non-revocability.
- OAuth: Authorization Code + PKCE only — don’t pick Implicit / Password Grant.
”JWT is new and cool” illusion (industry case)
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,
noneforbidden) - 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.
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.
📚 Series: Architecture Crash Course for the Generative-AI Era (24/89)