JWT (Bearer tokens) and Session Cookies are two mainstream approaches to authentication. This article explains JWT access/refresh token flows and cookie-backed server sessions (with a session store like Redis), then compares scalability, immediate logout/revocation behavior, and security considerations (XSS, CSRF) to help you choose the right approach for interviews and real projects.
Written by: Chia1104 CC BY-NC-SA 4.0
During my job interviews this year, I was frequently asked one question: "Have you implemented login authentication?" Since I was applying for frontend engineer positions but had backend experience, I typically discussed two approaches: JWT (Bearer token) and Session Cookie. A quick clarification: when I mention "Session" here, I'm referring to backend Sessions used with frontend Cookies—not the browser's one-time session cookies. Since these work together, I often discuss them as a unit, though I was surprised to find that some interviewers weren't familiar with the Session Cookie approach.
JWT has become nearly standard for authentication in modern frontend-backend separation architectures, especially when the frontend is built as a Single Page Application (SPA).
Its core concept is stateless the backend doesn't need to store user login state, the server simply validates the token's validity.
This design makes JWT well-suited for distributed systems or microservice architectures, since there's no need to share session state, reducing server burden.
However, the tradeoff is that once a token is issued, it's difficult to revoke mid-lifecycle unless it expires or is manually blacklisted—a risk point many beginners overlook.
When discussing JWT, the most common implementation is sending it via the Authorization header using the Bearer token format.
A typical request looks like this:
GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer <ACCESS_TOKEN>This access_token can typically be stored directly in localStorage, and the frontend retrieves it to place in the header. To refresh the token, use a refresh_token.
Some argue that storing tokens in cookies with HttpOnly is safer to prevent XSS (Cross-Site Scripting). However, I believe XSS can be prevented directly using CSP (Content Security Policy). After all, Bearer <ACCESS_TOKEN> needs to be manually placed in headers by the frontend—if using HttpOnly, the frontend cannot access it anyway.
In contrast, the traditional Session Cookie mechanism is stateful after login, the server generates a Session ID and returns it to the browser as a cookie.
With each request, the browser automatically attaches this cookie, and the backend matches it against stored session data.
Since state is controlled by the backend, operations like logout, permission changes, and forced logoff are easier to implement. This mechanism remains very stable in single-server architectures or when using Redis for session storage, and it's convenient for managing security (such as setting HttpOnly or Secure flags).
In Session Cookie mode, "refresh" actually means extending the session's validity time on the backend (sliding expiration), rather than issuing a new token like JWT does.
The typical flow works as follows:
At Login: The backend creates a session record (stored in memory or Redis), generates a sessionId, and sets an expiration time (e.g., 30 minutes). It returns Set-Cookie: sessionId=random_string; HttpOnly; Secure; SameSite=Strict to the browser.
With Each Request: The browser automatically sends the sessionId cookie. The backend uses this sessionId to query the session store (e.g., Redis). If data exists and hasn't expired, the user is considered logged in. If "sliding expiration" is enabled: whenever a request is validated, the session's expiration time is extended (e.g., another 30 minutes).
At Logout or Forced Logoff: The backend directly deletes the session data corresponding to that sessionId. Even if the browser still has the old cookie, it cannot find the session and is treated as logged out.
| Aspect | JWT (Bearer Token) | Session Cookie |
|---|---|---|
| State Storage | Stateless, login info in token itself, stored by client | Stateful, login info stored in server's session store, client only stores sessionId |
| Scalability | Suitable for multi-service, cross-domain, microservice distributed architectures—no need for shared session store | Single server or shared sessions via Redis/database works well for typical backend or enterprise internal systems |
| Revocation & Logout | Difficult to revoke immediately in pure stateless implementation; requires blacklist or token versioning mechanisms | Server can delete or mark session immediately; forced logoff and permission updates take effect instantly |
| Security Concerns | If token is stored where JS can access it, XSS attacks can directly steal it; larger size may leak excessive information | Cookie is just a random ID; can be enhanced with HttpOnly, Secure, SameSite attributes for stronger XSS and CSRF protection |
| Use Cases | SPAs, mobile apps, multi-service scenarios, or APIs exposed externally—when you have the capability to design complete token lifecycle and security strategies | Traditional web, backend management systems, enterprise internal systems, or when stable, easily controllable login state is needed |
In the real world, JWT and Session Cookie are rarely a simple "either-or" choice. More often, they're mixed and integrated into larger architectures: for example, web interfaces use Session Cookies to manage login state, while API Gateways or internal microservices use JWT to exchange authorization information.
When a project is small with only a single backend and simple deployment—Session Cookie is often the more stable choice and easier for teams to understand and maintain. Conversely, when systems begin splitting into services, implementing frontend-backend separation, or even opening APIs to third parties, JWT's "self-describing, verifiable across multiple services" characteristics become extremely valuable.