Back to skills
extension
Category: Development & EngineeringNo API key required

oauth2

OAuth 2.0 authorization framework. Covers flows, tokens, and provider integration. Use for third-party authentication. USE WHEN: user mentions "OAuth", "Google login", "GitHub auth", "social login", "authorization code flow", "PKCE", asks about "third-party auth", "provider integration" DO NOT USE FOR: JWT tokens (use jwt skill), NextAuth.js (use nextauth skill), API keys, simple password auth

personAuthor: jakexiaohubgithub

OAuth 2.0 Core Knowledge

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: oauth2 for comprehensive documentation.

Authorization Code Flow (Recommended)

1. User clicks "Login with Google"
2. Redirect to provider:
   GET https://accounts.google.com/oauth/authorize
     ?client_id=xxx
     &redirect_uri=https://app.com/callback
     &response_type=code
     &scope=openid email profile
     &state=random_state

3. User authorizes, provider redirects:
   GET https://app.com/callback?code=xxx&state=random_state

4. Backend exchanges code for tokens:
   POST https://oauth2.googleapis.com/token
     client_id=xxx
     client_secret=xxx
     code=xxx
     grant_type=authorization_code
     redirect_uri=https://app.com/callback

5. Receive tokens:
   { "access_token": "...", "refresh_token": "...", "id_token": "..." }

Implementation

// Step 1: Generate auth URL
function getAuthUrl(): string {
  const params = new URLSearchParams({
    client_id: process.env.GOOGLE_CLIENT_ID,
    redirect_uri: `${process.env.APP_URL}/callback`,
    response_type: 'code',
    scope: 'openid email profile',
    state: generateRandomState(),
  });
  return `https://accounts.google.com/oauth/authorize?${params}`;
}

// Step 2: Handle callback
async function handleCallback(code: string) {
  const tokens = await exchangeCodeForTokens(code);
  const userInfo = await getUserInfo(tokens.access_token);
  const user = await findOrCreateUser(userInfo);
  return generateSessionToken(user);
}

When NOT to Use This Skill

  • Simple JWT authentication - Use jwt skill for custom token-based auth
  • NextAuth.js integration - Use nextauth skill for Next.js projects
  • Internal authentication - Use traditional username/password with JWT
  • API-to-API communication - Use API keys or mTLS

Common Flows

| Flow | Use Case | |------|----------| | Authorization Code | Web apps (server-side) | | Authorization Code + PKCE | SPAs, mobile apps | | Client Credentials | Machine-to-machine | | Refresh Token | Long-lived sessions |

Anti-Patterns

| Anti-Pattern | Why It's Bad | Correct Approach | |--------------|--------------|------------------| | No state parameter | Vulnerable to CSRF attacks | Always generate and validate state | | PKCE without S256 | Weak code challenge | Use S256 (SHA-256), not plain | | Storing tokens in localStorage | XSS vulnerability | Use httpOnly cookies or secure storage | | Ignoring provider errors | Silent failures | Handle all error codes properly | | Hardcoded redirect URLs | Security risk | Use environment variables | | No nonce validation | ID token replay attacks | Validate nonce for OpenID Connect |

Quick Troubleshooting

| Issue | Cause | Solution | |-------|-------|----------| | "Invalid redirect_uri" | URL mismatch with provider | Exact match required, check protocol/trailing slash | | "Invalid state" | CSRF token mismatch | Verify state cookie exists and matches | | "Invalid code" | Code expired or used twice | Codes expire in ~10 minutes, can only be used once | | "Invalid client" | Wrong client_id/secret | Verify credentials from provider console | | CORS errors | Same-origin policy | Use backend proxy for token exchange | | "Invalid grant" | Code verifier mismatch | Ensure code_verifier matches code_challenge |

PKCE Extension

// For SPAs - no client_secret needed
const codeVerifier = generateRandomString(64);
const codeChallenge = base64url(sha256(codeVerifier));

// Add to auth URL
params.set('code_challenge', codeChallenge);
params.set('code_challenge_method', 'S256');

// Include in token exchange
body.code_verifier = codeVerifier;

Production Readiness

Security Configuration

// Secure state parameter (CSRF protection)
import { randomBytes, createHash } from 'crypto';

function generateState(): string {
  return randomBytes(32).toString('hex');
}

// Store state in httpOnly cookie before redirect
res.cookie('oauth_state', state, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax', // Required for OAuth redirects
  maxAge: 10 * 60 * 1000, // 10 minutes
});

// Validate state on callback
function validateState(receivedState: string, storedState: string): void {
  if (!receivedState || !storedState || receivedState !== storedState) {
    throw new Error('Invalid state parameter - possible CSRF attack');
  }
}

