JWT vs OAuth vs Session — Which Auth Approach Should You Use?
A developer on Reddit asked: "I am building a SaaS app with a React frontend and a Node.js API. Should I use JWT, OAuth, or sessions?"
Every reply told them something different. Use JWT, it is stateless. No, use sessions, they are more secure. Actually, OAuth is the standard. Wait, OAuth is not an auth mechanism. Use Passport.js. Use Auth0. Use Firebase Auth.
The confusion is understandable. These three terms — JWT, OAuth, and session authentication — operate at different levels of the auth stack, and they are not mutually exclusive. You can (and often do) use all three together.
This article untangles them. By the end, you will know exactly which approach fits your application and how to combine them effectively.
What Each Term Actually Means
Before comparing, we need precise definitions.
Server-Side Sessions
The traditional approach: the server creates a session record in a database or cache, returns a session ID (a random opaque string) to the client, and the client sends that session ID on every request. The server looks up the session data on each request.
// Session-based auth (simplified)
app.post('/api/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
const sessionId = crypto.randomUUID();
// Store session data server-side
await redis.set(`session:${sessionId}`, JSON.stringify({
userId: user.id,
role: user.role,
createdAt: Date.now(),
}), { EX: 86400 }); // 24-hour expiry
// Send session ID to client as httpOnly cookie
res.cookie('session_id', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 86400000,
});
res.json({ message: 'Logged in' });
});
app.get('/api/me', async (req, res) => {
const sessionId = req.cookies.session_id;
if (!sessionId) return res.sendStatus(401);
const sessionData = await redis.get(`session:${sessionId}`);
if (!sessionData) return res.sendStatus(401);
const session = JSON.parse(sessionData);
res.json({ userId: session.userId, role: session.role });
});
Characteristics: Server holds state. Client holds an opaque reference. Session can be revoked instantly by deleting the server record.
JWT Authentication
The stateless alternative: the server signs a JSON payload containing the user's identity and permissions. The client holds the complete token. The server verifies the signature on each request without touching a database.
// JWT-based auth
const jwt = require('jsonwebtoken');
app.post('/api/login', async (req, res) => {
const user = await verifyCredentials(req.body.email, req.body.password);
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ token, expiresIn: 900 });
});
app.get('/api/me', (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) return res.sendStatus(401);
try {
const payload = jwt.verify(authHeader.split(' ')[1], process.env.JWT_SECRET);
res.json({ userId: payload.userId, role: payload.role });
} catch {
res.sendStatus(401);
}
});
Characteristics: Server holds no session state. Token is self-contained. Revocation requires additional infrastructure (see How to Invalidate JWTs on Logout).
OAuth 2.0
OAuth is not an authentication mechanism. It is a delegated authorization protocol. It solves the problem of "I want to let a third-party app access my data without giving it my password."
User -> App: "I want to use you with my Google account"
App -> Google: "Can this user give me access?"
Google -> User: "Do you want to let this app access your data?"
User -> Google: "Yes"
Google -> App: "Here is an access token for this user's data"
App -> Google API: "Here is the token, give me the user's profile"
OAuth defines how tokens are obtained and used, but it does not specify the token format. In practice, OAuth 2.0 implementations almost always use JWTs as the access token format — but this is convention, not requirement.
Side-by-Side Comparison
| Aspect | Server Sessions | JWT Auth | OAuth 2.0 |
|---|---|---|---|
| State location | Server (DB/cache) | Client (token) | Token (typically JWT) |
| Revocation | Instant — delete session record | Requires extra infrastructure | Depends on token type and issuer |
| Scaling | Needs shared session store | Stateless — any server can verify | Same as JWT |
| Database per request | Yes (session lookup) | No (signature verification only) | No |
| Mobile-friendly | Requires cookie support | Works with any transport | Built for mobile/web/CLI |
| Third-party access | No | No | Yes — the entire point |
| Complexity | Low | Medium | High |
| Standardization | No standard | RFC 7519 | RFC 6749 |
When to Use Server Sessions
Server-side sessions are the right choice when you need strong revocation, you are building a single-server or small-scale application, and you do not need to share auth across multiple services.
Good Fit
- Monolithic web applications. A single backend serving a single frontend. Sessions are simple and well-understood.
- Applications requiring instant revocation. Banking apps, admin panels, compliance-heavy systems.
- Server-rendered applications. If you are using Rails, Django, Laravel, or Express with server-side templates, sessions are the natural choice.
- Small teams with limited infrastructure. Sessions do not require a shared secret, key rotation, or token management strategy.
Bad Fit
- Microservices. Each service would need access to the shared session store, creating a single point of failure and a coupling point.
- Mobile applications. Native apps struggle with cookies. Token-based auth works more naturally with the
Authorizationheader. - High-traffic APIs. Every authenticated request requires a session lookup, adding latency and database load.
When to Use JWT Authentication
JWT auth is the right choice when you need stateless verification, are building APIs consumed by multiple clients, or are deploying microservices.
Good Fit
- SPA + API architectures. The React frontend stores the token and sends it with every request. No cookie juggling.
- Microservices. Any service with the public key can verify a JWT without calling a central auth service. This is the killer use case for JWT.
- Mobile applications. JWTs fit naturally into mobile HTTP clients. Store the token in the platform's secure keystore.
- API-first products. Third-party developers receive a JWT and use it to authenticate API calls.
Bad Fit
- Applications with rapid user churn. If users log in and out frequently, JWT revocation becomes a problem (see How to Invalidate JWTs on Logout for solutions).
- Applications requiring strong confidentiality. JWT payloads are readable. If you cannot control what goes into claims, use sessions.
- Teams that cannot manage secrets. JWT security depends entirely on the signing key. If the key leaks, everything is compromised.
When to Use OAuth 2.0
OAuth is the right choice when you need third-party access, want to delegate authentication to an identity provider, or are building an API that other applications consume.
Good Fit
- "Login with Google/Facebook/GitHub." OAuth is how social login works. The identity provider authenticates the user and issues tokens to your application.
- API access for third-party apps. You are building a platform and want other developers to build apps that access user data.
- Enterprise SSO. OAuth 2.0 with OpenID Connect is the standard for single sign-on in enterprise environments.
- Multiple client types. OAuth handles web apps, mobile apps, SPAs, server-to-server, and CLI clients with different grant types for each.
Bad Fit
- Simple first-party authentication. If you only have one app and one API, OAuth adds unnecessary complexity. Use sessions or JWT directly.
- Small internal tools. Setting up an OAuth server for an internal dashboard is overkill.
Combining All Three
Here is the thing most tutorials do not tell you: production systems often use all three together.
A common pattern:
-
OAuth 2.0 with OpenID Connect handles the login flow. The user clicks "Log in with Google." Google authenticates them and redirects to your app with an authorization code.
-
Your auth server exchanges the code for tokens, creates a session or issues a JWT, and returns it to the client.
-
JWT is used as the access token format. Each microservice verifies the JWT independently.
-
Server sessions or refresh tokens handle long-lived access. The access JWT expires in 15 minutes. The refresh token is stored server-side and rotated on each use.
Here is what that looks like in practice:
// Auth server — the central token issuer
app.post('/api/auth/google', async (req, res) => {
// 1. Verify Google's OAuth response
const ticket = await googleClient.verifyIdToken({
idToken: req.body.credential,
audience: GOOGLE_CLIENT_ID,
});
const googleUser = ticket.getPayload();
// 2. Find or create user in your database
const user = await findOrCreateUser(googleUser);
// 3. Issue an access JWT (short-lived)
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
// 4. Issue a refresh token (server-side session)
const refreshToken = crypto.randomBytes(40).toString('hex');
await redis.set(
`refresh:${refreshToken}`,
JSON.stringify({ userId: user.id }),
{ EX: 30 * 24 * 60 * 60 } // 30 days
);
res.json({ accessToken, refreshToken });
});
// Microservice A — verifies JWT without calling auth server
app.get('/api/orders', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
const payload = jwt.verify(token, ACCESS_SECRET);
// payload.sub = user ID — no DB call to auth server
// ... fetch orders for this user
});
// Auth server — refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
const session = await redis.get(`refresh:${refreshToken}`);
if (!session) return res.sendStatus(401);
const { userId } = JSON.parse(session);
// Rotate refresh token
await redis.del(`refresh:${refreshToken}`);
const newRefreshToken = crypto.randomBytes(40).toString('hex');
await redis.set(
`refresh:${newRefreshToken}`,
JSON.stringify({ userId }),
{ EX: 30 * 24 * 60 * 60 }
);
// Issue new access token
const accessToken = jwt.sign(
{ sub: userId },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken, refreshToken: newRefreshToken });
});
Decision Framework
Here is a flowchart in text form:
- Do you need third-party app access? → Use OAuth 2.0.
- Are you building a simple monolithic app? → Use server sessions. They are simpler and provide instant revocation.
- Are you building a SPA + API or microservices? → Use JWT for access tokens, with refresh tokens for session persistence.
- Do you need social login or SSO? → Add OAuth 2.0 / OpenID Connect on top of your chosen session or JWT system.
- Are you building an API for third-party developers? → OAuth 2.0 is mandatory. The token format can be JWT.
Decision Table
| Application Type | Recommended Approach |
|---|---|
| Monolithic web app (Rails, Django, Laravel) | Server sessions |
| SPA (React, Vue, Angular) + API | JWT access + refresh tokens |
| Mobile app (iOS, Android) | JWT access + refresh tokens |
| Microservices | JWT (verified by each service) |
| Third-party API platform | OAuth 2.0 with JWTs |
| Enterprise SSO | OAuth 2.0 + OpenID Connect |
| Internal tool, low users | Server sessions (simplest) |
| Real-time apps (WebSocket) | JWT (stateless, no DB lookup) |
Common Misunderstandings
"OAuth and JWT are alternatives"
They are not. OAuth is a protocol for delegated authorization. JWT is a token format. OAuth 2.0 does not require JWT, but most implementations use JWT as the access token format because it is self-contained and verifiable.
"JWTs replace sessions"
They can, but they do not have to. You can use JWTs alongside sessions (the refresh token is essentially a server-side session). The access token is stateless; the refresh token is stateful. Each handles the part it is good at.
"OAuth is for authentication"
OAuth 2.0 is for authorization. It defines how a third-party app gets permission to access resources. For authentication (verifying who the user is), you need OpenID Connect, which is an authentication layer built on top of OAuth 2.0.
"Sessions are obsolete"
Sessions are not obsolete. They are the right choice for many applications. The "sessions vs JWT" debate is often overblown — the right answer depends on your architecture, team, and requirements.
FAQ
What is the difference between JWT and OAuth?
JWT is a token format — a signed JSON payload. OAuth is a protocol for obtaining and using tokens. JWT can be used without OAuth, and OAuth can use non-JWT tokens, but they are commonly used together.
Can I use JWT without OAuth?
Yes. JWT is just a token format. You can issue your own JWTs directly from your login endpoint without involving OAuth at all. This is the most common pattern for first-party SPA + API applications.
Can I use sessions and JWT together?
Yes. A common pattern is using JWTs as access tokens (stateless, short-lived) with server-side refresh token storage (stateful, revocable). This combines the best of both approaches.
Should I use OAuth for my simple web app?
Probably not. OAuth adds complexity. If you only have one application and one API, JWT or sessions are simpler. OAuth becomes relevant when you need third-party access, social login, or enterprise SSO.
Which is more secure — sessions or JWTs?
Both can be secure. Sessions have simpler revocation (delete the session record). JWTs require additional infrastructure for revocation. But JWTs avoid database lookups on every request. The choice depends on your threat model and architecture.
Is JWT stateless?
Yes. A JWT contains all the information needed to authenticate a request. The server verifies the signature without consulting a database. This makes JWT naturally stateless.
Do OAuth tokens have to be JWTs?
No. OAuth 2.0 defines the protocol for obtaining tokens but does not specify their format. They can be opaque strings (like session IDs), JWTs, or any other format the authorization server chooses.
Summary
The auth approach you choose depends on what you are building:
- Server sessions for monolithic apps where simplicity and instant revocation matter.
- JWT authentication for APIs, SPAs, mobile apps, and microservices where stateless verification matters.
- OAuth 2.0 for third-party access, social login, and enterprise SSO.
The best architectures combine all three: OAuth for the login flow, JWT for the access token format, and server-side sessions or refresh tokens for revocation and long-lived access.
When you need to inspect tokens during development or debug authentication flows, the JWT Decoder decodes any token instantly — regardless of whether it came from your own server, OAuth provider, or a third-party identity service.