Session vs Token Authentication: Key Differences

Session auth is stateful (server-side), while token auth (like JWT) is stateless (client-side).

Session-based and token-based authentication

Session-based and token-based authentication are the two dominant approaches to keeping users logged in across multiple requests. Choosing the right one affects your application's security model, scalability, revocation capabilities, and overall architecture in ways that are difficult to change later.

Session-Based Authentication

In session-based authentication, the server is responsible for maintaining state. After a user successfully logs in, the server creates a session record containing relevant user data such as their ID, role, and permissions, and stores it server-side in memory, a database, or a cache like Redis. The server then generates a unique, opaque session ID and sends it to the browser as a cookie. The actual user data never leaves the server. The client only ever holds a short random identifier that points to the real data stored on the server.

On every subsequent request, the browser automatically sends the session ID cookie. The server looks up the session ID in its store, retrieves the associated user data, and uses it to process the request. If the session record is not found or has expired, the server treats the request as unauthenticated. Because the session data lives on the server, it can be modified or deleted immediately, giving the server complete and instant control over every active user session.

  1. The user submits their credentials through a login form
  2. The server verifies the credentials against the user database
  3. The server creates a session record containing the user's ID, role, and any other relevant data, and stores it in its session store
  4. The server generates a random session ID and returns it to the browser as an HttpOnly cookie
  5. The browser automatically includes the session cookie with every subsequent request to the same domain
  6. The server reads the session ID from the cookie, looks up the session record in its store, and uses the stored data to handle the request
  7. When the user logs out, the server deletes the session record. The cookie becomes useless because the ID it contains no longer points to anything.
Session-based authentication flow (Node.js Express with express-session):
// Login endpoint: verify credentials and create session
app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.email);
  const valid = await bcrypt.compare(req.body.password, user.passwordHash);

  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });

  // Store user data in the session (server-side)
  req.session.userId = user.id;
  req.session.role   = user.role;

  res.json({ message: 'Logged in successfully' });
  // Browser receives: Set-Cookie: sessionId=abc123xyz; HttpOnly; Secure; SameSite=Lax
});

// Protected route: look up session on every request
app.get('/dashboard', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  res.json({ userId: req.session.userId, role: req.session.role });
});

// Logout: destroy the session record immediately
app.post('/logout', (req, res) => {
  req.session.destroy();
  res.clearCookie('sessionId');
  res.json({ message: 'Logged out' });
});

Token-Based Authentication (JWT)

In token-based authentication, the server does not store any session state. After login, the server generates a signed JSON Web Token (JWT) that contains the user's identity and claims directly within the token payload. The server signs the token with a secret key or private key and returns it to the client. From that point on, the server stores nothing about the user's session.

The client stores the token, typically in memory for security or in localStorage for persistence, and includes it in the Authorization header of every subsequent request. When the server receives a request, it verifies the token's cryptographic signature using its secret or public key. If the signature is valid and the token has not expired, the server trusts the claims inside the payload and processes the request without any database lookup. Any server that knows the signing key can verify any token independently, which is what makes token-based authentication stateless and horizontally scalable by default.

  1. The user submits their credentials through a login form or API call
  2. The server verifies the credentials against the user database
  3. The server generates a signed JWT containing the user's ID, role, and an expiry timestamp, signs it with its secret key, and returns it to the client
  4. The client stores the token in memory, localStorage, or a cookie depending on the security requirements
  5. The client includes the token in the Authorization header with every subsequent request to protected endpoints
  6. The server verifies the token signature cryptographically. No database lookup is needed. If valid and not expired, the request is processed.
  7. When the access token expires, the client uses a refresh token to obtain a new one without requiring the user to log in again
Token-based authentication flow (Node.js Express with jsonwebtoken):
// Login endpoint: verify credentials and issue JWT
app.post('/login', async (req, res) => {
  const user = await db.findUser(req.body.email);
  const valid = await bcrypt.compare(req.body.password, user.passwordHash);

  if (!valid) return res.status(401).json({ error: 'Invalid credentials' });

  // Sign a token with user data embedded in the payload
  const accessToken = jwt.sign(
    { userId: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { userId: user.id },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  res.json({ accessToken, refreshToken });
});

// Protected route: verify token signature (no DB lookup needed)
app.get('/dashboard', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ userId: decoded.userId, role: decoded.role });
  } catch (err) {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
});