PKCE Implementation (Required for SPAs/Mobile)

// Generate PKCE parameters
function generatePKCE(): { verifier: string; challenge: string } {
  const verifier = randomBytes(32)
    .toString('base64url')
    .replace(/[^a-zA-Z0-9]/g, '')
    .substring(0, 64);

  const challenge = createHash('sha256')
    .update(verifier)
    .digest('base64url');

  return { verifier, challenge };
}

// Store verifier securely (server-side session or encrypted cookie)
const { verifier, challenge } = generatePKCE();
session.codeVerifier = verifier;

// Include in authorization URL
const authUrl = new URL('https://provider.com/oauth/authorize');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

// Include in token exchange
const tokenResponse = await fetch('https://provider.com/oauth/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code,
    code_verifier: session.codeVerifier,
    client_id: process.env.OAUTH_CLIENT_ID!,
    redirect_uri: process.env.OAUTH_REDIRECT_URI!,
  }),
});

Token Handling

// Secure token storage and refresh
async function handleTokens(tokens: OAuthTokens) {
  // Encrypt tokens before storing
  const encryptedAccess = encrypt(tokens.access_token);
  const encryptedRefresh = encrypt(tokens.refresh_token);

  // Store in database with user association
  await db.oauthTokens.upsert({
    where: { userId_provider: { userId, provider: 'google' } },
    create: {
      userId,
      provider: 'google',
      accessToken: encryptedAccess,
      refreshToken: encryptedRefresh,
      expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
    },
    update: {
      accessToken: encryptedAccess,
      refreshToken: encryptedRefresh,
      expiresAt: new Date(Date.now() + tokens.expires_in * 1000),
    },
  });
}

// Auto-refresh expired tokens
async function getValidAccessToken(userId: string): Promise<string> {
  const stored = await db.oauthTokens.findUnique({
    where: { userId_provider: { userId, provider: 'google' } },
  });

  if (!stored) throw new Error('No OAuth tokens found');

  // Refresh if expired or expiring soon
  if (stored.expiresAt < new Date(Date.now() + 5 * 60 * 1000)) {
    const newTokens = await refreshOAuthToken(decrypt(stored.refreshToken));
    await handleTokens(newTokens);
    return newTokens.access_token;
  }

  return decrypt(stored.accessToken);
}

Provider Verification

// Verify ID token (for OpenID Connect)
import * as jose from 'jose';

async function verifyIdToken(idToken: string, provider: string): Promise<jose.JWTPayload> {
  const JWKS = jose.createRemoteJWKSet(
    new URL('https://www.googleapis.com/oauth2/v3/certs')
  );

  const { payload } = await jose.jwtVerify(idToken, JWKS, {
    issuer: 'https://accounts.google.com',
    audience: process.env.GOOGLE_CLIENT_ID!,
  });

  // Verify nonce if used
  if (payload.nonce !== session.nonce) {
    throw new Error('Invalid nonce');
  }

  return payload;
}

Monitoring Metrics

| Metric | Alert Threshold | |--------|-----------------| | OAuth callback failures | > 50/hour | | State validation failures | > 10/hour | | Token refresh failures | > 20/hour | | Invalid provider responses | > 5/hour |

Error Handling

async function handleOAuthCallback(req: Request) {
  try {
    // Check for provider errors
    if (req.query.error) {
      const error = req.query.error as string;
      const description = req.query.error_description as string;

      if (error === 'access_denied') {
        // User cancelled - redirect to login
        return redirect('/login?cancelled=true');
      }

      throw new OAuthError(error, description);
    }

    // Validate state
    validateState(req.query.state, req.cookies.oauth_state);

    // Exchange code for tokens
    const tokens = await exchangeCode(req.query.code);

    // Create/update user
    const user = await findOrCreateUser(tokens);

    // Create session
    await createSession(user);

  } catch (error) {
    // Log security events
    logger.warn('OAuth callback error', {
      error: error.message,
      ip: req.ip,
      provider: 'google',
    });

    return redirect('/login?error=oauth_failed');
  }
}

Checklist

  • [ ] State parameter for CSRF protection
  • [ ] PKCE for all public clients (SPAs, mobile)
  • [ ] Validate state before code exchange
  • [ ] Verify ID token signature and claims
  • [ ] Encrypt stored OAuth tokens
  • [ ] Auto-refresh expired tokens
  • [ ] Handle provider errors gracefully
  • [ ] Log all OAuth security events
  • [ ] Use localhost for dev redirect URIs
  • [ ] Strict redirect URI validation
  • [ ] Rate limit callback endpoint

Reference Documentation