Networking & Security

Networking, OAuth 2.0, SSO & Cryptography

Transport protocols, HTTP evolution, authentication flows, JWT, TLS, and cryptographic primitives — with trade-offs and attack vectors.

HTTP Protocol Evolution

HTTP/1.1 vs HTTP/2 vs HTTP/3 — Differences & Trade-offs

Must KnowNetworking
HTTP/1.1HTTP/2HTTP/3
TransportTCPTCPQUIC (UDP-based)
MultiplexingNo (pipelining broken)Yes — multiple streams on one TCP connYes — independent QUIC streams
Head-of-line blockingYes (per connection)Yes (at TCP level)No — per stream independent
Header compressionNoneHPACK (static+dynamic tables)QPACK
Server PushNoYes (largely unused, removed in H3)No
TLSOptionalEffectively required (browsers)Always (built into QUIC)
Connection setup1 RTT TCP + 2 RTT TLS 1.21 RTT TCP + 1 RTT TLS 1.30-1 RTT (QUIC + TLS combined)
Best forSimple, legacyWeb apps, APIsMobile, 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, OPTIONS
Idempotent (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
TCPUDP
Connection3-way handshake (SYN, SYN-ACK, ACK)Connectionless
ReliabilityGuaranteed delivery, ordering, retransmitBest-effort, may drop/reorder
OverheadHigh (20-byte header, ACKs, flow control)Low (8-byte header)
LatencyHigher (setup + ACKs)Lower
Use casesHTTP, HTTPS, FTP, SMTP, SSH, databasesDNS, 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
RecordPurposeExample
AIPv4 addressexample.com → 93.184.216.34
AAAAIPv6 addressexample.com → 2606:2800::1
CNAMEAlias to another hostnamewww → example.com
MXMail serverexample.com → mail.example.com (priority 10)
TXTText (SPF, DKIM, domain verify)"v=spf1 include:..."
NSAuthoritative nameserversexample.com → ns1.cloudflare.com
SOAZone authority infoSerial number, TTL defaults
SRVService 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
MethodDirectionProtocolBest ForBottleneck
Short PollingClient pullsHTTPSimple, infrequent updatesWasteful — many empty responses
Long PollingClient pulls (held)HTTPWhen WebSocket unavailableConnection churn, server threads blocked
SSE (Server-Sent Events)Server → Client onlyHTTP/1.1 text/event-streamLive feeds, logs, notificationsUnidirectional; 6-connection limit in HTTP/1.1
WebSocketBidirectional full-duplexWS/WSS (upgrades HTTP)Chat, gaming, collaborationStateful — 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 CSP frame-ancestors)
  • X-Content-Type-Options: nosniff: Prevent MIME sniffing attacks
  • Strict-Transport-Security: max-age=31536000; includeSubDomains: Force HTTPS
  • Referrer-Policy: strict-origin-when-cross-origin: Control referrer leakage
  • Permissions-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).
FlowWhen to UseTokens
Authorization Code + PKCEWeb apps, SPAs, mobile apps (recommended for all public clients)Authorization code → access + refresh tokens
Client CredentialsMachine-to-machine (no user), backend servicesAccess token only (no refresh)
Device CodeDevices without browser (TV, CLI, IoT)Polling until user authorizes on another device
Implicit (deprecated)Old SPAs — never use, token in URL fragment exposedToken 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)

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.0OIDC (OpenID Connect)
Data formatXML assertionsJSON / JWT
TransportHTTP redirect/POST (browser-based)OAuth 2.0 flows (code, device)
Mobile/SPA supportPoor (XML heavy, redirect-based)Excellent (JSON, API-friendly)
Enterprise adoptionVery high (legacy enterprise)High and growing
ID providersOkta, ADFS, Ping, ShibbolethGoogle, Okta, Auth0, Azure AD, GitHub
TokenSAML 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.

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
SymmetricAsymmetric
KeysOne shared secret keyKey pair: public + private
SpeedFast (AES hardware acceleration)~1000x slower (RSA)
Key distributionHard — how to share secret key?Public key can be shared openly
Use casesBulk data encryption (files, TLS data)Key exchange, signatures, TLS handshake
AlgorithmsAES-256-GCM, ChaCha20-Poly1305RSA-2048/4096, ECDSA (P-256), X25519
Mode concernUse GCM (authenticated) not ECB/CBCNever 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
AlgorithmPurposeSpeedSaltRecommendation
MD5Legacy checksumVery fastNoNever for security
SHA-256Data integrity, HMAC, digital signaturesFastNo (use HMAC)Good for integrity checks
SHA-3 / BLAKE3Integrity, HMACFastNoModern alternative to SHA-2
bcryptPassword hashingSlow (intentional)Built-inGood, widely supported
PBKDF2Password / key derivationConfigurableRequiredFIPS compliant, use SHA-256
Argon2idPassword hashingSlow + memory-hardBuilt-inBest 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.
Signature: RS256(base64(header) + "." + base64(payload), privateKey)