Auth and Sessions skill for Claude Code

Login, sessions and recovery an attacker can't shortcut. Makes Claude build login the way a security engineer would: argon2id with transparent rehashing, NIST rev. 4 password rules with breach checks, uniform responses that prevent enumeration, session IDs rotated at login, __Host- cookies, server-enforced MFA, and reset tokens that are hashed, short-lived and single-use. Includes an audit checklist for existing auth code.

  • nameauth-sessions
  • size176 lines · ≈2,050 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Implements and audits first-party authentication — argon2id password hashing, login throttling, server-side sessions vs. tokens, cookie flags (__Host-, Secure, HttpOnly, SameSite), session rotation and timeouts, CSRF, MFA with TOTP and passkeys, and safe password reset and account recovery. Use when building login, signup, logout, "remember me", password reset, or MFA, when choosing sessions vs. JWTs for a web app, or when reviewing auth code for weaknesses.

Try asking

  • Build email and password login for our Flask app with secure sessions
  • Audit our password reset flow for account takeover risks
  • Should our Next.js app use JWTs in localStorage or session cookies?

Use it when

  • Building signup, login, logout, remember-me, or password reset
  • Adding MFA with passkeys or TOTP
  • Choosing between server-side sessions and tokens for a web app
  • Reviewing first-party auth code before launch

Skip it when

  • You are integrating Clerk, Auth0, or Auth.js rather than building auth — use auth-providers instead
  • The work is delegated login with an external IdP — use oauth-oidc instead

How to check the result

Tests show the session ID changes at login, logout kills the server-side session, reset tokens work once and expire, and curl -i shows Secure, HttpOnly and SameSite on the session cookie.

The common mistake it prevents

Forgetting to regenerate the session ID after login, which lets an attacker who planted a session ID ride the victim's authenticated session.

~/.claude/skills/auth-sessions/SKILL.md

--- name: auth-sessions description: Implements and audits first-party authentication — argon2id password hashing, login throttling, server-side sessions vs. tokens, cookie… license: MIT ---

Done well means: passwords are stored with a memory-hard hash and checked against breach lists; login cannot be brute-forced or used to enumerate accounts; sessions are unguessable, rotated at privilege changes, revocable, and carried in hardened cookies; recovery is no weaker than login. If a managed provider fits, prefer it and integrate it correctly (see auth-providers).

Decision: sessions or tokens

SituationUse
Server-rendered or same-site SPA + API on your domainServer-side session, opaque ID in a cookie (default)
Mobile app or third-party API clientsShort-lived access token + refresh token (rotate refresh on use)
Service-to-servicemTLS or OAuth client credentials, not user sessions
Need instant logout / revocation everywhereServer-side sessions (JWTs need a denylist to revoke)

Do not put JWTs in localStorage for a browser app to “avoid cookies”: any XSS reads them. A cookie with HttpOnly cannot be read by script. For JWT specifics use jwt-handling.

Password storage

Default: argon2id. Minimum parameters (OWASP): memory 19 MiB, iterations 2, parallelism 1. Raise memory if login latency budget allows (target 100–500 ms per hash on production hardware). Acceptable fallbacks: scrypt, bcrypt (cost ≥ 10; note bcrypt ignores bytes past 72), PBKDF2-HMAC-SHA256 ≥ 600,000 iterations (FIPS contexts).

Python (pip install argon2-cffi):

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher()  # argon2id, RFC 9106 low-memory profile defaults

def set_password(user, raw: str) -> None:
    user.password_hash = ph.hash(raw)

def check_password(user, raw: str) -> bool:
    try:
        ph.verify(user.password_hash, raw)
    except VerifyMismatchError:
        return False
    if ph.check_needs_rehash(user.password_hash):
        user.password_hash = ph.hash(raw)   # transparent upgrade on login
    return True

Node (npm i argon2):

import argon2 from "argon2";
const hash = await argon2.hash(password, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 });
const ok = await argon2.verify(hash, password);

Migrating from a weak hash: on next successful login, verify with the old scheme and re-hash with argon2id. For dormant accounts, wrap the old hash (argon2id(md5_hash)) in a batch job so nothing weak remains at rest.

Password policy (NIST SP 800-63B rev. 4)

  • Minimum 15 characters when the password is the only factor; 8 when MFA is enforced. Allow at least 64 characters and all Unicode/spaces.
  • No composition rules (one uppercase, one symbol) and no periodic forced rotation. Force change only on evidence of compromise.
  • Reject passwords found in breach corpora. The Pwned Passwords range API uses k-anonymity: send the first 5 hex chars of the SHA-1, compare suffixes locally: GET https://api.pwnedpasswords.com/range/{first5}.

