Security

oauth-flow

$npx skills add blunotech-dev/agents --skill oauth-flow

Implement or audit OAuth 2.0 authorization code flow. Use this skill for OAuth, PKCE, authorization codes, token exchange, scopes, redirect URIs, state parameter, social login, or "login with Google/GitHub/etc." — including narrow questions like "do I need PKCE?" or "what scopes should I request?".

References

2 files
namedescriptioncategory
oauth-flowImplement or audit OAuth 2.0 authorization code flow. Use this skill for OAuth, PKCE, authorization codes, token exchange, scopes, redirect URIs, state parameter, social login, or "login with Google/GitHub/etc." — including narrow questions like "do I need PKCE?" or "what scopes should I request?".Security

OAuth 2.0 Authorization Code Flow

Flow at a Glance

1. Client → Auth Server: GET /authorize?response_type=code&client_id=...&state=...&code_challenge=...
2. Auth Server → Client: redirect to /callback?code=...&state=...
3. Client → Auth Server: POST /token { code, code_verifier, redirect_uri }
4. Auth Server → Client: { access_token, refresh_token, expires_in }
5. Client → Resource Server: GET /api with Authorization: Bearer <access_token>

Rule: Always response_type=code. Never response_type=token (implicit flow — deprecated).


PKCE

Required for all public clients (SPAs, mobile). Recommended for server-side too.

// 1. Generate before redirect
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = base64url(await crypto.subtle.digest('SHA-256', encode(verifier)));
sessionStorage.setItem('pkce_verifier', verifier);

// 2. Authorization request
params.set('code_challenge', challenge);
params.set('code_challenge_method', 'S256'); // never 'plain'

// 3. Token exchange
body.set('code_verifier', sessionStorage.getItem('pkce_verifier')!);
sessionStorage.removeItem('pkce_verifier');

function base64url(buf: ArrayBuffer | Uint8Array) {
  return btoa(String.fromCharCode(...new Uint8Array(buf as ArrayBuffer)))
    .replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'');
}

State Parameter

Prevents CSRF. Must be cryptographically random, stored, and validated before token exchange.

// Before redirect
const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
sessionStorage.setItem('oauth_state', state);

// On callback — validate first, then use code
const returned = params.get('state');
const saved = sessionStorage.getItem('oauth_state');
sessionStorage.removeItem('oauth_state');
if (!returned || returned !== saved) throw new Error('State mismatch');

Tip: Embed returnTo path inside state — btoa(JSON.stringify({ nonce, returnTo })) — gives CSRF protection plus post-login redirect in one field.


Authorization Request

const url = new URL('https://provider.com/authorize');
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', REDIRECT_URI); // exact match, HTTPS
url.searchParams.set('scope', 'openid profile email');
url.searchParams.set('state', state);
url.searchParams.set('code_challenge', challenge);
url.searchParams.set('code_challenge_method', 'S256');

Callback Handling

const params = new URLSearchParams(location.search);

// 1. Error first
if (params.get('error')) throw new Error(params.get('error_description')!);

// 2. State before code
validateState(params.get('state'));

// 3. Exchange
const code = params.get('code')!;
const tokens = await exchangeCode(code);

Token Exchange

Public client (SPA):

const res = await fetch('https://provider.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code, redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: verifier,
  }),
});

Confidential client (server-side): swap code_verifier for client_secret (or use both).


Scopes

  • Request minimum necessary; add more incrementally when features need them
  • Validate granted scopes — server may return fewer than requested
  • Include offline_access (or provider equivalent) to get a refresh token
  • Include openid for OIDC id_token

Common Mistakes

MistakeWhy it's wrongFix
response_type=token (implicit)Token in URL → leaks into history/logsUse response_type=code
No state / static stateCSRF → attacker links victim session to their accountRandom state per request
No PKCECode interception → attacker exchanges codePKCE with S256
Token in redirect URLLeaks in browser history, server logs, RefererTokens in body/headers only
Skip id_token validationCan't verify issuer, audience, expiryValidate sig + iss + aud + exp
Open post-login redirectPhishing via ?next=https://evil.comAllowlist or same-origin check
client_secret in frontendExtractable from bundlePublic clients use PKCE only
localStorage for tokensXSS exfiltrates tokensMemory (access) + httpOnly cookie (refresh)
code_challenge_method=plainNo security — challenge == verifierAlways S256

id_token Validation (OIDC)

Always validate when using OpenID Connect:

  • Signature against provider's JWKS endpoint
  • iss = expected issuer
  • aud = your client_id
  • exp > now (allow ≤30s clock skew)
  • nonce matches what you sent

Use a library (jose, openid-client, python-jose). Never roll your own.


Flow Selection

Client typeFlow
SPA / mobileAuthorization code + PKCE (no secret)
Server-side web appAuthorization code + client secret (+ PKCE)
Machine-to-machineClient credentials (no user)
Input-constrained deviceDevice authorization flow

Audit Checklist

  • response_type=code, not token
  • PKCE with S256 on all client types
  • State: random per request, validated before code use
  • redirect_uri: exact string, HTTPS, registered with provider
  • Callback errors handled (?error=)
  • Tokens never in URLs
  • id_token validated (if OIDC)
  • Post-login redirect: allowlist or same-origin only
  • client_secret server-side only, not in frontend bundles
  • Minimum scopes; granted scopes validated
  • Access token in memory; refresh token in httpOnly cookie

Reference Files

Load on demand:

  • references/stack-examples.md — Node/Express, Python/FastAPI, SPA full implementations
  • references/provider-quirks.md — Google, GitHub, Microsoft, Auth0 provider-specific notes