Getting authentication wrong is the fastest way to compromise your entire application. In 2026, the auth landscape has matured significantly โ passkeys (WebAuthn) are gaining traction, OAuth 2.1 is clarifying long-standing ambiguities, and JWT best practices have crystallized. This guide covers the patterns that protect production applications, with code examples in Node.js and Python.
Authentication Methods Compared
| Method | Security Level | UX | Complexity | Best For |
|---|---|---|---|---|
| Session Tokens (cookie-based) | High (with proper config) | Excellent | Low | Traditional web apps, server-rendered pages |
| JWT (stateless) | Medium-High | Good | Medium | APIs, microservices, mobile apps |
| OAuth 2.1 + OIDC | High | Good (redirect flow) | Medium-High | Third-party login, enterprise SSO |
| Passkeys (WebAuthn) | Highest (phishing-resistant) | Excellent (biometric) | Medium | Consumer apps, replacing passwords |
| Magic Links | Medium | Good (email-based) | Low | Low-security apps, quick onboarding |
| API Keys | Medium (if stored properly) | N/A (machine-to-machine) | Low | Server-to-server APIs, CI/CD, SDKs |
Session Tokens: The Gold Standard for Web Apps
Best for: Server-rendered web applications where the same origin serves both frontend and API. Key rules:
- Use httpOnly, Secure, SameSite=Lax cookies
- Store session data in Redis (not in-memory, not in JWT) for fast lookup and revocation
- Rotate the session ID on login (prevent session fixation)
- Implement CSRF protection for cookie-based sessions (double-submit cookie pattern or Synchronizer Token)
- Set reasonable session duration: 15 minutes idle timeout, 8 hours absolute max
JWT: When and How to Use Safely
Best for: APIs consumed by multiple client types (web, mobile, third-party). Critical rules: Never store sensitive data in JWT payload (it is base64-encoded, not encrypted). Always set short expiration (15-60 min) and use refresh tokens for renewal. Maintain a server-side token denylist for revoked tokens.
// Node.js: Signing a JWT securely
const jwt = require('jsonwebtoken');
const token = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET, // >= 256-bit random string, stored in env
{ expiresIn: '15m', algorithm: 'HS256' } // Never use 'none' algorithm
);
// Refresh token rotation: issue a new refresh token each time
// and invalidate the old one (maintain a family of refresh tokens)
Passkeys (WebAuthn): The Future of Authentication
Best for: Consumer applications that want to eliminate passwords. Passkeys use public-key cryptography โ the private key stays on the user's device, and the server only stores the public key. This makes phishing and credential stuffing impossible. Implementation: Use the WebAuthn API on the client (navigator.credentials.create/get) and a library like @simplewebauthn/server on the backend.
OAuth 2.1: What Changed from 2.0
- PKCE is now required for all authorization code grants (no more implicit flow)
- Refresh token rotation is mandatory (one-time-use refresh tokens)
- The Resource Owner Password Credentials grant is removed (never send username/password to an authorization server)
- Bearer tokens must not be passed in URL query strings
Password Storage: Non-Negotiable Rules
| Rule | Correct | Wrong |
|---|---|---|
| Hash algorithm | bcrypt (cost 12+), argon2id | SHA-256, MD5, bcrypt with cost < 10 |
| Pepper | 32-byte random pepper stored in HSM or env var, separate from DB | No pepper, or pepper stored in same DB column |
| Password requirements | Minimum 8 chars, check against haveibeenpwned API | Requiring special chars that users forget; max length limits |
Bottom line: Use session tokens for web apps and JWTs for APIs โ do not use JWTs for web app sessions. Implement passkeys as your primary auth method if possible (highest security + best UX). Never roll your own crypto โ use well-tested libraries (bcrypt, @simplewebauthn, jose, node-crypto). See also: Clerk vs Auth0 vs Lucia and Web Security Basics.