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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- session-auth Installs to ~/.agents/skills/session-auth/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install session-auth The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project session-auth Installs to .agents/skills/session-auth/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/session-auth.zipunzip -o session-auth.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) session-auth PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- Read
AGENTS.mdfor the framework, test command and deployment shape. - 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. - 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-authoroauth2-oidcinstead. - 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, orSecurecookies will not be set.
- Same site as the API (one registrable domain)? Sessions fit. Third-party clients, mobile
apps or many APIs across domains? Consider
Cookie settings (the standard)
| Attribute | Value | Reason |
|---|---|---|
| Name | __Host-sid | Prefix forces Secure, Path=/, no Domain: subdomains cannot overwrite it |
HttpOnly | on | JavaScript (and XSS payloads) cannot read the ID |
Secure | on | Never sent over plain HTTP |
SameSite | Lax | Blocks cross-site POSTs; keeps top-level link navigation logged in |
Path | / | Required by __Host- |
Domain | omit | Host-only; set a domain only if subdomains truly must share the session |
Max-Age | omit or match absolute timeout | Omit 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_atandlast_seen_atin 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 auser_idcolumn) 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
| Store | Use when | Watch for |
|---|---|---|
| Redis / Valkey | Default for multi-instance apps | Set TTL = idle timeout; maxmemory-policy must not evict sessions silently (noeviction or a dedicated instance) |
| Postgres / MySQL table | Already have the DB, moderate traffic, need audit queries | Index on ID hash and user_id; a cleanup job for expired rows |
| Signed cookie (client-side) | Tiny stateless apps only | Cannot revoke; size limit ~4 KB; secret rotation logs everyone out |
| In-memory | Local dev only | Lost 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:
SameSite=Laxcookie (baseline).- 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-csrffor Express;csurfis deprecated).
- Also reject requests whose
Originheader is present and not in your allowlist, or whoseSec-Fetch-Siteiscross-site. Cheap and catches most attacks on its own. - 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
Originor missing CSRF token returns 403. - An idle session older than the timeout is rejected even if the cookie is still sent.
Gotchas
Securecookies silently not set behind a TLS proxy because the app seeshttp. Fix proxy trust (trust proxy,SECURE_PROXY_SSL_HEADER), not by droppingSecure.saveUninitialized: truecreates a store entry for every bot request and fills Redis.- Clearing a cookie with different
Path/Domainthan 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-Cookieor personalised pages; sendCache-Control: private, no-storeon authenticated pages. - A wildcard
Domain=.example.comcookie lets any compromised subdomain read or plant sessions. Prefer host-only. - Password hashing and login throttling belong next to this work: use
password-storageandauthentication-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.yaml
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.