A cookie is client-side storage, so the client can edit it. Signing does not hide the value — it proves your server wrote it. Pass a secret to cookieParser and set signed: true: Express 24,430 appends an HMAC-SHA-256 of the value (Hashes and HMACs), which cookie-parser 2,030 verifies before putting the result on req.signedCookies.
app.get('/set', (req, res) => {
res.cookie('uid', 'u_1042', { signed: true, httpOnly: true, sameSite: 'strict' });
res.send('ok');
});
app.get('/read', (req, res) =>
res.json({ cookies: req.cookies, signed: req.signedCookies }));Set-Cookie: uid=s%3Au_1042.Uo3%2BJOlPdoz2NKVtHxafmOH7GF3ns4lnO63fZV%2Fxjak; Path=/; HttpOnly;
SameSite=Strict
$ curl -s -b jar.txt .../read -> {"cookies":{"theme":"dark"},"signed":{"uid":"u_1042"}}
$ curl -s -H 'Cookie: uid=s%3Au_9999.Uo3%2BJOlPdoz2NKVtHxafmOH7GF3ns4l...' .../read
{"cookies":{"theme":"dark"},"signed":{"uid":false}}Changing u_1042 to u_9999 leaves the signature no longer matching, and the value arrives as false — not as a string, and not as an exception, so check for truthiness before trusting it. An array (cookieParser(['new', 'old'])) rotates keys: cookie-parser signs with the first and accepts any of them.
| Attribute | Effect | Use it for |
|---|---|---|
| httpOnly | JavaScript cannot read the cookie | every auth cookie |
| secure | sent over HTTPS only | every auth cookie in production |
| sameSite: 'lax' | suppressed on cross-site subrequests | the sensible default |
| sameSite: 'strict' | suppressed on all cross-site navigation | admin panels |
| sameSite: 'none' | always sent; requires secure | cross-origin SPA and API |
httpOnly turns a stolen-cookie bug into a non-event: an XSS payload can still make requests as the user, but it cannot read the session ID and mail it somewhere. sameSite: 'lax' blocks the classic CSRF form post, but not same-site subdomain attacks, so keep the defenses of CSRF Protection After csurf too. And secure: true over plain HTTP in development silently drops the cookie, so drive it from the environment.