Token Security skill for Codex
Keys you can detect, scope, rotate and kill. Codex designs API keys, access tokens and webhook secrets so that a leak is detectable and short-lived: prefixed high-entropy formats that scanners recognise, digests instead of raw tokens in the database, resource-level scopes, expiry and last-used tracking. It adds dual-secret rotation for zero-downtime changes and refresh token reuse detection, and moves tokens out of URLs and localStorage.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- token-security Installs to ~/.agents/skills/token-security/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install token-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 token-security Installs to .agents/skills/token-security/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/token-security.zipunzip -o token-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))) token-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 $token-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
Designs and reviews API keys, personal access tokens, refresh tokens, webhook secrets and service credentials - prefixed high-entropy formats that secret scanners can detect, hashed storage, least-privilege scopes, expiry, rotation with overlap, revocation, last-used tracking and safe transport and browser storage. Use when building an API key system, issuing tokens to customers or services, reviewing how tokens are stored or compared, planning key rotation, or deciding between cookies and localStorage for tokens.
Use it when
- Building API keys or personal access tokens for customers
- Reviewing how tokens are generated, stored, compared or logged
- Planning rotation for webhook secrets or service credentials
- Choosing token storage for a browser or mobile client
Use something else when
- You are validating or issuing JWTs specifically: use jwt-auth
- A secret was committed to git and must be found or purged: use secret-scanning
- You manage secrets for CI pipelines: use secrets-in-ci
How to check the result
Tests reject revoked, expired, wrong-scope and wrong-environment keys; the database holds only digests and last-4 characters; a staging rotation drill shows no failed requests.
The mistake it prevents
Storing raw API keys in the database or logging the Authorization header, so one read-only leak exposes every customer's credentials.
Done well means: a leaked token is detectable (recognisable format), useless after a short time or a single revoke, limited to the smallest scope that works, and never recoverable from your database; and rotating any secret is a routine, zero-downtime operation rather than an outage.
Inspect first
rg -n -i "api_?key|access_?token|refresh_?token|personal_?access|secret|bearer|x-api-key|webhook_secret|signing_secret" -g '!node_modules' -g '!*.lock' | head -80
rg -n "localStorage\.(set|get)Item\(.*(token|jwt|key)" -i -g '!node_modules'
rg -n "==\s*(api_?key|token)|token\s*===|\.equals\(.*[Tt]oken" -g '!node_modules'
Inventory every credential type: who issues it, to whom, what it can do, how long it lives, where it is stored (server and client), how it is revoked.
Choose the token type
| Need | Token | Lifetime |
|---|---|---|
| Customer or developer calling your API | Opaque API key / personal access token, scoped | Expiry chosen at creation (default 90 days, max 1 year), revocable |
| Browser session for your own web app | Session cookie (opaque ID) | Idle 30 min - 24 h, absolute 7-30 days |
| SPA or mobile app via OAuth | Short access token + rotating refresh token | Access 5-15 min; refresh 1-30 days with reuse detection |
| Service to service inside your platform | Workload identity (cloud IAM, SPIFFE, OIDC federation) | Minutes, auto-rotated |
| CI to cloud | OIDC federation, no stored key | Per job |
| Webhook signing | Per-endpoint HMAC secret | Rotate on demand with dual secrets |
Opaque tokens checked against a store are revocable instantly. Self-contained JWTs are
not, unless you add a denylist; keep them short-lived. JWT validation specifics belong to
$jwt-auth.
API key format
<prefix>_<random>_<checksum>
acme_live_4fQ9tY...Zk2 (prefix + env, 32 bytes base62 random, CRC32 checksum)
- Prefix (
acme_live_,acme_test_) lets secret scanners, log filters and support identify the key and its environment. Register the pattern with GitHub secret scanning partner program if you are a public API provider. - At least 128 bits (prefer 256) from a CSPRNG:
secrets.token_urlsafe(32),crypto.randomBytes(32),crypto/rand. - Checksum lets you reject typos and scanner false positives offline without a DB hit.
- Show the full key exactly once at creation. Afterwards show only the prefix and last 4 characters.
Storage and comparison
High-entropy random tokens do not need slow password hashing. Store SHA-256(token)
(or HMAC-SHA256 with a server key) and look up by the hash:
import hashlib, hmac, secrets
def issue_key(env: str) -> tuple[str, str]:
raw = f"acme_{env}_{secrets.token_urlsafe(32)}"
return raw, hashlib.sha256(raw.encode()).hexdigest() # give raw to user, store digest
def authenticate(presented: str):
digest = hashlib.sha256(presented.encode()).hexdigest()
row = db.api_keys.find_one(digest=digest, revoked_at=None)
if row is None or row.expires_at < now():
return None
db.api_keys.touch_last_used(row.id) # async/batched in practice
return row
When you must compare secrets directly (webhook signatures, static shared secrets), use
constant-time comparison: hmac.compare_digest, crypto.timingSafeEqual (equal-length
buffers), subtle.ConstantTimeCompare, MessageDigest.isEqual.
Passwords chosen by humans are different: use $password-storage.
Scopes and least privilege
- Scopes are resource + action (
orders:read,orders:write,billing:admin), checked on every endpoint, defaulting to deny. - Restrict keys further where the product allows: allowed IP ranges, specific projects or tenants, read-only by default.
- Separate test and live keys with different prefixes and backends; a test key must never work against live data.
- Admin and billing scopes require shorter expiry and step-up re-authentication to create.
Rotation without downtime
- [ ] Support two valid secrets per credential at once (current + next)
- [ ] Issue the new secret; deploy it to consumers
- [ ] Watch last_used_at: traffic moves from old to new
- [ ] Revoke the old secret (confirm with the user before revoking production credentials)
- [ ] Record rotation date; schedule the next one or alert on age
For webhook signing, send signatures for both secrets during the overlap (for example
v1=<sig_old>,v1=<sig_new>) so receivers can switch at their own pace.
Transport and client storage
| Rule | Reason |
|---|---|
Tokens in Authorization header, never in URLs | URLs land in logs, history, Referer headers and analytics |
| HTTPS only; reject plain HTTP requests carrying keys, then revoke that key | It has already crossed the network in clear text |
Browser apps: prefer HttpOnly; Secure; SameSite cookies or a backend-for-frontend | localStorage is readable by any XSS |
| If tokens must live in JS, keep them in memory, short-lived, with refresh via HttpOnly cookie | Limits theft window |
| Mobile: Keychain / Android Keystore, never shared preferences in plain text | |
| Redact tokens in logs, error trackers and APM (match your prefix) | Logs are the most common leak path |
Refresh token rotation
- Each refresh returns a new refresh token and invalidates the old one.
- Reuse of an already-used refresh token means theft: revoke the whole token family and force re-login.
- Bind refresh tokens to the client (DPoP or mTLS for high-value APIs) where supported.
Leak response
When a token leaks (commit, log, screenshot, support ticket): revoke first, then
investigate usage via last_used_at and access logs, then notify the owner. Purging it
from git history comes after revocation; see $secret-scanning. Revoking a customer’s
production key affects their service: confirm with the user and notify the customer.
Verification
- Unit tests: a revoked key, an expired key, a key with the wrong scope and a test-env key on live all return 401/403.
rgfor the token prefix in log output from a test run returns nothing.- Database inspection shows only digests, prefixes and last-4, no raw tokens.
- A rotation drill in staging: issue new, switch consumer, revoke old, no failed requests.
Output template
## Token review: <system>
| Credential | Format | Storage | Scope | Expiry | Rotation | Issues |
|---|---|---|---|---|---|---|
| Customer API key | acme_live_ + 32B | sha256 digest | per-scope | none -> 90 d | manual -> dual-key | raw key logged in access log |
Leak detection: <prefix registered with scanners? log redaction?>
Next actions: <ordered list>
Gotchas
- Logging the full
Authorizationheader in request logs or error reports is the most common way keys leak; scrub at the logger, not at each call site. crypto.timingSafeEqualthrows on unequal lengths; hash both sides first or check lengths in a way that does not leak the secret’s length.- Rate-limit authentication failures per key prefix and per IP to slow online guessing.
- Long-lived keys with no
last_used_atcannot be safely cleaned up; add tracking first.
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: "Token Security"
short_description: "Keys you can detect, scope, rotate and kill"
default_prompt: "$token-security Design an API key system for our public API with scopes and rotation" 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.