[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Secure an API with OAuth 2.0, Rate Limiting, and Security Scanning

Secure an API with OAuth 2.0, Rate Limiting, and Security Scanning

Harden a production API with OAuth 2.0 + PKCE authentication, tiered rate limiting, secrets management with Vault, and automated security scanning in CI — a complete security posture for a SaaS API.

DevOps#oauth2#oidc#authentication#authorization#jwt
Works with:claude-codeopenai-codexgemini-clicursor
agent@terminalskills — playground
full playground →

Try this use case on your own files

simulated preview
$
$

real run in an isolated sandbox · files auto-deleted after 7 days · nothing touches your machine

Skills stack · 4 skills

Avg quality 92/100·All SAFE
>

oauth2-oidc

v1.0.0

Assists with implementing OAuth 2.0 and OpenID Connect for secure authentication and authorization. Use when configuring authorization flows, validating tokens, implementing PKCE for public clients, setting up social login, or building secure token handling for SPAs, mobile apps, and APIs. Trigger words: oauth, oidc, openid connect, authorization code, pkce, jwt, access token, refresh token.

87/100 quality
1.56× impact
SAFE
View skill
>

rate-limiter

v1.1.0

Designs and implements API rate limiting middleware and abuse protection. Use when you need to add request throttling, per-user quotas, IP-based blocking, sliding window or token bucket algorithms, Redis-backed distributed counters, or 429 response handling. Trigger words: rate limit, throttle, API abuse, too many requests, request quota, DDoS protection, brute force prevention, credential stuffing defense.

93/100 quality
1.66× impact
SAFE
View skill
>

hashicorp-vault

v1.0.0

Assists with managing secrets, encryption keys, and dynamic credentials using HashiCorp Vault. Use when configuring secret engines, setting up dynamic database credentials, implementing access policies, integrating with Kubernetes, or using Transit for encryption as a service. Trigger words: vault, secrets management, dynamic secrets, transit engine, pki, approle, vault agent.

87/100 quality
5.06× impact
SAFE
View skill
>

owasp-zap

v1.1.0

OWASP ZAP (Zed Attack Proxy) is an open-source web application security scanner that crawls a site, passively inspects traffic and optionally attacks it to find XSS, SQL injection, missing security headers and other OWASP Top 10 issues. Use when configuring automated scans, running zap-baseline, zap-full-scan or zap-api-scan, adding security scanning to CI/CD or GitHub Actions, or triaging scan results. Trigger words: owasp zap, security scan, vulnerability scanner, penetration testing, zap-baseline, active scan, passive scan.

100/100 quality
0.99× impact
SAFE
View skill
$

Diego runs a fintech API that processes 2 million requests per day. A security audit flagged four issues: API keys in environment variables (no rotation), no rate limiting (one customer's automation script caused a 10-minute outage), basic auth on some endpoints (passwords in headers), and no automated security testing. He needs production-grade security before the company's SOC 2 audit in 8 weeks.

Step 1 — Replace API Keys with OAuth 2.0 + PKCE

Static API keys are the biggest risk: they don't expire, can't be scoped, and are often leaked in logs, Git history, or Slack messages. OAuth 2.0 with short-lived access tokens limits the blast radius of any leak.

typescript
// src/auth/oauth-config.ts — OAuth 2.0 provider configuration.
// Uses the provider's OIDC discovery endpoint to auto-configure URLs.
// Supports both user-facing (Authorization Code) and machine (Client Credentials) flows.

import { Issuer, Strategy } from "openid-client";

// Auto-discover endpoints from the OIDC discovery URL
const issuer = await Issuer.discover(process.env.OIDC_ISSUER_URL!);
// issuer.metadata contains: authorization_endpoint, token_endpoint, jwks_uri, etc.

export const oauthClient = new issuer.Client({
  client_id: process.env.OAUTH_CLIENT_ID!,
  client_secret: process.env.OAUTH_CLIENT_SECRET!,
  redirect_uris: [process.env.OAUTH_REDIRECT_URI!],
  response_types: ["code"],
  token_endpoint_auth_method: "client_secret_post",
});

// JWKS for token verification — cached and auto-rotated
export const JWKS = issuer.metadata.jwks_uri;
typescript
// src/middleware/auth.ts — JWT validation middleware.
// Validates the access token on every API request.
// Checks signature, expiration, issuer, audience, and scopes.

import { createRemoteJWKSet, jwtVerify, JWTPayload } from "jose";
import { Request, Response, NextFunction } from "express";

const JWKS = createRemoteJWKSet(
  new URL(process.env.JWKS_URI!)
);

interface AuthenticatedRequest extends Request {
  user: JWTPayload & {
    sub: string;
    scope: string;
    org_id?: string;
    tier: "free" | "pro" | "enterprise";
  };
}

export async function requireAuth(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({
      error: "unauthorized",
      message: "Missing or invalid Authorization header",
    });
  }

  const token = authHeader.slice(7);

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: process.env.OIDC_ISSUER_URL,
      audience: process.env.OAUTH_AUDIENCE,         // Ensures token was issued for this API
      clockTolerance: 5,                             // 5-second clock skew tolerance
    });

    (req as AuthenticatedRequest).user = payload as any;
    next();
  } catch (err: any) {
    if (err.code === "ERR_JWT_EXPIRED") {
      return res.status(401).json({
        error: "token_expired",
        message: "Access token has expired. Use your refresh token to get a new one.",
      });
    }

    return res.status(401).json({
      error: "invalid_token",
      message: "Token validation failed",
    });
  }
}

