Volby v autentizaci jsou tři nezávislé osy, ne jedna škála
„Používáme session, nebo tokeny" je otázka, ve které jsou slité tři různé věci. Kde leží stav. Čím se ta hodnota přenáší. A kdo k ní má na klientovi přístup.
Ty osy jdou kombinovat volně. JWT v cookie s HttpOnly je běžná a rozumná konfigurace. Opaque session identifikátor v localStorage je taky možná — a je to ta nejhorší varianta ze všech. Zamotanost téhle debaty pochází z toho, že se odpovědi na tři otázky vyslovují jedním slovem.
Osa první: kde leží stav
Opaque identifikátor nenese nic. Je to náhodný řetězec a server si podle něj v úložišti najde, kdo jsi. Autorita zůstává na serveru: odhlášení je smazání záznamu, změna práv se projeví okamžitě, cena je jedno vyhledání na požadavek.
Token nese tvrzení v sobě a podpis dokládá, že ho vydal server. Server si nemusí nic pamatovat, což je jediný skutečný výnos — škáluje se bez sdíleného stavu. Cena je, že odvolání neumí. Podepsané tvrzení platí do expirace, protože jediný, kdo by ho mohl zrušit, je právě ten stav, který jsi se rozhodl nemít.
MDN to říká jako doporučení k opatrnosti: zvaž „opaque identifikátor, který si server vyhledá, místo ukládání citlivých informací přímo do cookies". Prakticky z toho plyne pravidlo — potřebuješ-li okamžité odvolání, potřebuješ stav, a krátká expirace je jeho náhražka, ne ekvivalent.
Osa druhá: čím se to přenáší
Cookie má proti hlavičce Authorization jednu vlastnost, která je zároveň její největší výhoda i její jediná zranitelnost: prohlížeč ji přiloží sám.
To je výhoda, protože JavaScript se o autentizaci nemusí starat a hodnotu nemusí nikdy vidět. A je to zranitelnost, protože ji prohlížeč přiloží i k požadavku, který vznikl z cizí stránky — a tomu se říká CSRF. Hlavička Authorization tímhle netrpí, protože ji nikdo sám nepřidá, a právě proto potřebuje kód, který ji přidává, a ten k té hodnotě musí mít přístup.
Odpovědí na CSRF u cookies je SameSite. Strict se pošle jen při same-site požadavku, Lax se pošle i při navigaci na cookie origin, None vždy a musí být doplněná o Secure. Podstatné je vědět, jaký je default: podle MDN „pokud není SameSite nastavená, cookie se chová jako Lax". Lax tedy dostaneš i bez rozhodnutí, ale u čehokoli citlivého se rozhodnout vyplatí.
Osa třetí: kdo k tomu smí sáhnout
HttpOnly znamená, že se na cookie nedá dosáhnout přes document.cookie. MDN je v tom přímočará: „cookie, které uchovávají uživatelskou session, by měly mít nastavený atribut HttpOnly — bylo by opravdu nebezpečné zpřístupnit je JavaScriptu."
Tady je ale potřeba být přesný v tom, co to kupuje. HttpOnly neodstraní XSS. Odstraní jen jednu jeho konzumaci, totiž odvezení hodnoty pryč. Skript v tvé stránce pořád může poslat požadavek v tvém jméně — a prohlížeč mu tu cookie sám přiloží, protože o tom skriptu neví nic zlého. HttpOnly mění krádež identity na její zneužití na místě, což je výrazně menší dopad a ne nula.
Právě tady je důvod, proč je token v localStorage špatná volba a nesouvisí to s tím, že je to token. localStorage je JavaScriptu dostupné z definice, takže jakékoli XSS znamená exfiltraci. Volbu úložiště děláš tímhle, ne formátem hodnoty.
Zbytek atributů určuje dosah, ne přístup. Domain rozšíří cookie na subdomény, Path ji zúží na cestu — a MDN výslovně upozorňuje, že Path „není myšlená jako bezpečnostní opatření". Secure zajistí přenos jen po HTTPS. Session cookie je ta bez Max-Age a Expires; zaniká s koncem session, jenže tu si definuje prohlížeč a obnova sezení po restartu jí dovolí vydržet libovolně dlouho.
Prefixy jmen jsou levná pojistka, protože je vymáhá prohlížeč, ne tvůj kód. __Host- vyžaduje Secure, zakazuje Domain a připouští jen Path=/. Tím zaručí, že cookie dorazí jen tomu hostu, který ji nastavil, a že ji nikdo nepřepíše z jiné cesty ani ze sousední subdomény. Server ale musí kontrolovat celé jméno včetně prefixu, protože prohlížeč ho neodstraňuje.
A jedna věc, která nepatří k žádné ose a zapomíná se: po přihlášení session identifikátor vždycky vygeneruj znovu, i když už existuje. Bez toho je otevřená session fixation, kdy útočník podstrčí hodnotu předem a přihlášením se stane platnou.
Co si z toho odnést
Rozhoduj se po jedné ose. Stav na serveru, pokud potřebuješ odvolat přístup okamžitě; podepsaný token, pokud potřebuješ ověřovat bez sdíleného úložiště a umíš žít s expirací. Přenos přes cookie pro prohlížeč, přes Authorization pro klienty, které neovládá prohlížeč.
Pro cookie ve webové appce je rozumný default HttpOnly; Secure; SameSite=Lax a jméno s prefixem __Host-. Strict tam, kde nepotřebuješ přežít příchod z cizího odkazu. A pamatuj, co HttpOnly nedělá: nezabrání skriptu jednat ve tvém jméně, jen mu zabrání odnést si hodnotu.
Jestli si z toho máš odnést jednu větu: úložiště rozhoduje o tom, která třída zranitelností tě potká, formát hodnoty o tom, jak drahé je odhlášení.