Session Auth skill for Codex

Revocable logins behind a locked-down cookie. Codex sets up server-side sessions the way a security reviewer expects: an opaque ID in a __Host- HttpOnly SameSite cookie, state in Redis or the database, a fresh ID on every privilege change, enforced idle and absolute timeouts, Origin and token CSRF checks, and a per-user index so password resets can revoke every device. It ships tests for fixation, replay after logout and cross-origin POSTs.

  • Backend & APIs
  • $session-auth
  • 209 lines
  • 2 files
  • ~2,450 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- session-auth

Installs to ~/.agents/skills/session-auth/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $session-auth, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and audits server-side cookie session authentication - opaque session IDs, Redis or database session stores, HttpOnly/Secure/SameSite and __Host- cookies, ID rotation on login, idle and absolute timeouts, CSRF tokens and Origin checks, logout and revoke-all-devices. Covers express-session, Django, Rails and Flask. Use when adding login to a server-rendered or same-site app, fixing session fixation or CSRF findings, moving sessions to Redis, or choosing sessions over JWTs.

Use it when

  • A server-rendered or same-site SPA app needs login
  • Sessions must move from memory or signed cookies to a shared store
  • A security review flagged cookie flags, fixation or CSRF
  • Password change or account lockout must revoke existing logins

Use something else when

  • Mobile apps or third-party clients need bearer tokens: use jwt-auth
  • You want a ready-made auth framework rather than hand wiring: use auth-libraries
  • The task is a CSRF-only audit of an existing app: use csrf-protection

How to check the result

Inspect Set-Cookie for __Host-, HttpOnly, Secure and SameSite, then run tests proving the session ID changes at login, old cookies fail after logout, and foreign-Origin POSTs get 403.

The mistake it prevents

Logging in without regenerating the session ID, which lets an attacker who planted a known ID ride the victim's authenticated session.

~/.agents/skills/session-auth/SKILL.md

--- name: session-auth description: Builds and audits server-side cookie session authentication - opaque session IDs, Redis or database session stores, HttpOnly/Secure/SameSite and… license: MIT ---

Done well means: the browser holds only an unguessable opaque ID in a locked-down cookie, all session state lives server-side where it can be revoked instantly, the ID changes whenever privilege changes, and every state-changing request proves it came from your own origin.

Before changing anything

  1. Read AGENTS.md for the framework, test command and deployment shape.
  2. Find the current mechanism: rg -n "session|cookie|csrf" --type-add 'cfg:*.{env,yml,yaml,toml}' -t cfg -t py -t ts -t js -t rb.
  3. Answer these, because they decide the design:
    • Same site as the API (one registrable domain)? Sessions fit. Third-party clients, mobile apps or many APIs across domains? Consider jwt-auth or oauth2-oidc instead.
    • How many app instances? More than one requires a shared store (Redis, Postgres).
    • Is there a proxy or load balancer terminating TLS? The app must trust it to see https, or Secure cookies will not be set.
AttributeValueReason
Name__Host-sidPrefix forces Secure, Path=/, no Domain: subdomains cannot overwrite it
HttpOnlyonJavaScript (and XSS payloads) cannot read the ID
SecureonNever sent over plain HTTP
SameSiteLaxBlocks cross-site POSTs; keeps top-level link navigation logged in
Path/Required by __Host-
DomainomitHost-only; set a domain only if subdomains truly must share the session
Max-Ageomit or match absolute timeoutOmit for “session cookie”; server enforces timeouts anyway

Use SameSite=Strict only for high-value apps where losing the session on inbound links is acceptable. SameSite=None needs Secure and is only for genuine cross-site embedding; then CSRF tokens become mandatory, not defence in depth.

Session lifecycle rules

  • ID generation: at least 128 bits from a CSPRNG (crypto.randomBytes(32), secrets.token_urlsafe(32)). Framework defaults already do this; never roll your own from timestamps or user IDs.
  • Store hashed IDs if the session table could leak through backups or read replicas: store sha256(id) and look up by hash.
  • Rotate on every privilege change: login, logout, MFA step-up, role change, password change. Rotation (new ID, copy data, delete old) defeats session fixation.
  • Timeouts: idle timeout (15-30 min for sensitive apps, hours for low-risk) refreshed on activity, plus an absolute timeout (e.g. 12 h - 30 d) that never extends. Store created_at and last_seen_at in the session and enforce both server-side.
  • Logout: delete the server record, then clear the cookie with identical attributes. Clearing only the cookie leaves a replayable session.
  • Revoke all devices: keep a per-user index (user_sessions:<user_id> Redis set, or a user_id column) so password reset and “sign out everywhere” can delete them all.
  • Store little: user ID, auth time, MFA level, CSRF secret. Re-read roles from the database or a short cache, so permission changes apply without re-login.

Store choice

StoreUse whenWatch for
Redis / ValkeyDefault for multi-instance appsSet TTL = idle timeout; maxmemory-policy must not evict sessions silently (noeviction or a dedicated instance)
Postgres / MySQL tableAlready have the DB, moderate traffic, need audit queriesIndex on ID hash and user_id; a cleanup job for expired rows
Signed cookie (client-side)Tiny stateless apps onlyCannot revoke; size limit ~4 KB; secret rotation logs everyone out
In-memoryLocal dev onlyLost on restart, wrong with >1 instance