// Scope-based authorization: check that token has required permissions
export function requireScope(...requiredScopes: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const user = (req as AuthenticatedRequest).user;
    const tokenScopes = user.scope?.split(" ") || [];

    const hasAllScopes = requiredScopes.every((s) => tokenScopes.includes(s));

    if (!hasAllScopes) {
      return res.status(403).json({
        error: "insufficient_scope",
        message: `Required scopes: ${requiredScopes.join(", ")}`,
        required: requiredScopes,
        granted: tokenScopes,
      });
    }

    next();
  };
}

Step 2 — Implement Tiered Rate Limiting

Different customer tiers get different limits. The implementation uses Redis for distributed counting across multiple API server instances, and a sliding window algorithm for accurate rate enforcement.

typescript
// src/middleware/rate-limiter.ts — Tiered rate limiting with Redis.
// Sliding window counter: accurate, distributed, memory-efficient.
// Returns standard rate limit headers so clients can implement backoff.

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { Request, Response, NextFunction } from "express";

// Initialize Redis-backed rate limiter
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

// Tier-based rate limits
const limiters = {
  free: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(100, "1 m"),     // 100 requests/minute
    prefix: "rl:free",
    analytics: true,
  }),
  pro: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(1000, "1 m"),    // 1,000 requests/minute
    prefix: "rl:pro",
    analytics: true,
  }),
  enterprise: new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(10000, "1 m"),   // 10,000 requests/minute
    prefix: "rl:enterprise",
    analytics: true,
  }),
};

// Endpoint-specific cost multipliers.
// Expensive operations consume more of the quota.
const endpointCosts: Record<string, number> = {
  "POST /api/transactions/export": 10,    // Heavy DB query + file generation
  "POST /api/reports/generate": 20,       // Report compilation is expensive
  "GET /api/transactions": 1,             // Standard read
  "POST /api/transactions": 2,            // Write operation
};

export async function rateLimit(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const user = (req as any).user;
  if (!user) return next();  // Skip if not authenticated (auth middleware handles it)

  const tier = user.tier || "free";
  const limiter = limiters[tier as keyof typeof limiters] || limiters.free;

  // Compute cost for this request
  const endpointKey = `${req.method} ${req.path}`;
  const cost = endpointCosts[endpointKey] || 1;

  // Rate limit by organization (not individual user) for fairness
  const identifier = user.org_id || user.sub;

  const result = await limiter.limit(identifier, { rate: cost });

  // Set standard rate limit headers
  res.set({
    "X-RateLimit-Limit": String(result.limit),
    "X-RateLimit-Remaining": String(result.remaining),
    "X-RateLimit-Reset": String(Math.ceil(result.reset / 1000)),
  });

  if (!result.success) {
    const retryAfter = Math.ceil((result.reset - Date.now()) / 1000);

    res.set("Retry-After", String(retryAfter));

    return res.status(429).json({
      error: "rate_limit_exceeded",
      message: `Rate limit exceeded. Retry after ${retryAfter} seconds.`,
      limit: result.limit,
      remaining: 0,
      retryAfter,
      tier,
      upgradeUrl: tier === "free" ? "https://api.diego.dev/pricing" : undefined,
    });
  }

  next();
}

Step 3 — Manage Secrets with Vault

typescript
// src/config/vault.ts — Dynamic secret loading from HashiCorp Vault.
// Application fetches secrets at startup and refreshes before TTL expiry.
// No secrets in environment variables, .env files, or code.

import VaultClient from "node-vault";

const vault = VaultClient({
  apiVersion: "v1",
  endpoint: process.env.VAULT_ADDR || "https://vault.internal:8200",
});

// Authenticate with AppRole (machine-to-machine auth)
async function authenticate() {
  const result = await vault.approleLogin({
    role_id: process.env.VAULT_ROLE_ID!,
    secret_id: process.env.VAULT_SECRET_ID!,  // Only 2 env vars needed
  });

  vault.token = result.auth.client_token;

  // Schedule token renewal before expiry
  const ttl = result.auth.lease_duration;
  setTimeout(() => authenticate(), (ttl - 60) * 1000);  // Renew 60s before expiry
}

