OAuth Security Review skill for Codex
Catch the takeover before it ships. Codex reviews an OAuth or OIDC integration against the OAuth 2.1 baseline: correct flow for the client type, PKCE, exact redirect_uri matching, verified state and nonce, full ID token validation against JWKS, and tokens kept out of localStorage and URLs. It greps for legacy flows and disabled verification, traces the real account-takeover chains, and writes findings with fixes.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- oauth-security-review Installs to ~/.agents/skills/oauth-security-review/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install oauth-security-review 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 oauth-security-review Installs to .agents/skills/oauth-security-review/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/oauth-security-review.zipunzip -o oauth-security-review.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))) oauth-security-review 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 $oauth-security-review, 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
Reviews OAuth 2.0 and OpenID Connect implementations for the flaws that cause account takeover - wrong grant choice, missing PKCE, weak redirect_uri matching, missing or unverified state and nonce, token leakage and storage, audience and issuer validation, scope creep, and refresh-token handling. Covers clients (SPA, mobile, server) and the relying-party side of an IdP integration. Use when adding "Sign in with", wiring an OAuth/OIDC client or provider, reviewing an auth integration PR, or responding to a finding about redirect URIs, state or tokens.
Use it when
- You are adding a social or enterprise sign-in flow
- An auth integration PR needs a security pass
- A finding mentions redirect URIs, state, nonce or token storage
- You are moving off the implicit or password grant
Use something else when
- You are building first-party password login and MFA: use authentication-security
- The question is how to store and validate JWTs you issue yourself: use token-security
How to check the result
A tampered or missing state is rejected, a lookalike redirect_uri is refused, an ID token with the wrong aud or signature is rejected, and no tokens appear in localStorage, URLs or logs.
The mistake it prevents
Decoding a JWT without verifying its signature, issuer and audience, or matching redirect_uri with startsWith so an attacker path on the same host captures the code.
Done well means: the implementation uses the flow current guidance recommends for its client type, every redirect and token is validated against spoofing and replay, and tokens live somewhere script and logs cannot read them. OAuth is authorization; OIDC adds authentication. Mixing them up (using an access token as proof of identity) is itself a bug.
Establish the shape first
Read the code and config before judging. Identify:
- Client type: SPA/browser, native/mobile, confidential server-side, or machine-to-machine.
- Flow in use: authorization code (+PKCE), client credentials, device code, or a legacy flow (implicit, ROPC) that should be replaced.
- Role: are you the client (relying party) or operating the authorization server? This review centres on the client / relying-party side.
- Library: prefer a maintained, certified library over hand-rolled endpoints. Hand-rolled OAuth is where most of these bugs live.
| Client type | Use | Not |
|---|---|---|
| SPA / browser | Authorization Code + PKCE, tokens handled by a backend-for-frontend where possible | Implicit (removed in OAuth 2.1), tokens in localStorage |
| Native / mobile | Authorization Code + PKCE, system browser (AppAuth), claimed https redirect | Embedded webview, custom-scheme without PKCE |
| Server web app | Authorization Code + PKCE, client secret, tokens server-side | Tokens in the browser |
| Service-to-service | Client credentials | A user flow with a shared service user |
OAuth 2.1 consolidates current best practice: PKCE for all authorization-code clients, exact redirect-URI matching, no implicit or password grant. Review against that baseline.
Checklist
Authorization request and redirect
-
statepresent, unguessable, bound to the user’s session, and verified on return (CSRF defence). Reject if missing or mismatched. - PKCE:
code_challengewithS256(notplain) sent,code_verifierchecked. Required even for confidential clients now. -
redirect_uriregistered and matched exactly by the server (scheme, host, path, no wildcards, no trailing-slash or subpath tolerance). Open-redirect-style matching is the classic account-takeover bug. - OIDC:
noncesent in the request and verified in the ID token to bind it to this session (replay defence). - No tokens or codes in the URL fragment reaching third-party scripts; no sensitive data in query strings that land in logs/referrers.
Token handling (relying party)
- Authorization code is exchanged server-side (or via BFF), single-use, short-lived.
- ID token validated fully: signature against the IdP’s JWKS (
kid, rotate-aware),issexact match,audequals this client ID,exp/iat/nbf, andnonce. Never trust an unverified JWT or acceptalg: none. - Access tokens are treated as opaque by the client unless it is the intended audience; do not read claims from someone else’s access token to make decisions.
- The userinfo/ID-token
sub(stable, per-issuer) is the account key, not email (email is mutable and can be unverified). Checkemail_verifiedbefore trusting email. - Storage: web — HttpOnly, Secure, SameSite cookies, not localStorage/sessionStorage; mobile — Keychain/Keystore, not plaintext prefs.
Scopes, refresh, logout
- Request the least scope needed; review any admin/offline scope.
- Refresh tokens for public clients are rotated (new refresh token each use) with reuse detection that revokes the chain; confidential clients store them server-side.
- Token revocation on logout; back-channel logout or short access-token lifetime so revocation is effective. Clearing a cookie is not revocation.
Provider side (if you operate the AS)
- Exact redirect-URI registration, PKCE enforced, codes single-use and short TTL, consent for third-party clients, and no token leakage via error redirects.
High-severity patterns to grep for
rg -n -i "response_type=token|grant_type=password|implicit" . # legacy flows
rg -n -i "localStorage|sessionStorage" . | rg -i "token|access|id_token|refresh"
rg -n -i "redirect_uri" . | rg -i "startswith|includes|indexOf|regex|\\*" # loose matching
rg -n -i "state" . | rg -i "oauth|authorize" | rg -vi "verify|compare|===|equals" # unchecked state
rg -n -i "alg.{0,10}none|verify=false|skipVerification|insecure" # disabled validation
rg -n -i "decode(?!.*verify)|jwt\\.decode\\(" . # decode without verify
Each hit is a lead; confirm against the code path before calling it a finding.
Common account-takeover chains
- Loose
redirect_urimatching + attacker-controlled path on the same host → code/token sent to the attacker. - Missing
state→ login CSRF, attacker logs victim into the attacker’s account (or vice versa). - Accepting an ID token whose
audis a different client, or skipping signature verification → forged identity. - Trusting unverified
emailfrom the IdP to match an existing local account → takeover by registering that email at a sloppy IdP. - Access token from one app replayed at another that does not check
aud.
Verification
- Replay an authorization response with a tampered or absent
state: the client must reject it. - Register a lookalike
redirect_uri(extra path, different case, trailing slash): the AS must refuse it. - Present an ID token signed by a different key or with
audset to another client: rejected. - Confirm tokens are absent from
localStorage, URLs, and server/access logs (rg "access_token|id_token" logs). - For refresh rotation: use a refresh token twice and confirm the second use revokes the session.
Output template
## OAuth/OIDC review: web app "Sign in with Acme" (authorization code + PKCE)
| ID | Severity | Finding | Location | Fix |
|---|---|---|---|---|
| O-1 | Critical | ID token signature not verified (decode only) | auth/callback.ts:38 | Verify against JWKS; check iss/aud/exp/nonce |
| O-2 | High | redirect_uri matched with startsWith | auth/config.ts:12 | Exact-match against the registered set |
| O-3 | Medium | Access token in localStorage | hooks/useAuth.ts:21 | Move to HttpOnly cookie via BFF |
Baseline: OAuth 2.1 / current BCP. PKCE: present (S256). Not reviewed: the IdP's own config. 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: "OAuth Security Review"
short_description: "Catch the takeover before it ships"
default_prompt: "$oauth-security-review Review our Sign in with Google integration for security issues" 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.