@nextrush/cookies
Secure cookie parsing and serialization with signing support.
Handle HTTP cookies with secure defaults and signing support.
Cookies are essential for sessions, authentication, and client-side state. This middleware provides parsing, setting, and optional HMAC-SHA256 signing with RFC 6265 compliance.
Installation
$ pnpm add @nextrush/cookies
Quick Start
import { createApp } from '@nextrush/core';
import { cookies } from '@nextrush/cookies';
const app = createApp();
app.use(cookies());
app.use(async (ctx) => {
// Read a cookie
const session = ctx.state.cookies.get('session');
// Set a cookie (secure defaults: httpOnly, sameSite=lax, path=/)
ctx.state.cookies.set('session', 'user-123', {
secure: true,
maxAge: 86400,
});
ctx.json({ session });
});Default Behavior
With default options, cookies() applies these defaults to every cookie you set:
httpOnly: true— blocks JavaScript access viadocument.cookiesameSite: 'lax'— prevents CSRF on cross-origin POST requestspath: '/'— scopes the cookie to the entire domain
Values are URL-decoded and sanitized (CRLF characters stripped) during parsing.
Reading Cookies
app.use(async (ctx) => {
// Get a single cookie
const token = ctx.state.cookies.get('token');
// Get all parsed cookies
const all = ctx.state.cookies.all();
// Check if a cookie exists
const hasSession = ctx.state.cookies.has('session');
ctx.json({ token, all, hasSession });
});Setting Cookies
app.use(async (ctx) => {
// Uses secure defaults (httpOnly, sameSite=lax, path=/)
ctx.state.cookies.set('name', 'value');
// Override specific options
ctx.state.cookies.set('session', 'user-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 86400,
path: '/',
domain: '.example.com',
});
ctx.json({ ok: true });
});Configuration Options
CookieOptions
| Property | Type | Description |
|---|---|---|
httpOnly | boolean= true | Block JavaScript access via document.cookie |
secure | boolean= false | Send only over HTTPS. Required for SameSite=None and cookie prefixes. |
sameSite | 'strict' | 'lax' | 'none' | boolean= 'lax' | CSRF protection level |
maxAge? | number | Lifetime in seconds. Use 0 to expire immediately. |
expires? | Date | number | Expiration date. If both maxAge and expires are set, maxAge takes precedence. |
path | string= '/' | URL path scope for the cookie |
domain? | string | Domain scope. Leading dots are ignored. |
priority? | 'low' | 'medium' | 'high' | Browser priority hint (Chrome extension) |
partitioned? | boolean | Partition by top-level site for third-party cookies (CHIPS) |
Deleting Cookies
app.use(async (ctx) => {
ctx.state.cookies.delete('session');
// Path and domain must match the original cookie
ctx.state.cookies.delete('session', {
path: '/',
domain: '.example.com',
});
ctx.json({ ok: true });
});Secure Presets
Both presets are functions that accept additional options and return merged CookieOptions.
secureOptions()
Returns options for production-grade cookies.
import { cookies, secureOptions } from '@nextrush/cookies';
app.use(cookies());
app.use(async (ctx) => {
ctx.state.cookies.set('auth', 'token', secureOptions({ maxAge: 86400 }));
});Produces: httpOnly: true, secure: true, sameSite: 'strict', path: '/'.
sessionOptions()
Returns options for session cookies that expire when the browser closes.
import { cookies, sessionOptions } from '@nextrush/cookies';
app.use(cookies());
app.use(async (ctx) => {
ctx.state.cookies.set('session', 'user-123', sessionOptions());
});Produces: httpOnly: true, sameSite: 'lax', path: '/'. No maxAge or expires.
Signed Cookies
Prevent cookie tampering with HMAC-SHA256 signatures. Signed cookies use a separate middleware and a separate context property.
import { signedCookies } from '@nextrush/cookies';
app.use(
signedCookies({
secret: process.env.COOKIE_SECRET!,
})
);
app.use(async (ctx) => {
// Set a signed cookie (async)
await ctx.state.signedCookies.set('user', 'user-123', { httpOnly: true });
// Get and verify a signed cookie (async)
const user = await ctx.state.signedCookies.get('user');
// Returns undefined if the signature is invalid or the cookie is missing
ctx.json({ user });
});Signed cookie operations (get and set) are async. Forgetting await returns a Promise, not
the value.
The signing secret must be kept secure and rotated periodically.
Never Hardcode Secrets
Store COOKIE_SECRET in environment variables. Rotate keys periodically using previousSecrets
for zero-downtime migration. Never commit signing keys to source control.
Key Rotation
Provide previous secrets to verify cookies signed with older keys during rotation:
app.use(
signedCookies({
secret: process.env.COOKIE_SECRET_NEW!,
previousSecrets: [process.env.COOKIE_SECRET_OLD!],
})
);New cookies are signed with secret. Verification tries secret first, then each entry in previousSecrets in order.
Cookie Prefixes
Security prefixes enforce browser-level constraints.
__Secure- Prefix
Requires secure: true. The helper auto-sets this flag.
import { createSecurePrefixCookie } from '@nextrush/cookies';
const header = createSecurePrefixCookie('token', 'abc123', { httpOnly: true });
// '__Secure-token=abc123; Secure; HttpOnly'__Host- Prefix
Requires secure: true, path: '/', and no domain. The helper enforces all three.
import { createHostPrefixCookie } from '@nextrush/cookies';
const header = createHostPrefixCookie('session', 'abc123', { httpOnly: true });
// '__Host-session=abc123; Secure; Path=/; HttpOnly'Parsing Utilities
Low-level cookie parsing for use outside the middleware:
import { parseCookies, getCookie, getCookieNames, hasCookie } from '@nextrush/cookies';
const cookies = parseCookies('name=value; session=abc');
// { name: 'value', session: 'abc' }
const session = getCookie('name=value; session=abc', 'session');
// 'abc'
const names = getCookieNames('name=value; session=abc');
// ['name', 'session']
const exists = hasCookie('name=value', 'name');
// trueSerialization Utilities
import { serializeCookie, createDeleteCookie } from '@nextrush/cookies';
const header = serializeCookie('name', 'value', {
httpOnly: true,
secure: true,
});
const deleteHeader = createDeleteCookie('name', { path: '/' });Signing Utilities
Manual cookie signing without the middleware:
import { signCookie, unsignCookie, unsignCookieWithRotation } from '@nextrush/cookies';
const signed = await signCookie('value', 'secret-key');
// 'value.BASE64_SIGNATURE'
const value = await unsignCookie(signed, 'secret-key');
// 'value' or undefined if tampered
const rotated = await unsignCookieWithRotation(signed, {
current: 'new-key',
previous: ['old-key'],
});Common Mistakes
Forgetting to await signed cookie operations
// Wrong — returns a Promise, not the value
const userId = ctx.state.signedCookies.get('userId');
// Correct
const userId = await ctx.state.signedCookies.get('userId');Using SameSite=None without Secure
// Browsers reject this combination
ctx.state.cookies.set('cross', 'value', { sameSite: 'none' });
// Correct
ctx.state.cookies.set('cross', 'value', { sameSite: 'none', secure: true });Setting secure cookies on HTTP
// Browser ignores the cookie on plain HTTP
ctx.state.cookies.set('session', 'value', { secure: true });Troubleshooting
Cookie not appearing in the browser:
Check that secure: true cookies are served over HTTPS. On localhost, most browsers allow secure cookies only on https://localhost.
Signed cookie returns undefined:
The signature verification failed. Confirm the secret matches the one used to sign the cookie. If you rotated keys, pass the old keys in previousSecrets.
SecurityError: Cookie with __Host- prefix must have Path set to "/":
__Host- prefixed cookies require secure: true, path: '/', and no domain attribute. Use createHostPrefixCookie() to handle these constraints automatically.
Related
- Middleware Overview — All middleware packages
- @nextrush/helmet — Security headers
- @nextrush/rate-limit — Rate limiting