Flask’s default session and Rails’ default CookieStore are client-side signed cookies. They are fine for flash messages, not for revocable auth. Switch to a server store when “log out everywhere” or instant revocation is a requirement.

Wiring examples

Express (express-session + Redis)

import session from "express-session";
import { createClient } from "redis";
// connect-redis export style differs by major version; check its README for your version.
import { RedisStore } from "connect-redis";

const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();

app.set("trust proxy", 1); // behind one TLS-terminating proxy, so Secure cookies work
app.use(session({
  name: "__Host-sid",
  store: new RedisStore({ client: redis, prefix: "sess:" }),
  secret: [process.env.SESSION_SECRET_NEW!, process.env.SESSION_SECRET_OLD!].filter(Boolean),
  resave: false,
  saveUninitialized: false,        // no session (and no cookie) until login
  rolling: true,                   // refresh idle timeout on activity
  cookie: { httpOnly: true, secure: true, sameSite: "lax", path: "/", maxAge: 30 * 60_000 },
}));

app.post("/login", async (req, res, next) => {
  const user = await verifyCredentials(req.body);           // constant-time compare inside
  if (!user) return res.status(401).json({ error: "invalid_credentials" });
  req.session.regenerate((err) => {                         // rotation: defeats fixation
    if (err) return next(err);
    req.session.userId = user.id;
    req.session.authAt = Date.now();
    req.session.save((err2) => (err2 ? next(err2) : res.status(204).end()));
  });
});

app.post("/logout", (req, res, next) => {
  req.session.destroy((err) => {
    if (err) return next(err);
    res.clearCookie("__Host-sid", { path: "/", secure: true, httpOnly: true, sameSite: "lax" });
    res.status(204).end();
  });
});

The secret array lets you rotate: new secret first signs, old one still verifies.

Django

SESSION_ENGINE = "django.contrib.sessions.backends.cache"   # or "db" / "cached_db"
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_AGE = 60 * 60 * 12          # absolute-ish; enforce idle in middleware if needed
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")  # only behind a proxy that sets it

django.contrib.auth.login() already rotates the key; call request.session.cycle_key() yourself after MFA step-up. logout() flushes the session.

Rails

Call reset_session before setting session[:user_id] on login (the Rails 8 authentication generator does this). Keep protect_from_forgery with: :exception.

CSRF

Cookies are sent automatically, so a session app must prove request origin:

  1. SameSite=Lax cookie (baseline).
  2. For every unsafe method (POST, PUT, PATCH, DELETE), verify one of:
    • a synchronizer token tied to the session (Django, Rails, Laravel do this by default), or
    • a signed double-submit token for SPAs (csrf-csrf for Express; csurf is deprecated).
  3. Also reject requests whose Origin header is present and not in your allowlist, or whose Sec-Fetch-Site is cross-site. Cheap and catches most attacks on its own.
  4. Never perform state changes on GET.
const ALLOWED = new Set(["https://app.example.com"]);
app.use((req, res, next) => {
  if (["GET", "HEAD", "OPTIONS"].includes(req.method)) return next();
  const origin = req.get("origin");
  if (origin && !ALLOWED.has(origin)) return res.status(403).end();
  if (req.get("sec-fetch-site") === "cross-site") return res.status(403).end();
  next();
});

For deeper CSRF analysis (login CSRF, JSON content-type tricks), use csrf-protection.

Workflow checklist

  • Confirm sessions fit (same-site browser clients) and pick the store
  • Set cookie attributes from the table; configure proxy trust
  • Regenerate the session on login, step-up and privilege change
  • Enforce idle and absolute timeouts server-side
  • Implement logout (server delete + cookie clear) and revoke-all
  • Add CSRF token or Origin checks on all unsafe methods
  • Write the tests below; run the project’s test command from AGENTS.md

Verification

curl -si -X POST https://localhost:3000/login -H 'content-type: application/json' \
  -d '{"email":"[email protected]","password":"test-pass"}' | grep -i set-cookie

Check that Set-Cookie shows __Host-, HttpOnly, Secure, SameSite=Lax, Path=/ and no Domain. Then cover in automated tests:

  • Session ID before login differs from the ID after login (fixation test).
  • Replaying the old cookie after logout returns 401.
  • After password change, a second session for the same user is rejected.
  • A POST with a foreign Origin or missing CSRF token returns 403.
  • An idle session older than the timeout is rejected even if the cookie is still sent.

Gotchas

  • Secure cookies silently not set behind a TLS proxy because the app sees http. Fix proxy trust (trust proxy, SECURE_PROXY_SSL_HEADER), not by dropping Secure.
  • saveUninitialized: true creates a store entry for every bot request and fills Redis.
  • Clearing a cookie with different Path/Domain than it was set with does nothing.
  • Redis eviction under memory pressure logs users out at random; size it or use noeviction.
  • Caching layers (CDN, reverse proxy) must never cache responses carrying Set-Cookie or personalised pages; send Cache-Control: private, no-store on authenticated pages.
  • A wildcard Domain=.example.com cookie lets any compromised subdomain read or plant sessions. Prefer host-only.
  • Password hashing and login throttling belong next to this work: use password-storage and authentication-security.

Output

When reporting, list: store and TTLs, cookie attributes as shipped, rotation points, CSRF mechanism, revoke-all path, tests added, and anything deferred (for example, MFA).

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Session Auth"
  short_description: "Revocable logins behind a locked-down cookie"
  default_prompt: "$session-auth Add login sessions to this Express app backed by Redis"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc