JWT, Sessions, and OAuth2: What Each One Is Actually For
JWT, sessions, and OAuth2 are three things that frequently get conflated in discussions about authentication. Developers reach for JWTs because “that’s what everyone uses,” implement OAuth2 without understanding what problem it solves, and wonder why session management is suddenly complicated.
They solve different problems. Understanding which problem each one addresses makes the choice obvious.
Server-Side Sessions
A session is the oldest approach. When a user logs in, the server creates a session record - an object that stores the user’s identity and any relevant state - and stores it somewhere (in memory, a database, Redis). The server sends back a session ID, typically in a cookie. On subsequent requests, the browser sends the cookie, the server looks up the session ID, retrieves the session data, and knows who’s making the request.
# Flask session example
@app.route('/login', methods=['POST'])
def login():
user = verify_credentials(request.form['email'], request.form['password'])
if not user:
return abort(401)
session['user_id'] = user.id # stored server-side, keyed by session cookie
return redirect('/dashboard')
@app.route('/dashboard')
def dashboard():
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
# ...
The server owns the session. This has an important implication: you can invalidate a session instantly. User reports their account compromised? Delete the session record. They’re logged out immediately on the next request.
The downside: sessions require shared state. If you run multiple instances of your server, they all need access to the same session store (Redis is the standard choice). This is a solved problem, but it’s infrastructure you have to operate.
JWTs
A JSON Web Token is a self-contained credential. It encodes a payload (user ID, roles, expiration time) and signs it with a key the server holds. The client receives the token and sends it back on subsequent requests - usually in the Authorization: Bearer <token> header.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzEzNDg1MjAwfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoded, the payload is:
{"user_id": 123, "role": "user", "exp": 1713485200}
The server verifies the signature using its key. If valid, it trusts the payload without any database lookup. This is the key property: stateless verification.
This makes JWTs attractive for distributed systems. Every service can verify a token independently, as long as it has the signing key (or the public key for asymmetric signing). No shared session store required.
The catch: you can’t invalidate a JWT before it expires. Once issued, it’s valid until the exp claim says otherwise. If a user logs out, you can delete the token from the client, but if someone else has a copy, it still works.
The workarounds for this - maintaining a token blacklist, short expiration times with refresh tokens - partially reintroduce the statefulness you were trying to avoid. Short-lived access tokens (15 minutes) combined with longer-lived refresh tokens is the standard pattern, but it adds implementation complexity.
JWTs also have a known class of vulnerabilities. The alg: none attack (where an attacker strips the signature and sets the algorithm to none), weak secret keys, and storing sensitive data in the payload (it’s base64-encoded, not encrypted - anyone can decode it). These are avoidable with proper implementation, but they’re footguns that don’t exist with simple sessions.
Choosing Between Sessions and JWTs
The tradeoff comes down to this: sessions give you instant revocation at the cost of a shared state requirement. JWTs give you stateless verification at the cost of revocation complexity.
For a traditional web application with a server you control: sessions are simpler, more secure by default, and easier to reason about. The “JWTs are scalable, sessions aren’t” argument applies mainly to systems at a scale most applications never reach.
For a microservices architecture where many independent services need to verify identity: JWTs make sense because they eliminate the need for every service to call a central auth service.
For mobile clients and single-page applications where you’re already managing state client-side: JWTs fit naturally.
For APIs consumed by third parties: OAuth2, which is a different thing entirely.
OAuth2
OAuth2 is not an authentication mechanism. It’s an authorization delegation protocol. OAuth2 solves a specific problem: how does a third-party application get permission to access resources on a user’s behalf, without the user giving that application their password?
When you click “Sign in with Google” on some application, what’s happening is an OAuth2 flow:
- The application redirects you to Google with a request for specific permissions (read your email, see your profile, etc.)
- Google shows you what the application is requesting and asks if you consent
- You consent, Google redirects you back with an authorization code
- The application exchanges the code for an access token (and usually a refresh token)
- The application uses the access token to make API calls to Google on your behalf
The user’s password never reaches the third-party application. Access can be scoped to specific resources. Permissions can be revoked independently.
OAuth2 is widely misused as an authentication protocol. “The user has a valid Google token, therefore they’re authenticated” is not what OAuth2 provides on its own. OAuth2 only tells you that the user authorized something. OpenID Connect (OIDC) is the authentication layer built on top of OAuth2 - it adds an ID token (typically a JWT) that contains the user’s identity.
When you see “sign in with” flows, what’s actually happening is OAuth2 + OIDC. The OAuth2 part handles the authorization flow. The OIDC part provides the identity claim.
The Decision Tree
If your application authenticates its own users (not third-party):
- Start with sessions. They’re simpler, revocation works, security defaults are good.
- Switch to JWTs if you genuinely have a distributed verification requirement.
If you’re building a system where third-party applications access your API on behalf of your users:
- Implement OAuth2. This is what it’s for.
If you’re adding “sign in with Google/GitHub/etc.”:
- Use an OAuth2 + OIDC library. Don’t implement this yourself.
The mistake most teams make is choosing a mechanism because it sounds modern, not because it fits the problem. Sessions are not legacy technology. JWTs are not universally better. OAuth2 is not authentication.