Networking, OAuth 2.0, SSO & Cryptography
Transport protocols, HTTP evolution, authentication flows, JWT, TLS, and cryptographic primitives — with trade-offs and attack vectors.
HTTP/1.1 vs 2 vs 3
TCP vs UDP
DNS
WebSockets & SSE
CORS
OAuth 2.0
SSO / SAML / OIDC
JWT
Cryptography
TLS Handshake
Hashing & Passwords
HTTP Protocol Evolution
HTTP/1.1 vs HTTP/2 vs HTTP/3 — Differences & Trade-offs
Must KnowNetworking
| HTTP/1.1 | HTTP/2 | HTTP/3 | |
|---|---|---|---|
| Transport | TCP | TCP | QUIC (UDP-based) |
| Multiplexing | No (pipelining broken) | Yes — multiple streams on one TCP conn | Yes — independent QUIC streams |
| Head-of-line blocking | Yes (per connection) | Yes (at TCP level) | No — per stream independent |
| Header compression | None | HPACK (static+dynamic tables) | QPACK |
| Server Push | No | Yes (largely unused, removed in H3) | No |
| TLS | Optional | Effectively required (browsers) | Always (built into QUIC) |
| Connection setup | 1 RTT TCP + 2 RTT TLS 1.2 | 1 RTT TCP + 1 RTT TLS 1.3 | 0-1 RTT (QUIC + TLS combined) |
| Best for | Simple, legacy | Web apps, APIs | Mobile, high-latency, lossy networks |
HTTP/1.1: Request → Response → Request → Response (sequential, or 6 parallel connections)
HTTP/2: [Stream 1: GET /html] [Stream 2: GET /css] [Stream 3: GET /js]
All multiplexed over ONE TCP connection
⚠ TCP packet loss blocks ALL streams (HOL blocking at TCP layer)
HTTP/3 (QUIC):
[Stream 1: GET /html] ── independent QUIC stream
[Stream 2: GET /css] ── independent QUIC stream
[Stream 3: GET /js] ── independent QUIC stream
Lost UDP packet only affects its stream, not others
HTTP Methods — Idempotency & Safety
Safe (no side effects): GET, HEAD, OPTIONSIdempotent (same result if repeated): GET, PUT, DELETE, HEAD, OPTIONS
Neither: POST (creates new resource), PATCH (may not be idempotent)
TCP vs UDP — When to Use Each
Transport
| TCP | UDP | |
|---|---|---|
| Connection | 3-way handshake (SYN, SYN-ACK, ACK) | Connectionless |
| Reliability | Guaranteed delivery, ordering, retransmit | Best-effort, may drop/reorder |
| Overhead | High (20-byte header, ACKs, flow control) | Low (8-byte header) |
| Latency | Higher (setup + ACKs) | Lower |
| Use cases | HTTP, HTTPS, FTP, SMTP, SSH, databases | DNS, DHCP, video streaming, gaming, VoIP, QUIC |
TCP 3-Way Handshake:
Client ──── SYN (seq=x) ──────────────────► Server
Client ◄─── SYN-ACK (seq=y, ack=x+1) ────── Server
Client ──── ACK (ack=y+1) ───────────────── Server
[Connection established — data transfer]
TCP 4-Way Close:
Client ──── FIN ──────────────────────────► Server
Client ◄─── ACK ─────────────────────────── Server
Client ◄─── FIN ─────────────────────────── Server
Client ──── ACK ──────────────────────────► Server
[TIME_WAIT on client for 2×MSL to handle delayed packets]
TCP Flow Control & Congestion Control
- Flow control (receiver-driven): Receiver advertises window size; sender can't exceed it. Prevents overwhelming a slow receiver.
- Congestion control (network-driven): Slow start → congestion avoidance → fast retransmit/recovery. Responds to packet loss as congestion signal.
- Nagle's algorithm: Buffers small packets (disable with TCP_NODELAY for latency-sensitive apps like games)
DNS — Domain Name System
DNS Resolution Process & Record Types
Networking
DNS Resolution: browser.com → 93.184.216.34
1. Browser cache (TTL-based)
2. OS cache / /etc/hosts
3. Recursive Resolver (ISP or 8.8.8.8)
├── Root DNS server → "try .com TLD servers"
├── .com TLD server → "try browser.com nameservers"
└── Authoritative NS → returns A record: 93.184.216.34
4. Resolver caches response (per TTL)
5. Browser connects to 93.184.216.34
| Record | Purpose | Example |
|---|---|---|
| A | IPv4 address | example.com → 93.184.216.34 |
| AAAA | IPv6 address | example.com → 2606:2800::1 |
| CNAME | Alias to another hostname | www → example.com |
| MX | Mail server | example.com → mail.example.com (priority 10) |
| TXT | Text (SPF, DKIM, domain verify) | "v=spf1 include:..." |
| NS | Authoritative nameservers | example.com → ns1.cloudflare.com |
| SOA | Zone authority info | Serial number, TTL defaults |
| SRV | Service location | _sip._tcp.example.com → sip.example.com:5060 |
Low TTL trade-off: Faster failover during outages but higher DNS query load. High TTL = less load but slow propagation of changes. Route 53 health-check failover works best with TTL ≤ 60s.
Real-Time Communication Patterns
WebSockets vs SSE vs Long Polling vs Short Polling
Real-TimeNetworking
| Method | Direction | Protocol | Best For | Bottleneck |
|---|---|---|---|---|
| Short Polling | Client pulls | HTTP | Simple, infrequent updates | Wasteful — many empty responses |
| Long Polling | Client pulls (held) | HTTP | When WebSocket unavailable | Connection churn, server threads blocked |
| SSE (Server-Sent Events) | Server → Client only | HTTP/1.1 text/event-stream | Live feeds, logs, notifications | Unidirectional; 6-connection limit in HTTP/1.1 |
| WebSocket | Bidirectional full-duplex | WS/WSS (upgrades HTTP) | Chat, gaming, collaboration | Stateful — harder to load-balance; sticky sessions needed |
# WebSocket server — Python (websockets library)
import asyncio, websockets, json
connected = set()
async def handler(websocket):
connected.add(websocket)
try:
async for message in websocket:
data = json.loads(message)
# Broadcast to all connected clients
await asyncio.gather(*[
ws.send(json.dumps({"user": data["user"], "msg": data["msg"]}))
for ws in connected if ws != websocket
])
finally:
connected.remove(websocket)
async def main():
async with websockets.serve(handler, "localhost", 8765):
await asyncio.Future() # run forever
WebSocket at Scale
Use Redis Pub/Sub as the broadcast layer across multiple WebSocket server instances. Each server subscribes to Redis; when a message arrives, broadcast to local connected clients. This enables horizontal scaling without sticky sessions for broadcasting.CORS & Security Headers
CORS — Cross-Origin Resource Sharing
SecurityHTTP
Same-origin: protocol + host + port must all match
https://app.example.com:443/api ✅ same origin
http://app.example.com/api ✗ different protocol
https://api.example.com/api ✗ different subdomain
https://app.example.com:8080/api ✗ different port
Simple request (no preflight):
GET/POST with basic headers → browser adds Origin header
Server responds with Access-Control-Allow-Origin
Preflight (OPTIONS) for non-simple requests:
Client ── OPTIONS /api ──────────────────────────────► Server
← Access-Control-Allow-Origin: https://app.com
← Access-Control-Allow-Methods: GET, POST, PUT
← Access-Control-Allow-Headers: Content-Type, Authorization
← Access-Control-Max-Age: 86400 (cache preflight 24h)
Client ── Actual Request ──────────────────────────── Server
# FastAPI CORS configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # never use "*" with credentials
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=86400,
)
Security Headers
Content-Security-Policy: Whitelist sources for scripts/styles/images. Prevents XSS.X-Frame-Options: DENY: Prevent clickjacking (or use CSPframe-ancestors)X-Content-Type-Options: nosniff: Prevent MIME sniffing attacksStrict-Transport-Security: max-age=31536000; includeSubDomains: Force HTTPSReferrer-Policy: strict-origin-when-cross-origin: Control referrer leakagePermissions-Policy: Restrict camera, microphone, geolocation access
OAuth 2.0 — Authorization Framework
OAuth 2.0 Flows — Authorization Code, PKCE, Client Credentials
Must KnowAuth
OAuth 2.0 is Authorization, not Authentication
OAuth gives an application limited access to a user's resources. Use OpenID Connect (OIDC) on top of OAuth for authentication (identity).| Flow | When to Use | Tokens |
|---|---|---|
| Authorization Code + PKCE | Web apps, SPAs, mobile apps (recommended for all public clients) | Authorization code → access + refresh tokens |
| Client Credentials | Machine-to-machine (no user), backend services | Access token only (no refresh) |
| Device Code | Devices without browser (TV, CLI, IoT) | Polling until user authorizes on another device |
| Implicit (deprecated) | Old SPAs — never use, token in URL fragment exposed | Token directly (insecure) |
Authorization Code + PKCE Flow (recommended for SPAs/mobile):
1. Client generates code_verifier (random 64 bytes) and
code_challenge = BASE64URL(SHA256(code_verifier))
2. Client ── GET /authorize?
response_type=code
&client_id=xyz
&redirect_uri=https://app.com/callback
&scope=openid profile email
&state=random_csrf_token ← CSRF protection
&code_challenge=ABCD...
&code_challenge_method=S256 ──────────────────► Auth Server
3. User authenticates + consents on Auth Server
4. Auth Server ── Redirect to https://app.com/callback?
code=AUTHORIZATION_CODE
&state=random_csrf_token ─────────────────► Client
5. Client verifies state == original (CSRF check)
6. Client ── POST /token
code=AUTHORIZATION_CODE
code_verifier=ORIGINAL_VERIFIER ← PKCE proof
grant_type=authorization_code
redirect_uri=... ─────────────────────────────► Auth Server
7. Auth Server verifies SHA256(code_verifier) == code_challenge
Auth Server ── { access_token, refresh_token, id_token } ► Client
Security pitfalls:
❌ Never put access tokens in URL parameters (logged by servers/proxies)
❌ Don't skip state parameter validation (CSRF attack vector)
❌ Implicit flow — token in URL fragment cached in browser history
❌ Client secrets in mobile/SPA code — use PKCE instead
✅ Always validate redirect_uri server-side (open redirect attacks)
❌ Never put access tokens in URL parameters (logged by servers/proxies)
❌ Don't skip state parameter validation (CSRF attack vector)
❌ Implicit flow — token in URL fragment cached in browser history
❌ Client secrets in mobile/SPA code — use PKCE instead
✅ Always validate redirect_uri server-side (open redirect attacks)
Client Credentials & Token Refresh
AuthM2M
import httpx, time
class OAuthClient:
def __init__(self, token_url, client_id, client_secret, scope):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.scope = scope
self._token = None
self._expires_at = 0
def get_access_token(self) -> str:
if self._token and time.time() < self._expires_at - 60: # 60s buffer
return self._token
return self._refresh_token()
def _refresh_token(self) -> str:
resp = httpx.post(self.token_url, data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": self.scope,
})
resp.raise_for_status()
data = resp.json()
self._token = data["access_token"]
self._expires_at = time.time() + data["expires_in"]
return self._token
def request(self, method, url, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self.get_access_token()}"
return httpx.request(method, url, headers=headers, **kwargs)
SSO — Single Sign-On
SSO Architecture — SAML vs OIDC
Must KnowSSO
| SAML 2.0 | OIDC (OpenID Connect) | |
|---|---|---|
| Data format | XML assertions | JSON / JWT |
| Transport | HTTP redirect/POST (browser-based) | OAuth 2.0 flows (code, device) |
| Mobile/SPA support | Poor (XML heavy, redirect-based) | Excellent (JSON, API-friendly) |
| Enterprise adoption | Very high (legacy enterprise) | High and growing |
| ID providers | Okta, ADFS, Ping, Shibboleth | Google, Okta, Auth0, Azure AD, GitHub |
| Token | SAML Assertion (XML signed) | ID Token (JWT) + Access Token |
OIDC SSO Flow (web app):
1. User visits app.example.com → not authenticated → redirect to IdP
2. IdP (Okta/Auth0) authenticates user (credentials, MFA)
3. IdP issues:
- id_token (JWT) — who the user is (name, email, sub)
- access_token — what the user can access
- refresh_token — get new access tokens without re-login
4. App validates id_token signature (JWKs endpoint)
5. App creates session ← SESSION TOKEN stored in httpOnly cookie
Federation: Company uses Okta as IdP
Employee logs into Okta once → SSO into Salesforce, Jira, Github, Slack
Each SP trusts Okta's assertions. No per-app password.
Session Management in SSO
- SP Session: Each service provider maintains its own session (httpOnly cookie). Duration typically shorter than IdP session.
- IdP Session: Central session at identity provider. User only re-authenticates here.
- Single Logout (SLO): Terminating IdP session + propagating logout to all SPs. Often brittle — many apps use session TTL instead.
- Token expiry strategy: Short-lived access tokens (15min) + long-lived refresh tokens (7-30 days, rotated on use). Refresh token rotation prevents token theft.
JWT — JSON Web Tokens
JWT Structure, Validation & Security
Must KnowJWT
JWT = Base64URL(Header) . Base64URL(Payload) . Base64URL(Signature)
Header: { "alg": "RS256", "typ": "JWT", "kid": "key-id-2024" }
Payload: { "sub": "user123", // subject (user id)
"iss": "https://auth.example.com", // issuer
"aud": ["api.example.com"], // audience
"exp": 1716825600, // expiry (Unix timestamp)
"iat": 1716822000, // issued at
"nbf": 1716822000, // not before
"jti": "unique-token-id", // JWT ID (for revocation)
"email": "alice@example.com",
"roles": ["user", "editor"] }
Signature: RS256(base64(header) + "." + base64(payload), privateKey)
import jwt, time
from cryptography.hazmat.primitives import serialization
# ── Verify a JWT (RS256) ───────────────────────────────────
def verify_jwt(token: str, jwks_uri: str) -> dict:
"""Fetch public key from JWKS endpoint and validate token."""
import httpx
header = jwt.get_unverified_header(token)
# Fetch JWKS and find matching key by kid
jwks = httpx.get(jwks_uri).json()
public_key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
return jwt.decode(
token,
jwt.algorithms.RSAAlgorithm.from_jwk(public_key),
algorithms=["RS256"],
audience="api.example.com",
issuer="https://auth.example.com",
options={"require": ["exp", "iat", "sub", "iss", "aud"]},
)
# ── Security checks that JWT library handles ──────────────
# ✅ Signature verification
# ✅ Expiry (exp)
# ✅ Not-before (nbf)
# ✅ Issuer (iss)
# ✅ Audience (aud)
# ⚠ Revocation — JWT is stateless; use jti + revocation list in Redis
JWT Security Vulnerabilities:
alg:none attack — Never trust the algorithm from the header. Always hardcode expected algorithm server-side.
RS256 → HS256 confusion — An attacker changes alg to HS256 and signs with the server's public key (which is known). Server mistakenly uses HMAC with the public key string.
No revocation — JWTs are valid until expiry. For logout, maintain a revocation list (Redis) and check jti on each request.
alg:none attack — Never trust the algorithm from the header. Always hardcode expected algorithm server-side.
RS256 → HS256 confusion — An attacker changes alg to HS256 and signs with the server's public key (which is known). Server mistakenly uses HMAC with the public key string.
No revocation — JWTs are valid until expiry. For logout, maintain a revocation list (Redis) and check jti on each request.
HS256 vs RS256
- HS256 (HMAC-SHA256): Symmetric — same secret for sign and verify. Use only when signer == verifier (same service). Secret must be kept hidden from all parties.
- RS256 (RSA-SHA256): Asymmetric — private key signs, public key verifies. Auth server keeps private key; any resource server can verify with public key from JWKS endpoint. Preferred for distributed systems.
- ES256 (ECDSA-SHA256): Elliptic curve variant of RS256 — smaller signatures, faster verification, same security level with smaller key sizes.
Cryptography — Symmetric, Asymmetric & Digital Signatures
Symmetric vs Asymmetric Encryption
Must KnowCryptography
| Symmetric | Asymmetric | |
|---|---|---|
| Keys | One shared secret key | Key pair: public + private |
| Speed | Fast (AES hardware acceleration) | ~1000x slower (RSA) |
| Key distribution | Hard — how to share secret key? | Public key can be shared openly |
| Use cases | Bulk data encryption (files, TLS data) | Key exchange, signatures, TLS handshake |
| Algorithms | AES-256-GCM, ChaCha20-Poly1305 | RSA-2048/4096, ECDSA (P-256), X25519 |
| Mode concern | Use GCM (authenticated) not ECB/CBC | Never encrypt large data directly |
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# ── AES-256-GCM (symmetric, authenticated encryption) ─────
key = AESGCM.generate_key(bit_length=256) # 32 bytes
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce — NEVER reuse with same key
aad = b"authenticated but not encrypted metadata"
ciphertext = aesgcm.encrypt(nonce, b"secret message", aad)
plaintext = aesgcm.decrypt(nonce, ciphertext, aad)
# If AAD or ciphertext is tampered → InvalidTag exception (authentication)
# ── RSA encryption/decryption ─────────────────────────────
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
encrypted = public_key.encrypt(
b"secret",
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
decrypted = private_key.decrypt(encrypted,
padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)
)
# RSA practical limit: encrypt only small data (< key_size - overhead)
# In TLS: RSA/ECDH exchange symmetric session key, then use AES for bulk
Digital Signatures & Certificate Chain
CryptographyPKI
Digital Signature (Non-repudiation + Integrity):
Sign: hash = SHA-256(message)
signature = RSA_privateKey_encrypt(hash)
Verify: hash1 = SHA-256(message)
hash2 = RSA_publicKey_decrypt(signature)
valid = (hash1 == hash2)
Certificate Chain (PKI):
Root CA (self-signed, trusted by OS)
└── Intermediate CA (signed by Root CA)
└── Server Certificate example.com (signed by Intermediate CA)
Browser verification:
1. Get server cert from TLS handshake
2. Verify signature with Intermediate CA public key
3. Verify Intermediate CA signature with Root CA
4. Root CA is in trusted store → chain valid
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes, serialization
# Sign a message (private key)
signature = private_key.sign(
b"document content",
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256()
)
# Verify (public key) — raises InvalidSignature if tampered
try:
public_key.verify(
signature,
b"document content",
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256()
)
print("Signature valid")
except Exception:
print("Tampered!")
TLS 1.3 Handshake
TLS 1.3 — Handshake & What Changed from 1.2
Must KnowTLS
TLS 1.3 Full Handshake (1 RTT):
Client ── ClientHello ──────────────────────────────────► Server
Supported cipher suites (only AEAD: AES-GCM, ChaCha20)
Key share: Client ECDH ephemeral public key (X25519)
Supported TLS versions
Server ── ServerHello ◄──────────────────────────────────
Selected cipher suite
Server ECDH ephemeral public key
[Both sides derive shared secret via ECDH — no more RSA key exchange]
Server ── EncryptedExtensions, Certificate, CertificateVerify, Finished ◄──
(Everything after ServerHello is encrypted)
Client ── Finished ──────────────────────────────────────► Server
[Application data starts flowing — 1 RTT total]
TLS 1.3 0-RTT Session Resumption:
Client caches session ticket → next connection sends data immediately
⚠ 0-RTT data has no replay protection — only use for idempotent GETs
Removed in TLS 1.3 (security improvements):
❌ RSA key exchange (no forward secrecy)
❌ DH static key exchange
❌ RC4, DES, 3DES, MD5, SHA-1
❌ Compression (CRIME attack)
❌ Renegotiation
Forward Secrecy (Perfect Forward Secrecy)
TLS 1.3 mandates ECDHE (Ephemeral Diffie-Hellman). A new key pair is generated per session. Even if the server's private key is compromised later, past sessions cannot be decrypted because the ephemeral keys were discarded.
TLS 1.2 with RSA key exchange had NO forward secrecy — recording all traffic and later getting the private key allowed decryption of all past sessions.
Hashing — Integrity vs Password Storage
SHA-256 vs bcrypt vs PBKDF2 vs Argon2
Must KnowSecurity
| Algorithm | Purpose | Speed | Salt | Recommendation |
|---|---|---|---|---|
| MD5 | Legacy checksum | Very fast | No | Never for security |
| SHA-256 | Data integrity, HMAC, digital signatures | Fast | No (use HMAC) | Good for integrity checks |
| SHA-3 / BLAKE3 | Integrity, HMAC | Fast | No | Modern alternative to SHA-2 |
| bcrypt | Password hashing | Slow (intentional) | Built-in | Good, widely supported |
| PBKDF2 | Password / key derivation | Configurable | Required | FIPS compliant, use SHA-256 |
| Argon2id | Password hashing | Slow + memory-hard | Built-in | Best current recommendation |
import hashlib, hmac, os
import bcrypt
# pip install argon2-cffi
from argon2 import PasswordHasher
# ── SHA-256 for data integrity ─────────────────────────────
digest = hashlib.sha256(b"file contents").hexdigest()
# HMAC — keyed hash (prevents length extension attacks)
mac = hmac.new(b"secret_key", b"message", hashlib.sha256).hexdigest()
# Use hmac.compare_digest() for constant-time comparison (prevent timing attacks)
valid = hmac.compare_digest(mac, received_mac)
# ── bcrypt for passwords ───────────────────────────────────
password = b"user_password_123"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12)) # cost=12
verified = bcrypt.checkpw(password, hashed) # True
# ── Argon2id (recommended) ─────────────────────────────────
ph = PasswordHasher(
time_cost=3, # iterations
memory_cost=65536, # 64 MB
parallelism=4, # threads
)
hashed = ph.hash("user_password")
try:
ph.verify(hashed, "user_password") # True
# Check if rehash needed (parameters upgraded)
if ph.check_needs_rehash(hashed):
hashed = ph.hash("user_password")
except Exception:
pass # invalid password
Why fast hashes (SHA-256) are bad for passwords: An attacker with a GPU can compute ~10 billion SHA-256 hashes/second. Argon2id at default settings: ~10 hashes/second. Brute-force becomes 10⁹× harder. Always use a password hashing function with configurable work factor.
Rainbow Table Defense
Never hash passwords without a per-user salt. Even if two users have the same password, their hashes will differ. bcrypt and Argon2 include a cryptographically random salt automatically.