Web Development
Authentication: JWT, OAuth, Sessions
In 2014 Slack discovered that an attacker had pulled user tokens through an XSS in their chat. Those tokens sat in localStorage. Two years later Slack moved to HttpOnly cookies plus short-lived JWTs - and avoided becoming the next Equifax-scale breach (1.4 billion dollars).
- **Slack (2014)**: XSS in chat plus localStorage produced a wide-scale token leak
- **Equifax (2017)**: 1.4 billion dollars in penalties after credential compromise hit 147 million users
- **OAuth 2.0 Security BCP (RFC 8252)**: Implicit Flow officially deprecated, PKCE required
- **Auth0/Okta**: industry standard is a hybrid - 15-minute access JWT plus a refresh token in a database
- **Chrome 80+ (2020)**: SameSite=Lax as default - a silent global CSRF mitigation
JWT: stateless tokens
A JSON Web Token is a compact self-contained format for carrying claims between parties. Structure: header.payload.signature - three base64url segments joined by dots. The server signs the token with a shared secret (HS256) or a private key (RS256); the client returns it on every request.
**JWT's core pain point:** there is no revocation. A leaked access token stays valid until exp. Mitigations: short TTL (5-15 min), refresh token rotation, a Redis blocklist for early revocation.
What does the JWT signature provide?
OAuth 2.0 and OIDC
OAuth 2.0 is a delegation protocol: an application receives a token to access resources on behalf of a user without learning the user's password. OpenID Connect (OIDC) adds an identity layer on top: the server issues an id_token (a JWT) carrying user claims.
**Authorization Code Flow + PKCE** is the modern standard for web and mobile: 1. Client redirects to the authorize endpoint with a code_challenge 2. User authenticates with the provider 3. Provider returns a code on the redirect_uri 4. Client exchanges code + code_verifier for access_token + id_token at the token endpoint
**Implicit flow is deprecated.** Forbidden in the OAuth 2.1 draft and in the OAuth Security BCP. Authorization Code + PKCE is the right choice even for SPAs.
Why does the authorization code flow need PKCE?
Server sessions vs JWT
Server sessions are the classic approach: the server keeps session state in Redis or a database and the client carries an opaque session_id cookie. JWT is the stateless approach: state lives inside the token, and the server only verifies the signature.
**Trade-off matrix:** | Dimension | Server Session | JWT | |---|---|---| | Revocation | instant | needs a blocklist | | Scaling | needs a shared store | stateless | | Per-request size | 32 bytes | 500-1500 bytes | | Database store | required | not needed | | Permission updates | immediate | wait for TTL |
**Hybrid pattern (Auth0, Okta):** a short-lived access JWT (15 min) plus a long-lived refresh token in a database. Access is stateless for speed; refresh is stateful for revocation. Best of both worlds.
A team is choosing between session-based and JWT for a new application. When does JWT win?
Cookies: HttpOnly, Secure, SameSite
Storing a token in localStorage is a known vulnerability: any XSS reads the token and ships it to the attacker. The standard fix is to put the token in a cookie with three required flags: HttpOnly, Secure, SameSite.
**localStorage = XSS exposure.** Any `<script>` on the page - injected via an ad, a compromised npm dependency, an unsanitized user comment - can read the token. HttpOnly cookies are unreachable from JavaScript.
**CSRF protection on top of SameSite:** double-submit cookie (CSRF token in both cookie and header, server compares) or the synchronizer pattern (one-time token in the form). SameSite=lax is the first line of defense, not the only one.
A JWT in localStorage is safe because it is cryptographically signed.
The signature protects against token forgery, not theft. If an XSS reads the token and ships it to an attacker, the attacker presents a signed, valid token and passes every check.
Yahoo, Twitter, and Slack have all lost user sessions through XSS plus localStorage exactly this way. A digital signature proves the data has not been altered but does not bind the token to a specific user or browser. The correct path is HttpOnly cookies, a short TTL, and a strong CSP.
Why is `localStorage.setItem('token', jwt)` a bad practice?
Key ideas
- JWT is a stateless signed token. Self-contained claims are read without a database. Core pain: cannot be revoked without a blocklist
- OAuth 2.0 = delegation, OIDC = identity layer. The modern standard is Authorization Code Flow + PKCE. Implicit flow is deprecated
- Server sessions are stateful (instant revocation, shared store needed). JWT is stateless (scales, but no revocation). Hybrid: access JWT + refresh in DB
- Cookies beat localStorage. Required flags: HttpOnly (XSS protection), Secure (HTTPS only), SameSite=lax (CSRF protection)
Related topics
Authentication is a cross-cutting layer that touches API design, transport, and secret storage; it intersects every earlier topic in the course:
- GraphQL — auth layer over the API
- REST API Design — Authorization header + Bearer tokens
- Node.js and Express — passport.js, express-session, jsonwebtoken
- HTTP in the browser — cookies, CORS, secure flags at the transport layer
Вопросы для размышления
- When would you reach for JWT, and when for server sessions? Give two criteria for each
- Why does an SPA need PKCE if it cannot safely hold a client_secret anyway?
- Describe an attack that succeeds against JWT-in-localStorage and fails against an HttpOnly cookie
- What is the recovery path if an access token leaks but the refresh token still lives in the database?
Связанные уроки
- web-13 — GraphQL requires special auth configuration
- web-15 — After auth - advanced security patterns
- sec-01 — Security basics before JWT and OAuth
- crypto-19-hash-functions — Hash functions behind JWT HMAC signatures
- net-15-tcp-basics — HTTPS/TLS is the mandatory foundation for OAuth flow