Web authentication has evolved dramatically. What used to be "just hash the password and store a session" now spans half a dozen distinct patterns, each with real trade-offs. After building auth systems across multiple projects, here's my breakdown of the five patterns that matter most in 2026.
1. Session-Based Authentication
The OG. Server creates a session, stores it (usually in Redis or a database), and hands the client a cookie.
// Express.js session setup
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: 'redis://localhost:6379' });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));SameSite cookies.
Cons: Requires server-side storage, harder to scale across multiple servers without shared session store, doesn't work well for mobile apps.
When to use: Traditional server-rendered apps, internal tools, admin dashboards where you control the entire stack.
2. JWT (JSON Web Tokens)
Stateless tokens that carry claims. The server signs them, and any service with the public key can verify them.
import jwt from 'jsonwebtoken';
// Signing
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'RS256' }
);
// Verification middleware
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
req.user = jwt.verify(token, process.env.JWT_PUBLIC_KEY);
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}3. OAuth 2.0 / OpenID Connect
Delegated authorization. Let Google, GitHub, or your own identity provider handle the heavy lifting.
// Using openid-client library
import { Issuer } from 'openid-client';
const googleIssuer = await Issuer.discover('https://accounts.google.com');
const client = new googleIssuer.Client({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uris: ['https://myapp.com/callback'],
response_types: ['code'],
});
// Generate auth URL
const authUrl = client.authorizationUrl({
scope: 'openid email profile',
state: crypto.randomUUID(),
nonce: crypto.randomUUID(),
});
// Handle callback
app.get('/callback', async (req, res) => {
const params = client.callbackParams(req);
const tokenSet = await client.callback(
'https://myapp.com/callback',
params,
{ state: req.session.state, nonce: req.session.nonce }
);
const userInfo = await client.userinfo(tokenSet.access_token);
// Create or update user...
});4. Passkeys / WebAuthn
The future that's finally here. Cryptographic key pairs stored on the user's device. No passwords, no phishing.
// Registration (simplified)
const options = await generateRegistrationOptions({
rpName: 'My App',
rpID: 'myapp.com',
userID: user.id,
userName: user.email,
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
});
// Client-side
const credential = await navigator.credentials.create({
publicKey: options,
});
// Server verification
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: options.challenge,
expectedOrigin: 'https://myapp.com',
expectedRPID: 'myapp.com',
});5. Web3 Wallet Authentication
Sign a message with your crypto wallet to prove identity. No central authority involved.
import { verifyMessage } from 'ethers';
// Server generates a nonce
const nonce = `Sign this message to log in to MyApp.\nNonce: ${crypto.randomUUID()}`;
// Client signs with wallet
const signature = await signer.signMessage(nonce);
// Server verifies
app.post('/auth/web3', (req, res) => {
const { address, signature, message } = req.body;
const recovered = verifyMessage(message, signature);
if (recovered.toLowerCase() !== address.toLowerCase()) {
return res.status(401).json({ error: 'Signature verification failed' });
}
// Create session for this address...
});Choosing the Right Pattern
There's no universal best choice. Most production apps combine multiple patterns:
| App Type | Primary | Secondary |
|----------|---------|-----------|
| SaaS B2B | OAuth/OIDC (SSO) | Passkeys + email/password |
| Consumer mobile | Passkeys | OAuth (social login) |
| Internal tool | Session-based | OAuth (company SSO) |
| DApp | Wallet auth | Email link as fallback |
| API platform | JWT | API keys |
Tools That Support Multiple Patterns
If you're building auth from scratch, you'll want a framework or service that handles multiple patterns out of the box. The good options in 2026 include Auth.js (open-source, great for Next.js), Clerk (managed, excellent DX), Auth0 (enterprise-grade), Authon (self-hostable with multi-pattern support), and Keycloak (battle-tested open-source). Pick based on your hosting preference, budget, and how much control you need.
Final Thoughts
The trend is clear: passwords are dying, passkeys are rising, and most apps need to support at least two auth patterns. Whatever you build, make sure you're not rolling your own crypto, you're using httpOnly cookies or secure token storage, and you're thinking about the recovery flow from day one.
Authentication is one of those things that seems simple until it isn't. Take the time to understand these patterns deeply — your users' security depends on it.