// How the client sends the token with every request:
// Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Session vs Token: Full Comparison

FeatureSession-BasedToken-Based (JWT)
State StorageServer stores session data. Client holds only an opaque session ID.Stateless. The token itself carries all data. The server stores nothing.
ScalabilityMore complex. Multiple servers need access to a shared session store such as Redis to handle any user's requests.Simple. Any server instance can verify any token using the shared secret or public key.
RevocationImmediate. Delete the session record and the user is logged out instantly regardless of what cookie they hold.Difficult. Tokens remain valid until they expire. Requires a blacklist or short expiry with refresh token rotation.
Data Exposed to ClientMinimal. The session ID is opaque and reveals nothing about the user.The token payload is Base64-encoded and readable by anyone who holds it. Never store sensitive data in a JWT payload.
Storage LocationSession ID in an HttpOnly Secure cookie. Session data on the server.Access token in memory or cookie. Refresh token in an HttpOnly cookie for security.
Cross-Domain SupportLimited. Cookies require matching domain or CORS configuration with credentials. Subdomains need explicit Domain attribute.Natural. The Authorization header works across any domain without cookie domain restrictions.
Database Lookup per RequestYes. Every request requires a session store lookup to retrieve user data.No. Token verification is purely cryptographic. No database involved for standard requests.
CSRF VulnerabilityCookies are automatically sent by browsers, making CSRF attacks possible. Requires SameSite attribute and CSRF tokens.Authorization header tokens are not sent automatically, so CSRF attacks do not apply.
XSS VulnerabilityHttpOnly cookies are inaccessible to JavaScript, protecting session IDs from XSS theft.Tokens stored in localStorage are accessible to JavaScript and can be stolen by XSS attacks.
Best ForTraditional server-rendered web applications on a single domain with a small number of serversSPAs, mobile apps, public APIs, microservices, and any system where multiple independent servers need to authenticate the same users

Refresh Tokens and Token Rotation

Access tokens in token-based authentication are kept short-lived, typically 15 minutes to 1 hour, to limit the window of exposure if a token is stolen. But requiring users to log in again every 15 minutes would be unacceptable. Refresh tokens solve this by providing a longer-lived credential used specifically to obtain new access tokens without re-entering credentials.

A refresh token is issued alongside the access token at login and stored securely in an HttpOnly cookie on the client. When the access token expires, the client sends the refresh token to a dedicated endpoint. The server validates the refresh token, issues a new access token, and optionally rotates the refresh token itself by issuing a new one and invalidating the old one. Refresh token rotation means that even if a refresh token is stolen, it can only be used once before it becomes invalid, limiting the damage of a compromise.

Refresh token flow:
// Access token expires after 15 minutes
// Client uses refresh token to get a new access token

app.post('/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken; // stored in HttpOnly cookie
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);

    // Issue a new short-lived access token
    const newAccessToken = jwt.sign(
      { userId: decoded.userId, role: decoded.role },
      process.env.JWT_SECRET,
      { expiresIn: '15m' }
    );

    // Rotate the refresh token (invalidate old, issue new)
    const newRefreshToken = jwt.sign(
      { userId: decoded.userId },
      process.env.REFRESH_SECRET,
      { expiresIn: '7d' }
    );

    res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: true });
    res.json({ accessToken: newAccessToken });

  } catch (err) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Security Considerations

