How Authentication Works: Sessions, Tokens, and JWT
Every useful web application eventually asks the same question: who is this user, and what are they allowed to do? Authentication answers the first part. Authorization answers the second. This guide explains how authentication works internally so you can debug login bugs, design APIs safely, and understand why cookies, tokens, and JWTs exist.
You will see authentication in login pages, mobile apps, REST APIs, admin dashboards, and cloud consoles. If you already read our guides on cookies, HTTPS, and REST APIs, this article connects those pieces into one request flow.
What Authentication Actually Means
Authentication is the process of proving an identity. The system starts with an unknown client and ends with a trusted identity such as a user id, email, or service account.
It is not the same as authorization. Authentication says "this request belongs to user 42." Authorization says "user 42 may delete this order." Mixing the two is a common source of security bugs.
In practice, a server rarely re-checks a password on every request. Passwords are expensive to verify and risky to send repeatedly. Instead, the first successful login creates a short-lived proof of identity. Later requests present that proof.
Simple Explanation
Think of a hotel. The front desk checks your ID once (login). You receive a room key (session or token). The key does not contain your passport. It is a small object the hotel recognizes. Housekeeping does not re-check your passport every time you open the door. They check the key.
On the web, the "key" is usually one of these:
- A session id stored in an HTTP cookie
- An opaque access token stored by a client
- A signed JWT that the server can verify without a database lookup
All three solve the same problem: remember a successful login without sending the password again.
How It Works Internally
A typical browser login looks like this:
User | v Browser form (email + password) | v HTTPS POST /login | v Server: hash password, compare to stored hash | v Server: create session or token | v Response: Set-Cookie or JSON token | v Later requests include cookie or Authorization header | v Server: resolve identity, then authorize the action
1. Credentials arrive over HTTPS
The password must travel inside TLS. Without HTTPS, anyone on the network can steal it. After the TLS handshake, the HTTP body can carry JSON or form fields such as email and password.
2. The server verifies the password
Servers must not store raw passwords. They store a slow one-way hash with a unique salt per user. Common algorithms include Argon2id, bcrypt, and scrypt. On login the server hashes the submitted password with the stored salt and compares the results in constant time.
If the hashes match, identity is established. If not, the server returns a generic error. Revealing whether the email exists helps attackers enumerate accounts.
3. The server creates a session or token
After a successful check, the server needs a reusable proof.
Session cookies. The server generates a long random session id, stores {sessionId → userId, expiry, ip or user-agent hints} in Redis or a database, and sends Set-Cookie: session=...; HttpOnly; Secure; SameSite=Lax. The browser stores the cookie and attaches it to later requests for that site. The server looks up the id and loads the user.
Opaque tokens. Same idea, but the client stores the token and sends Authorization: Bearer <token>. Useful for mobile apps and APIs that are not browser-same-site.
JWTs (JSON Web Tokens). A JWT is three Base64url parts: header.payload.signature. The payload can include sub (user id), exp (expiry), and roles. The signature is created with a server secret (HMAC) or a private key (RSA/ECDSA). The server verifies the signature and expiry. It does not need to store every token if it accepts the claims as-is.
4. Every later request re-authenticates the proof
Middleware reads the cookie or Authorization header, validates the proof, and attaches req.user to the request. Route handlers then run authorization checks.
Architecture components
- Credential store: users table with password hashes
- Session store: Redis, database, or signed cookie
- Token issuer: login or OAuth authorization server
- Resource server: your API that trusts the proof
- Optional identity provider: Google, GitHub, or a company SSO system using OAuth 2.0 / OpenID Connect
Sessions vs Tokens vs JWT
| Approach | Where state lives | Revocation | Best fit |
|---|---|---|---|
| Server session + cookie | Server | Delete the session row | Traditional websites |
| Opaque token | Server | Delete or blacklist the token | Mobile and APIs |
| JWT access token | Mostly in the token | Hard until expiry unless you add a denylist or short TTL | Distributed APIs |
JWTs are not automatically more secure. They move state into the token. That makes horizontal scaling easy and logout harder. Short-lived access tokens plus refresh tokens are the usual compromise.
Real-World Examples
Website login. You submit email and password. The server sets an HttpOnly session cookie. Visiting /account sends the cookie automatically. Logout deletes the server session and clears the cookie.
SPA talking to a REST API. A React app posts to /login and receives an access token. It stores the token in memory (safer than localStorage against XSS) and sends Authorization: Bearer on fetch calls. Refresh happens through a separate HttpOnly cookie or a refresh endpoint.
Sign in with Google. Your app never sees the Google password. The user authenticates at Google. Google returns an authorization code. Your backend exchanges the code for tokens using a client secret. OpenID Connect adds an ID token that describes the user.
Internal microservices. Service A calls Service B with a JWT signed by an internal issuer. Service B verifies the signature with a public key and trusts the subject and scopes. No shared session database is required.
Code Example
The following Node-style sketch shows the idea, not a production library. Use well-tested packages in real apps.
// Login: verify password, then issue a session id
app.post('/login', async (req, res) => {
const user = await users.findByEmail(req.body.email);
if (!user) return res.status(401).send('Invalid credentials');
const ok = await argon2.verify(user.passwordHash, req.body.password);
if (!ok) return res.status(401).send('Invalid credentials');
const sessionId = crypto.randomBytes(32).toString('hex');
await redis.setEx('sess:' + sessionId, 60 * 60 * 24, String(user.id));
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 86400000
});
res.json({ ok: true });
});
// Later requests: resolve the session to a user
app.use(async (req, res, next) => {
const id = req.cookies.session;
if (!id) return next();
const userId = await redis.get('sess:' + id);
if (userId) req.user = { id: userId };
next();
});
A JWT version would sign { sub: user.id, exp: now + 15min } instead of writing Redis. The middleware would verify the signature instead of a lookup. You still need a strategy for logout and refresh.
Common Misconceptions
"JWT is more secure than sessions." Security depends on HTTPS, HttpOnly cookies, XSS defenses, short expiry, and secret handling. A stolen JWT is as dangerous as a stolen session id until it expires.
"Put the JWT in localStorage." Any XSS script can read localStorage. HttpOnly cookies cannot be read by JavaScript. If you must store tokens in the page, keep them in memory and use a short lifetime.
"HTTPS is optional on localhost only, so production HTTP is fine." Production login over HTTP leaks passwords and session cookies. Always use TLS.
"Hashing with SHA-256 is enough for passwords." Fast hashes are designed for integrity, not password storage. Use a password-specific KDF such as Argon2id.
"Logout always destroys a JWT." A signed token remains valid until exp unless the server tracks revoked tokens or uses very short access tokens.
"Authentication equals hiding a URL." Secret paths are not authentication. Anyone who finds the URL can call it. Check identity on the server for every sensitive action.
Best Practices and Key Takeaways
- Use HTTPS everywhere that credentials or cookies travel.
- Store passwords with Argon2id or bcrypt, unique salts, and constant-time compare.
- Prefer HttpOnly, Secure, SameSite cookies for browser sessions.
- Keep access tokens short-lived. Use refresh tokens with rotation and reuse detection.
- Validate tokens on the server. Never trust a role field sent by the client in plain JSON.
- Rate-limit login, lock accounts after repeated failures, and log authentication events.
- Separate authentication from authorization. After you know who the user is, check permissions.
- Protect against CSRF on cookie-based sessions and against XSS for any token in JavaScript.
FAQ
What is the difference between authentication and authorization?
Authentication proves identity. Authorization decides whether that identity may perform an action or read a resource.
What is a session cookie?
It is a random id the browser stores and sends with later requests. The server maps that id to a logged-in user in a session store.
What is a JWT?
A JSON Web Token is a signed string with a header, claims payload, and signature. Servers can verify it without looking up every token if they trust the signing key and the expiry.
Should I store JWT in localStorage?
Avoid it when you can. XSS can steal localStorage. Memory storage or HttpOnly cookies reduce that risk.
How does Sign in with Google work?
OAuth 2.0 / OpenID Connect lets Google authenticate the user. Your app receives tokens after a code exchange. You do not receive the Google password.
Why do APIs use Bearer tokens?
APIs often serve browsers, mobile apps, and servers. A header is explicit, works across origins, and does not rely on cookie same-site rules.
How do I log a user out?
Delete the server session or refresh token and clear cookies. For JWTs, wait for expiry or maintain a revocation list and keep access tokens short.
Is Basic Auth still used?
Sometimes for simple machine clients over HTTPS. It sends a Base64 username:password on every request, so it is a poor default for user-facing apps.
Related Articles
- How Cookies and Sessions Work
- How HTTPS Works: TLS Handshake and Encryption
- How REST APIs Work: Architecture Explained for Beginners
- How HTTP Works: Request-Response Cycle Explained
- How Browsers Render Web Pages
Authentication is not a single library call. It is a pipeline: prove identity once, issue a carefully scoped proof, send that proof on later requests, and verify it on the server every time. Once you see that pipeline, login forms, JWT errors, and "unauthorized" API responses become much easier to reason about.