NextRush
API ReferenceMiddleware

@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 via document.cookie
  • sameSite: 'lax' — prevents CSRF on cross-origin POST requests
  • path: '/' — 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

PropertyTypeDescription
httpOnlyboolean= trueBlock JavaScript access via document.cookie
secureboolean= falseSend only over HTTPS. Required for SameSite=None and cookie prefixes.
sameSite'strict' | 'lax' | 'none' | boolean= 'lax'CSRF protection level
maxAge?numberLifetime in seconds. Use 0 to expire immediately.
expires?Date | numberExpiration date. If both maxAge and expires are set, maxAge takes precedence.
pathstring= '/'URL path scope for the cookie
domain?stringDomain scope. Leading dots are ignored.
priority?'low' | 'medium' | 'high'Browser priority hint (Chrome extension)
partitioned?booleanPartition 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.


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');
// true

Serialization 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

// 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.


On this page