// Fetch application secrets
export async function getSecrets() {
  await authenticate();

  // Static secrets from KV engine
  const appSecrets = await vault.read("secret/data/fintech-api/config");

  // Dynamic database credentials (auto-expire after 1 hour)
  const dbCreds = await vault.read("database/creds/fintech-api-role");

  // Dynamic Redis credentials
  const redisCreds = await vault.read("database/creds/redis-role");

  return {
    oauthClientSecret: appSecrets.data.data.oauth_client_secret,
    jwtSigningKey: appSecrets.data.data.jwt_signing_key,
    encryptionKey: appSecrets.data.data.encryption_key,
    database: {
      host: appSecrets.data.data.db_host,
      port: 5432,
      username: dbCreds.data.username,     // Dynamic: "v-approle-fintech-a-xyz123"
      password: dbCreds.data.password,     // Dynamic: auto-generated, 1-hour TTL
      database: "fintech",
    },
    redis: {
      username: redisCreds.data.username,
      password: redisCreds.data.password,
    },
  };
}

// Refresh database credentials before they expire
export function scheduleCredentialRefresh(
  onRefresh: (creds: Awaited<ReturnType<typeof getSecrets>>) => void
) {
  // Database creds have 1-hour TTL; refresh every 50 minutes
  setInterval(async () => {
    const newSecrets = await getSecrets();
    onRefresh(newSecrets);
  }, 50 * 60 * 1000);
}

Step 4 — Automate Security Scanning in CI

yaml
# .github/workflows/security.yml — Automated security scanning.
# Runs OWASP ZAP against the staging deployment after every merge to main.
# Baseline scan (passive only) on PRs, full scan weekly.

name: Security Scan
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  schedule:
    - cron: "0 3 * * 1"  # Full scan every Monday at 3 AM

jobs:
  # Quick passive scan on every PR — catches common issues in ~2 minutes
  baseline-scan:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Start application
        run: |
          docker compose -f compose.test.yml up -d
          sleep 10  # Wait for app to be ready

      - name: OWASP ZAP Baseline Scan
        uses: zaproxy/action-baseline@v0.12.0
        with:
          target: "http://localhost:3000"
          rules_file_name: ".zap/baseline-rules.tsv"
          cmd_options: "-a -j"  # Ajax spider + JSON report
          fail_action: "warn"   # Don't block PRs, but flag issues

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-baseline-report
          path: report_html.html

  # Full active scan on staging after merge to main
  full-scan:
    if: github.event_name == 'push' || github.event_name == 'schedule'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4

      - name: OWASP ZAP Full Scan
        uses: zaproxy/action-full-scan@v0.10.0
        with:
          target: ${{ vars.STAGING_URL }}
          rules_file_name: ".zap/full-scan-rules.tsv"
          cmd_options: >-
            -a
            -j
            -z "-config api.addrs.addr.name=.* -config api.addrs.addr.regex=true"
          fail_action: "fail"   # Block deployment on high-severity findings

      - name: Upload report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: zap-full-report
          path: report_html.html

      # Notify on critical findings
      - name: Alert on critical vulnerabilities
        if: failure()
        run: |
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"text": "🚨 OWASP ZAP found critical vulnerabilities in staging. Check the security scan report."}'

Results

Diego completed the security hardening in 5 weeks, well ahead of the SOC 2 audit:

  • Zero static secrets in env vars — Vault manages all credentials with dynamic generation. Database passwords rotate every hour automatically. If a credential leaks, it's valid for at most 60 minutes.
  • API key → OAuth migration: 100% of clients migrated in 3 weeks. Access tokens expire in 15 minutes (vs API keys that never expired). Token theft impact reduced from "indefinite access" to "15-minute window."
  • Rate limiting prevented 3 incidents in the first month — one customer's runaway script hit the 1,000/min pro tier limit instead of overwhelming the database. The 429 response with Retry-After header let the script's backoff logic handle it gracefully.
  • OWASP ZAP caught 12 issues in the first full scan: 2 high (reflected XSS in search, missing CSRF token on settings), 4 medium (security headers, cookie flags), 6 low. All high/medium fixed within a week.
  • SOC 2 audit passed — the auditor specifically noted the dynamic secret rotation, automated security scanning, and rate limit logging as strong controls. Vault's audit log provided the access trail required for compliance.
  • Rate limit headers improved client experience — third-party developers integrating with the API reported that X-RateLimit-Remaining let them build efficient polling without guessing. Support tickets about "random 500 errors" (actually overload) dropped to zero.

Related use cases