Authentication Security skill for Codex
Close every side door into the account. Codex inventories every route into an account, including reset, email change, MFA disable and OAuth callbacks, and holds each one to NIST 800-63B rev 4 rules. It checks enumeration through messages and timing, adds per-account and per-source throttling instead of naive lockout, tests MFA bypass paths, and writes failing tests for each gap before fixing it.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- authentication-security Installs to ~/.agents/skills/authentication-security/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install authentication-security 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 authentication-security Installs to .agents/skills/authentication-security/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/authentication-security.zipunzip -o authentication-security.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))) authentication-security 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 $authentication-security, 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
Hardens login, sign-up, MFA and account recovery flows against credential stuffing, brute force, account enumeration, phishing and recovery takeover, following NIST SP 800-63B rev 4 and OWASP ASVS 5.0. Covers passkeys/WebAuthn, TOTP, rate limits and lockout, breached-password checks, session fixation and step-up auth. Use when building or reviewing a login form, adding MFA or passkeys, designing password reset or email change, or after a credential-stuffing spike.
Use it when
- Building or reviewing login, sign-up or password reset endpoints
- Adding MFA, passkeys or recovery codes
- Responding to a spike in failed logins or account takeovers
- Designing email change or other sensitive account actions
Use something else when
- You only need to choose or migrate a password hashing algorithm: use password-storage
- You are reviewing an OAuth or OpenID Connect integration: use oauth-security-review
- You need cookie session storage design rather than login hardening: use session-auth
How to check the result
Tests show identical responses for unknown and known users, a 429 after repeated OTP guesses, and session ID rotation on login; a reset link always uses the configured base URL.
The mistake it prevents
Locking down the login form while the password reset, email change or API token path still lets an attacker in with less proof.
Done well means: an attacker with a list of leaked passwords, a phishing page, or access to the victim’s email inbox for ten minutes still cannot take the account, while a real user can always get back in through a recovery path that is no weaker than login itself.
Before changing anything
- Read
AGENTS.mdfor the framework, auth library and test command. - Map every way into an account, not just the login form:
rg -n -i "login|signin|sign_in|authenticate|reset|forgot|recover|magic.?link|otp|totp|webauthn|passkey|change.?email|verify" \
--type-add 'web:*.{ts,tsx,js,py,rb,go,java,kt,php,cs}' -t web -l
- List each entry point in a table before reviewing: login, sign-up, password reset request, reset confirm, email change, MFA enrol, MFA disable, recovery codes, OAuth callback, API token login, admin impersonation. The weakest one is the real login.
- Find out whether auth is hand-rolled or delegated (Auth.js, Devise, Django allauth, Spring Security, Keycloak, Clerk, Cognito). If delegated, audit configuration, not the library internals.
Baseline requirements
| Area | Requirement |
|---|---|
| Password length | Minimum 15 chars when the password is the only factor, 8 when always paired with MFA (800-63B-4). Accept at least 64. Allow spaces and all Unicode; normalise with NFKC before hashing |
| Composition rules | None. No forced digits/symbols, no periodic expiry. Force a change only on evidence of compromise |
| Breached passwords | Reject passwords found in a breach corpus (HIBP k-anonymity range API, or an offline list when network is restricted) |
| Storage | Argon2id or bcrypt per $password-storage |
| Failed attempts | Per-account throttling plus per-IP and global anomaly limits; 800-63B caps consecutive failures at 100 per account before a stronger control |
| Errors | One generic message for unknown user, wrong password, locked or unverified account |
| Session | New session ID after login and after privilege change; old one invalidated |
| MFA | Offer passkeys first, TOTP second; SMS only as a fallback with risk noted |
| Recovery | Single-use, short-lived, hashed tokens; never weaker than login |
| Sensitive actions | Re-authenticate (step-up) for email change, password change, MFA disable, payout details |
Workflow
- [ ] 1. Inventory entry points (table above) and the auth stack
- [ ] 2. Check enumeration on login, sign-up and reset (responses AND timing)
- [ ] 3. Check throttling on every credential-accepting endpoint
- [ ] 4. Check session handling at login, logout, password change
- [ ] 5. Check MFA enrolment, verification and bypass paths
- [ ] 6. Check recovery and email-change flows end to end
- [ ] 7. Write failing tests for each gap, then fix, then re-run
- [ ] 8. Report findings with severity and residual risk
Account enumeration
Enumeration turns a credential-stuffing list into a targeted one. Check three channels:
- Message: “Invalid email or password” for every failure. Reset request always says “If an account exists, we sent a link.”
- Status and shape: same HTTP status, same JSON keys, same redirect target.
- Timing: when the user is unknown, still run a dummy hash verify so the response takes as long as a real one.
DUMMY_HASH = ph.hash("dummy-password-for-timing")
def authenticate(email: str, password: str) -> User | None:
user = users.find_by_email(normalise_email(email))
if user is None:
try:
ph.verify(DUMMY_HASH, password) # equalise timing
except VerifyMismatchError:
pass
return None
try:
ph.verify(user.password_hash, password)
except VerifyMismatchError:
return None
return user
Sign-up is the hardest place to hide existence. Accepted pattern: always respond “check your email”, and send the existing user a “someone tried to register with your address” email instead of creating a duplicate.
Throttling and lockout
Hard lockout after N failures lets anyone lock out any user. Prefer escalating friction:
| Signal | Response |
|---|---|
| 5 failures on one account in 15 min | Exponential delay per account (1 s, 2 s, 4 s … capped) |
| 10+ failures on one account | Require CAPTCHA or email verification for the next attempt |
| One IP or ASN failing across many accounts | Throttle that source; this is stuffing, not a forgetful user |
| Global failure ratio jumps (e.g. 3x baseline) | Alert; enable challenge for all logins |
| Success after many failures | Notify the user; consider forcing MFA or reset |
Key throttles on the normalised account identifier and on the source, both. Store
counters in Redis or the DB, not process memory, or multiple instances reset each other.
Apply the same limits to reset confirm, OTP verify and recovery-code endpoints: a
six-digit TOTP is brute-forceable at 1,000,000 guesses without a limit. For the limiter
implementation itself, see $api-rate-limiting.
MFA
| Factor | Phishing resistant | Notes |
|---|---|---|
| Passkey / WebAuthn | Yes | Origin-bound. Default for new builds. Use a maintained library (SimpleWebAuthn, py_webauthn, webauthn4j, go-webauthn) |
| TOTP (RFC 6238) | No | 30 s step, accept +/-1 step, reject reuse of an already-accepted code, encrypt the seed at rest |
| Push approval | No | Require number matching to stop fatigue attacks |
| SMS / voice OTP | No | SIM swap risk; fallback only |
| Recovery codes | n/a | 10 single-use codes, about 50 bits each, stored hashed, shown once |
Common bypasses to test:
- MFA checked on the login page but the session is already fully authenticated before the second step. The pre-MFA session must only be able to call the MFA endpoint.
- The “remember this device” cookie is not bound to the user or never expires.
- MFA can be disabled, or the email changed, without re-entering a factor.
- API tokens or OAuth logins skip MFA that the password login enforces.
- The OTP verify endpoint has no attempt limit.
Recovery and email change
- [ ] Reset token: >= 128 bits from a CSPRNG, stored as SHA-256 hash, single use
- [ ] Expires in 15-60 minutes; issuing a new token invalidates older ones
- [ ] Reset link built from a configured base URL, never the request Host header
- [ ] Reset page sends Referrer-Policy: no-referrer (token is in the URL)
- [ ] Successful reset revokes all sessions and refresh tokens, notifies the user
- [ ] Reset does not bypass MFA: ask for the second factor or a recovery code after it
- [ ] Email change: confirm via the NEW address, notify the OLD address with an undo link
- [ ] Security questions are not used
Host header poisoning is the classic: request.host used to build the reset URL lets an
attacker send the victim a link to the attacker’s domain carrying a valid token.
Sessions at the auth boundary
- Rotate the session identifier on login (fixation) and on MFA completion.
- Cookies:
Secure,HttpOnly,SameSite=Lax(orStrictfor admin apps),__Host-prefix when no subdomain sharing is needed. - Logout invalidates server-side state, not just the cookie.
- Password change offers “sign out other sessions” and does it by default after a reset.
- For session storage design, see
$session-auth; for JWT specifics,$jwt-auth.
Verification
Write tests that fail before the fix:
def test_login_does_not_enumerate(client):
a = client.post("/login", data={"email": "[email protected]", "password": "x" * 16})
b = client.post("/login", data={"email": "[email protected]", "password": "wrong" * 4})
assert a.status_code == b.status_code
assert a.json() == b.json()
def test_otp_attempts_are_limited(client, logged_in_pre_mfa):
for _ in range(10):
client.post("/mfa/verify", data={"code": "000000"})
r = client.post("/mfa/verify", data={"code": valid_code()})
assert r.status_code == 429
Also measure timing for unknown vs known users over 50 requests each on a local build; medians should be within a few milliseconds. Run tests only against local or staging environments the user controls.
Output template
## Authentication review: <app>
Stack: <framework / auth library> | Entry points reviewed: <n>
| # | Severity | Entry point | Finding | Evidence | Fix |
|---|---|---|---|---|---|
| 1 | High | POST /password/reset | Reset URL uses Host header | app/auth/reset.py:41 | Use settings.BASE_URL |
### Passed checks
### Residual risk and follow-ups
Gotchas
- Normalise identifiers once (trim, lowercase the domain, NFKC) and use the same function
at sign-up, login and reset, or
[email protected]and[email protected]become two accounts. - Rate limits keyed only on IP do nothing against residential-proxy stuffing; key on the account too.
- Truncating passwords silently (bcrypt at 72 bytes, DB column length) makes long passphrases weaker than users think.
- Magic links in email are a password reset on every login: same token rules apply.
- Admin “log in as user” must be audited and must not reuse the user’s MFA state.
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: "Authentication Security"
short_description: "Close every side door into the account"
default_prompt: "$authentication-security Review our login and password reset flow for security holes" 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.