Login flow

  1. Normalize the identifier (trim, lowercase email); never the password.
  2. Rate limit per account and per IP (e.g. 5 failures per account per 15 min with exponential backoff; per-IP limits higher to tolerate NAT). Prefer progressive delays and CAPTCHA over hard lockout, which enables denial of service against users.
  3. Same response and similar timing for unknown user and wrong password. When the user does not exist, still run a dummy hash verify.
  4. On success: regenerate the session ID (prevents session fixation), record login event (user, IP, user agent, time), then check MFA before granting a full session.
  5. Log failures with reason internally; return a generic message externally.

Session management

  • ID: ≥ 128 bits from a CSPRNG (secrets.token_urlsafe(32), crypto.randomBytes(32)). Store server-side (DB/Redis) keyed by a hash of the ID so a DB leak does not yield live sessions.

  • Cookie:

    Set-Cookie: __Host-session=<id>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1209600

    __Host- forces Secure, Path=/, and no Domain attribute (no subdomain sharing). Use SameSite=Strict for admin apps; Lax for general apps so top-level links work.

  • Timeouts: idle timeout (15–30 min for sensitive apps, longer for low risk) and an absolute timeout (e.g. 12 h to 30 days with “remember me”). Enforce server-side.

  • Rotate the ID on login, logout, privilege change, and MFA step-up.

  • Logout deletes the server-side record, not just the cookie.

  • Password change or reset: invalidate all other sessions; show “active sessions” with revoke buttons.

  • Store and display device/session metadata; alert on new-device logins for sensitive apps.

CSRF

SameSite=Lax blocks most cross-site POSTs but not same-site attacks (sibling subdomains) or GET-based state changes. For cookie-authenticated, state-changing requests also use one of: framework CSRF tokens (Django, Rails, Laravel built-ins; synchronizer or double-submit token), or verify Origin/Sec-Fetch-Site headers and reject cross-site. Never change state on GET.

MFA

FactorUse
Passkeys / WebAuthnBest default; phishing-resistant. Use a maintained library (SimpleWebAuthn, py_webauthn, webauthn4j)
TOTP (RFC 6238)Good fallback. 30 s step, allow ±1 step drift, reject reused codes, store secret encrypted
SMS / email codesWeakest (SIM swap, phishing); allow only as recovery or low-risk option
Recovery codes8–10 single-use codes, hashed at rest, shown once

Enforce MFA on the server for every endpoint after login: a “pending MFA” session must not access anything but the MFA verify endpoint. Rate limit code attempts (e.g. 5 per session, then re-authenticate).

Password reset and account recovery

  • Token: ≥ 128 bits CSPRNG, store only its SHA-256 hash, single use, expires in 15–60 minutes, invalidated when a newer token is issued or the password changes.
  • Same response whether the email exists or not (“If an account exists, we sent a link”).
  • Build the reset link from a configured base URL, never from the request Host header (host header poisoning sends tokens to attackers).
  • Do not log users in automatically after reset if MFA is enabled; require the second factor.
  • After reset: invalidate all sessions, notify the user by email.
  • Email change: confirm via the new address and notify the old address with a revert link.
  • Security questions are not a recovery factor.

Audit checklist

- [ ] argon2id/scrypt/bcrypt with adequate cost; no fast hashes; rehash on login
- [ ] breached-password check; length-based policy; no forced rotation
- [ ] per-account and per-IP throttling on login, MFA, reset, signup
- [ ] uniform responses and timing for unknown users
- [ ] session ID regenerated on login and privilege change
- [ ] cookies: __Host- prefix, Secure, HttpOnly, SameSite; no tokens in URLs
- [ ] idle and absolute timeouts enforced server-side; logout destroys server state
- [ ] CSRF protection on cookie-authenticated state changes
- [ ] MFA enforced server-side on every route; codes rate limited and single-use
- [ ] reset tokens hashed, short-lived, single-use; link built from config base URL
- [ ] auth events logged without passwords or tokens

Gotchas

  • Comparing tokens with == leaks timing; use hmac.compare_digest / crypto.timingSafeEqual.
  • “Remember me” implemented as a long-lived session ID without rotation turns one stolen cookie into months of access; rotate on use and bind to a server record.
  • OAuth “Sign in with X” accounts linked by unverified email allow account takeover; link only on verified email from the IdP, or require login to the existing account first.
  • Bcrypt with input pre-hashing (e.g. bcrypt(sha256(pw))) must base64 the digest first; raw bytes can contain NUL and truncate.

Verification

  • Unit tests: wrong password fails, unknown user takes comparable time, session ID differs before and after login, logout invalidates the old ID server-side, reset token works once and fails after expiry.
  • Inspect real responses (curl -i) for cookie attributes.
  • Attempt 20 rapid failed logins in a test environment and confirm throttling engages.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc