oauth-flow
Implements OAuth 2.0 and OpenID Connect correctly - authorization code with PKCE, client credentials for machine-to-machine, state and nonce, exact redirect URI matching, safe token storage, refresh rotation with reuse detection, scopes, ID token validation via JWKS, and logout - aligned with RFC 9700 (OAuth Security BCP) and RFC 7636 (PKCE). Use when someone asks "add Google or Microsoft login", "implement OAuth in our app", "where should I store the refresh token", "how do I validate an ID token", or is wiring an app to an identity provider or third-party API. Do NOT use for auditing a whole application's security posture - use security-audit instead; for line-level review of an auth diff, use secure-code-review.
- License 许可
- MIT
- 最近更新
- 2026-10-14
- Stars
- ★ 25
- Forks
- 3
01 前言 Front Matter
- name
- oauth-flow
- metadata
- { "title": "OAuth & OIDC Flow" }
- description
- Implements OAuth 2.0 and OpenID Connect correctly - authorization code with PKCE, client credentials for machine-to-machine, state and nonce, exact redirect URI matching, safe token storage, refresh rotation with reuse detection, scopes, ID token validation via JWKS, and logout - aligned with RFC 9700 (OAuth Security BCP) and RFC 7636 (PKCE). Use when someone asks "add Google or Microsoft login", "implement OAuth in our app", "where should I store the refresh token", "how do I validate an ID token", or is wiring an app to an identity provider or third-party API. Do NOT use for auditing a whole application's security posture - use security-audit instead; for line-level review of an auth diff, use secure-code-review.
03 SKILL.md 正文
OAuth & OIDC Flow
OAuth bugs fail silently: a login that works in the demo can still accept forged callbacks, leak refresh tokens to page scripts, or link an attacker's account to a victim's. The costly mistake this skill prevents is copying a flow that "works" - implicit grant, tokens in localStorage, unchecked state, an ID token decoded but never verified - and shipping an account-takeover path no functional test will catch.
Operating procedure
Decide the grant and token storage before writing a route; most OAuth flaws are architecture choices.
Step 1: Choose the grant
- A user signs in (web, SPA, native): authorization code + PKCE. RFC 9700 requires PKCE for public clients and recommends it for confidential ones - use it for both.
- No user present: client credentials, confidential clients only.
- Never implicit (fragment tokens leak via history and scripts) or password credentials (RFC 9700: must not be used).
- SPAs: prefer a backend-for-frontend (BFF) that holds tokens and gives the browser only an httpOnly session cookie.
- Native apps: system browser, not a webview (RFC 8252).
Step 2: Register the client tightly
- Redirect URIs use exact string matching - no wildcards or prefixes; only a native app's loopback port may vary.
- Separate clients and redirect URIs per environment.
- Prefer private_key_jwt or mTLS client auth over shared secrets; secrets stay server-side.
- Request minimum scopes, add more incrementally, and check the granted scope in the token response.
Step 3: Build the authorization request
- code_verifier: 43-128 characters from [A-Z a-z 0-9 - . _ ~]; 32 random bytes base64url-encoded gives 43.
- code_challenge = BASE64URL(SHA-256(code_verifier)), unpadded, with code_challenge_method=S256. Never plain.
- state: at least 128 random bits, bound to the browser session, single use. PKCE blocks code injection, but state still stops login CSRF where you cannot verify the provider enforces PKCE.
- nonce (OIDC): random, later compared to the ID token's nonce claim.
Keep verifier, state, nonce, and return path in a server-side transaction or encrypted httpOnly cookie (~10 minute TTL). Allowlist the return path, or the callback becomes an open redirector.
Step 4: Handle the callback
- On error show a generic failure. Compare state in constant time; reject on mismatch or missing transaction; delete the transaction (single use).
- If an iss parameter is returned (RFC 9207), require the expected issuer - this blocks mix-up attacks.
- Exchange the code server-side with the identical redirect_uri, the code_verifier, and client authentication.
- Validate the ID token (Step 5) before creating a session; rotate the session ID; redirect to the validated return path.
Step 5: Validate the ID token
Never just decode it. Verify:
- Signature via the provider's JWKS, key chosen by kid; on an unknown kid refetch once (rate limited) to follow rotation.
- alg in an explicit allowlist (e.g. RS256, ES256); reject none.
- iss exactly equals the discovered issuer; aud contains your client_id (with multiple audiences, azp equals client_id).
- exp in the future, clock skew of 60 seconds or less; nonce equals the stored value.
Key users by (iss, sub), never email; auto-link by email only when email_verified is true and the provider is authoritative for the domain. The ID token is not an API credential - APIs take the access token and check its audience.
Step 6: Store tokens
- Web apps and BFFs: server-side session store or an encrypted httpOnly, Secure, SameSite cookie.
- SPAs without a backend: access token in memory only; never refresh tokens in localStorage or sessionStorage - any XSS reads them.
- Native: Keychain or Android Keystore.
- Never put tokens in URLs, logs, or error reports.
Step 7: Refresh with rotation and reuse detection
- Each refresh issues a new refresh token and invalidates the old one; presentation of an already-used token revokes the whole token family.
- Public clients must use rotation or sender-constrained tokens (DPoP or mTLS) per RFC 9700.
- Make refresh single-flight so concurrent tabs do not trip reuse detection; refresh ~60 seconds before exp.
Step 8: Logout
Destroy the session, revoke the refresh token (RFC 7009), and for single sign-out redirect to end_session_endpoint with id_token_hint. Clearing a cookie alone leaves the refresh token valid.
Worked example
Next.js App Router BFF using jose. seal/unseal, createSession, fail, and basicAuth are your helpers.
// imports: NextRequest, NextResponse (next/server); createRemoteJWKSet, jwtVerify (jose);
// randomBytes, createHash, timingSafeEqual (node:crypto)
// app/api/auth/login/route.ts
export async function GET(req: NextRequest) {
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const state = randomBytes(32).toString("base64url");
const nonce = randomBytes(32).toString("base64url");
const returnTo = safeReturnPath(req.nextUrl.searchParams.get("returnTo"));
const url = new URL(OIDC.authorization_endpoint);
url.search = new URLSearchParams({
response_type: "code", client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
scope: "openid email profile", state, nonce,
code_challenge: challenge, code_challenge_method: "S256",
}).toString();
const res = NextResponse.redirect(url);
res.cookies.set("oauth_tx", await seal({ verifier, state, nonce, returnTo }), {
httpOnly: true, secure: true, sameSite: "lax", path: "/api/auth", maxAge: 600,
});
return res;
}
// app/api/auth/callback/route.ts
const JWKS = createRemoteJWKSet(new URL(OIDC.jwks_uri));
export async function GET(req: NextRequest) {
const p = req.nextUrl.searchParams;
const tx = await unseal(req.cookies.get("oauth_tx")?.value);
const state = p.get("state") ?? "";
const code = p.get("code");
if (!tx || !code || state.length !== tx.state.length ||
!timingSafeEqual(Buffer.from(state), Buffer.from(tx.state))) return fail();
if (p.get("iss") && p.get("iss") !== OIDC.issuer) return fail();
const r = await fetch(OIDC.token_endpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded",
Authorization: basicAuth(CLIENT_ID, CLIENT_SECRET) },
body: new URLSearchParams({ grant_type: "authorization_code", code,
redirect_uri: REDIRECT_URI, code_verifier: tx.verifier }),
});
if (!r.ok) return fail();
const tokens = await r.json();
const v = await jwtVerify(tokens.id_token, JWKS, { issuer: OIDC.issuer,
audience: CLIENT_ID, algorithms: ["RS256"], clockTolerance: 60,
}).catch(() => null); // throws on any failed check
if (!v || v.payload.nonce !== tx.nonce) return fail();
const { payload } = v;
const res = NextResponse.redirect(new URL(tx.returnTo, req.url));
res.cookies.delete({ name: "oauth_tx", path: "/api/auth" });
await createSession(res, { userKey: `${payload.iss}|${payload.sub}`,
refreshToken: tokens.refresh_token });
return res;
}
With response_mode=form_post (Apple, when requesting name or email) the callback is a cross-site POST that drops SameSite=Lax cookies: use SameSite=None; Secure and read the form body.
Provider gotchas
- Google: refresh tokens need access_type=offline and usually arrive only on first consent.
- Microsoft Entra ID: on the common or organizations endpoints iss carries the user's tenant ID - validate against an allowlist of tenant issuers.
- GitHub OAuth Apps are not OIDC: no ID token; key on the numeric user ID.
- Apple: name arrives only on first authorization; the client secret is a JWT you sign, valid six months at most.
Thresholds
- code_verifier 43-128 chars; state and nonce at least 128 bits; transaction TTL ~10 minutes, single use.
- Access tokens 5-60 minutes; rotated refresh tokens with idle timeout and absolute lifetime (e.g. 30-90 days).
- ID token clock skew 60 seconds or less; one refresh-and-retry per 401.
Deliverable
A working integration plus a design note (grant, client auth, redirect URIs per environment, token locations, rotation, revocation, ID token checks, logout) and negative tests for state mismatch, nonce mismatch, wrong audience, expired token, and refresh token reuse.
Do NOT
- Do not use implicit or password grants, PKCE plain, or wildcard redirect URIs.
- Do not store refresh tokens in localStorage, sessionStorage, or non-httpOnly cookies.
- Do not accept an ID token without verifying signature, iss, aud, exp, and nonce; never accept alg none.
- Do not use the ID token as an API bearer token, or link accounts by unverified email.
Quality bar
- Every check (state, nonce, iss, aud, exp, signature, reuse) has a negative test that fails closed.
- No token in a URL, log line, or script-readable storage; key rotation needs no deploy; logout makes the refresh token unusable.
Escalation
Use security-audit for the surrounding app, secure-code-review for the auth diff, threat-model-stride for account-linking designs, secrets-hygiene for a leaked client secret, and api-design for how APIs accept access tokens.
04 同仓库更多 Skills
有氧运动计划
zone-2-cardio-plan
当有人要求制定稳态有氧训练计划、进行有氧基础训练或达到第二区间、安排轻松或耐力型的每周有氧训练、按心率区间训练,或提高静息心率、最大摄氧量或耐力时,可以使用此方案。该方案构建了一个包含训练计划结构、训练方式选择和进度安排的心率区间有氧训练模块。本方案仅提供一般健身指导,不构成医疗建议。请勿用于设计力量或举重训练计划(请使用力量训练计划)、高强度间歇训练 (HIIT) 或循环训练(请使用体能训练和 HIIT 计划),或灵活性/柔韧性训练(请使用灵活性训练计划)。
youtube-script-writer
Writes retention-engineered long-form YouTube scripts on a hook/body/payoff architecture - a first-30-seconds hook that survives the retention cliff, a beat-sheet body with mini-hooks and bridges, timed pattern interrupts, and a payoff that over-delivers - with B-roll and editing cues marked inline. Use when someone says "script my next video", "my retention graph dies at the start", "tighten this draft", or "outline a video on X". Do NOT use for the 3-second opener of TikTok, Reels, or Shorts - use video-hook-writer instead; do NOT use for spoken voiceover synced to on-screen tutorial actions - use narration-script instead; do NOT use for a sales demo built on a buyer's discovery notes - use demo-script instead; do NOT use to design the thumbnail and title - use thumbnail-concept instead.
win-back-campaign
Designs segmented win-back email sequences for dormant users - dormancy tiers, a three-beat sequence with a single proportional incentive, and a suppression rule - and measures success by re-activation, not opens. Use when someone says "our users went quiet", "design a win-back campaign", "how do I re-engage lapsed customers", "should we email people who haven't logged in for months", or lifecycle messaging has stopped landing with a lapsed segment. Do NOT use for recovering an unfinished checkout - use abandoned-cart-sequence instead. Do NOT use for diagnosing and fixing why active customers cancel - use churn-reduction instead. Do NOT use for building always-on onboarding or nurture flows - use email-drip-builder instead.
wikipedia-style
Writes neutral, verifiable, encyclopedic prose following Wikipedia's core content policies and manual of style - NPOV, inline citations, notability-backed claims, and a strict ban on promotional language. Use when someone asks "write this like a Wikipedia article", "draft a Wikipedia page for this company or person", "make this neutral and encyclopedic", or "why does my article draft keep getting rejected". Do NOT use for persuasive or opinion writing - use op-ed-writer instead; for thesis-driven academic papers, use academic-essay; for verifying claims in existing text, use fact-checker.
whitepapers
Structures and writes authoritative B2B whitepapers - a standalone executive summary, a data-sized problem statement, methodology, evidence-driven findings, recommendations, and a soft call to action - flagging every claim that needs a real citation. Use when a marketer asks "write a whitepaper on this topic", "turn our research into a lead-gen asset", "make this credible enough for executives", or a draft reads like a product pitch instead of analysis. Do NOT use for customer-story-driven proof - use case-study-builder instead; for policy audiences, use policy-brief.
white-space-analysis
Finds unmet market gaps by ranking jobs-to-be-done on importance versus satisfaction, overlaying them against current solutions and workarounds, and validating that each gap is real, winnable, and worth winning. Use when someone asks "where are the gaps in this market", "what's underserved here", "where should we play next", or is hunting for product, feature, or market-entry opportunities. Do NOT use for profiling competitors and building battle cards - use competitive-intelligence instead; for putting a dollar figure on an identified opportunity - use market-sizing instead; for extracting jobs-to-be-done from raw customer interviews - use jtbd-extractor instead.