Authentication foundations
Set a secure session cookie
Use HTTPS and restrictive cookie attributes so scripts, insecure transport, and unnecessary cross-site requests cannot expose the session.
Whoever holds the session cookie is the user, as far as the server can tell. For the lifetime of the session, that cookie is worth as much as the password. So we protect it like a credential.
The protection is a handful of cookie attributes plus HTTPS everywhere. This is the header the Books API sends after login:
Set-Cookie: __Host-session=opaque-random-value; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=3600
Let’s go through it piece by piece.
What each attribute does
HttpOnlystops ordinary JavaScript on the page from reading the value withdocument.cookie.Securemakes the browser send it only over HTTPS.SameSite=Laxblocks the cookie on most cross-site requests, which cuts a lot of CSRF exposure.Path=/makes it available to the whole origin.Max-Age=3600gives it a controlled lifetime, one hour here.- No
Domainattribute. Leaving it out keeps subdomains from setting or receiving the cookie.
The __Host- prefix on the name adds rules the browser enforces itself. A cookie with that name must be Secure, must have Path=/, and must not have a Domain. That last part matters: because __Host- requires Path=/, you can’t have both the host-bound prefix and a narrow path. Pick one deliberately.
What these flags don’t do
These attributes reduce exposure. They don’t replace server-side validation, CSRF protection, or XSS prevention.
Notice the gap with HttpOnly. Injected script can’t read the cookie, but it can still call fetch('/books/17', { method: 'DELETE' }) from the page, and the browser will attach the cookie for it. The attacker doesn’t need to see the value to use it.
Logout has two halves
On logout, do two things: expire the browser cookie and revoke the server record.
If you only clear the cookie, a copy of the token someone already stole stays valid until it expires. If you only revoke the record, you’re safe, but the browser keeps sending a dead credential on every request until Max-Age runs out. Do both.
Check it in the browser
Secure cookies need HTTPS. Modern browsers treat localhost as a secure context, so plain http://localhost usually works. If your setup doesn’t, use a documented local exception, never a production config that drops Secure.
Log in, then open DevTools, Application tab, Cookies. You should see the cookie with HttpOnly and Secure ticked, SameSite set to Lax, and no domain other than your host. Now delete the session row on the server and reload a protected page. The browser still sends the cookie, and the server must answer 401. If it doesn’t, your middleware is trusting the cookie’s presence instead of the record behind it.
Lesson completed