RiskSessionsTokens (JWT)
Token or Session TheftUse HttpOnly and Secure cookie attributes to prevent JavaScript access and transmission over HTTPStore access tokens in memory rather than localStorage. Store refresh tokens in HttpOnly cookies.
CSRF AttacksSet SameSite=Lax or SameSite=Strict on the session cookie. Use a CSRF token for state-changing requests.Not vulnerable to CSRF because tokens are sent in the Authorization header, which browsers do not send automatically on cross-site requests.
XSS AttacksHttpOnly cookies are inaccessible to injected scripts. Session IDs cannot be read by JavaScript even if XSS occurs.Tokens in localStorage are readable by any JavaScript on the page. A successful XSS attack can steal them. Store in memory or HttpOnly cookies instead.
Instant RevocationDelete the session record from the store immediately. The user is logged out on the next request.Use short access token expiry (15 minutes). Maintain a server-side blacklist for tokens that need immediate invalidation. Invalidate the refresh token at the database level.
Sensitive Data ExposureSession data stored server-side is never exposed to the client or interceptable from the token.JWT payload is Base64-encoded, not encrypted. Anyone who holds the token can decode the payload. Never include passwords, PII, or sensitive fields.

Frequently Asked Questions

  1. Is JWT always better than sessions?
    No. JWT is better suited to stateless, distributed architectures where multiple independent servers need to authenticate the same users without sharing a central session store. For a traditional server-rendered web application running on a single domain with a small number of servers, session-based authentication is simpler to implement, easier to reason about, and provides instant revocation without any additional infrastructure. The choice depends on your architecture. Defaulting to JWT because it seems more modern is a common mistake that introduces unnecessary complexity for applications that would be better served by sessions.
  2. Can you log out a user with JWT?
    Not directly in a truly stateless way, because a valid JWT remains valid until its expiry timestamp regardless of any server-side action. The practical solutions are keeping access tokens short-lived (15 minutes is common), storing a token blacklist or denylist server-side containing the IDs of revoked tokens and checking it on each request, and invalidating the refresh token in the database so the user cannot obtain new access tokens. The combination of short access token expiry with refresh token revocation gives you effective logout while keeping the authentication mostly stateless for normal requests.
  3. What is a refresh token and how is it different from an access token?
    An access token is a short-lived JWT that grants access to protected resources. It is sent with every API request in the Authorization header and typically expires after 15 minutes to 1 hour. A refresh token is a long-lived credential, valid for days or weeks, stored in an HttpOnly cookie rather than JavaScript memory. Its only purpose is to obtain new access tokens when the current one expires. By separating these two concerns, the system limits the exposure of the sensitive credential (the refresh token is harder to steal from an HttpOnly cookie) while keeping the credential that is sent frequently (the access token) short-lived so its theft window is small.
  4. Where should I store a JWT token on the client?
    In-memory JavaScript variables are the most secure option for access tokens because they are not accessible after a page refresh, not persisted to disk, and not vulnerable to XSS-based exfiltration from browser storage. The trade-off is that the user must re-authenticate after every page reload unless a refresh token mechanism is in place. localStorage is convenient but means any JavaScript on the page can read the token, making it vulnerable to XSS attacks. An HttpOnly cookie is the most secure persistent option because JavaScript cannot read it, but it requires careful CSRF protection. For most production applications, the recommended pattern is to store the access token in memory and the refresh token in an HttpOnly Secure SameSite=Strict cookie.
  5. Do I need both session-based and token-based auth in the same application?
    In most cases, no. Applications typically choose one approach consistently. However, some architectures use both for different purposes. For example, a web application might use session-based authentication for its server-rendered pages and issue JWTs for a public API that third-party clients consume. A common pattern in hybrid frameworks like Next.js is to use an HttpOnly cookie to store a session or token for the server-rendered portions of the app while the client-side JavaScript uses the same token through a dedicated endpoint. The key is having a clear understanding of why you are using each approach and ensuring the security properties of each are maintained.

Conclusion

Session-based and token-based authentication each solve the same fundamental problem of maintaining user identity across stateless HTTP requests, but they do so in ways that have very different implications for scalability, revocation, security, and infrastructure requirements. Session-based authentication is stateful, simpler to implement for traditional web applications, and provides instant revocation by deleting a server-side record. Token-based authentication is stateless, scales horizontally without a shared session store, and is the natural choice for APIs, SPAs, mobile applications, and microservices. Neither approach is universally superior and the right choice is determined by your application's architecture and requirements. Combining short-lived access tokens with HttpOnly refresh token cookies and rotation gives you most of the scalability benefits of JWT while maintaining practical revocation capabilities. Continue with JWT tokens, OAuth, and authentication vs authorization to build a complete understanding of modern web authentication.